This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
import Link from "next/link";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import dynamic from "next/dynamic";
|
||||
import { useRouter } from "next/navigation";
|
||||
import {
|
||||
Badge,
|
||||
@@ -17,6 +18,17 @@ import {
|
||||
} from "@/components/ui";
|
||||
import { apiFetch, ApiClientError } from "@/lib/client-api";
|
||||
|
||||
// three.js + occt wasm are heavy — keep them out of the page's initial bundle
|
||||
// and out of SSR. The viewer only loads when the user clicks "Show 3D".
|
||||
const StepViewer = dynamic(() => import("@/components/StepViewer"), {
|
||||
ssr: false,
|
||||
loading: () => (
|
||||
<div className="relative w-full h-[480px] rounded-lg border border-slate-200 bg-slate-50 flex items-center justify-center text-sm text-slate-500">
|
||||
Loading viewer…
|
||||
</div>
|
||||
),
|
||||
});
|
||||
|
||||
interface FileView {
|
||||
id: string;
|
||||
originalName: string;
|
||||
@@ -195,6 +207,10 @@ export default function PartDetailClient({
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{part.stepFile ? (
|
||||
<StepViewerSection fileId={part.stepFile.id} fileName={part.stepFile.originalName} />
|
||||
) : null}
|
||||
|
||||
<OperationsSection
|
||||
partId={part.id}
|
||||
operations={operations}
|
||||
@@ -658,6 +674,58 @@ function OperationModal({
|
||||
);
|
||||
}
|
||||
|
||||
// -------- 3D viewer ------------------------------------------------------
|
||||
|
||||
function StepViewerSection({ fileId, fileName }: { fileId: string; fileName: string }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [edges, setEdges] = useState(true);
|
||||
// The viewer fetches via `/api/v1/files/:id/download`, which streams the
|
||||
// stored STEP bytes with correct auth (admin session cookie is already set).
|
||||
const url = `/api/v1/files/${fileId}/download`;
|
||||
|
||||
return (
|
||||
<section className="mb-8">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h2 className="text-lg font-semibold">3D viewer</h2>
|
||||
<div className="flex items-center gap-2">
|
||||
{open ? (
|
||||
<label className="flex items-center gap-1.5 text-xs text-slate-600">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={edges}
|
||||
onChange={(e) => setEdges(e.target.checked)}
|
||||
/>
|
||||
Show edges
|
||||
</label>
|
||||
) : null}
|
||||
<Button variant={open ? "secondary" : "primary"} size="sm" onClick={() => setOpen((v) => !v)}>
|
||||
{open ? "Hide" : "Show 3D"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{open ? (
|
||||
<Card>
|
||||
<div className="p-3">
|
||||
<div className="mb-2 text-xs text-slate-500 truncate" title={fileName}>
|
||||
Viewing: <code className="font-mono text-slate-700">{fileName}</code>
|
||||
</div>
|
||||
<StepViewer key={`${fileId}-${edges ? "e" : "ne"}`} url={url} showEdges={edges} />
|
||||
<p className="mt-2 text-[11px] text-slate-500">
|
||||
Drag to rotate · scroll to zoom · right-drag to pan. Parsing and rendering happens
|
||||
in your browser — nothing is uploaded.
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
) : (
|
||||
<p className="text-sm text-slate-500">
|
||||
Loads the STEP file into an interactive 3D view. Click <span className="font-medium">Show 3D</span>{" "}
|
||||
to start — the viewer downloads ~2 MB of OpenCascade WASM on first use.
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// -------- Files ----------------------------------------------------------
|
||||
|
||||
function FileSlot({
|
||||
|
||||
Reference in New Issue
Block a user