stage 8-step viewer
Build and Push Docker Image / build (push) Successful in 1m15s

This commit is contained in:
jason
2026-04-21 13:26:48 -05:00
parent 5847a175af
commit 76308b8aa3
9 changed files with 603 additions and 2 deletions
@@ -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({