53 lines
11 KiB
TypeScript
53 lines
11 KiB
TypeScript
import { CircleAlert, FileBox, FileInput, RotateCcw, createIcons } from "lucide";
|
|
import { createImportCreateRecomputeRequest, createImportSaveReopenRequest, createImportStepRequest, IMPORT_CREATE_RECOMPUTE_FIXTURE, IMPORT_FIXTURE, IMPORT_SAVE_REOPEN_FIXTURE, ImportStepResponse } from "./import-facade.js";
|
|
import { createImportInspectRequest, IMPORT_INSPECT_FIXTURE } from "./import-inspect-facade.js";
|
|
import { createImportPythonRequest, IMPORT_PYTHON_FIXTURE } from "./import-python-facade.js";
|
|
import { createCancel } from "./facade-envelope.js";
|
|
|
|
interface ImportBrowserState {
|
|
stage: "starting" | "ready" | "success" | "expected-error" | "error" | "reset";
|
|
requestId: string | null;
|
|
response: ImportStepResponse | null;
|
|
workerMessages: number;
|
|
}
|
|
|
|
declare global { interface Window { __freecadImportState: ImportBrowserState; __freecadImportDispatch?: (requestId: string, invalid?: boolean) => void; __freecadImportCancel?: (requestId: string) => void; } }
|
|
const root = document.querySelector<HTMLDivElement>("#app");
|
|
if (!root) throw new Error("Import root element is missing");
|
|
root.innerHTML = `<main class="shell"><header class="topbar"><div class="brand"><span class="brand-mark">FC</span><span>FreeCAD 1.2 Web</span></div><div class="workbench"><i data-lucide="file-input"></i><span>Import</span></div></header><section class="workspace"><aside class="tree"><div class="section-title">Model</div><div class="tree-row"><i data-lucide="file-box"></i><span>freecad-sample.step</span></div><div class="tree-meta"><span>STEP AP203</span><span>86.8 KB</span></div></aside><section class="canvas"><div class="model"><span class="plate"></span><span class="bracket"></span><span class="bolt"></span></div><div id="status-line" class="status-line"><b id="status-dot"></b><span id="status">Starting Import worker</span></div></section><aside class="properties"><div class="section-title">Import result</div><dl><div><dt>Objects</dt><dd id="objects">-</dd></div><div><dt>Parts</dt><dd id="parts">-</dd></div><div><dt>Solids</dt><dd id="solids">-</dd></div><div><dt>Volume</dt><dd id="volume">-</dd></div><div><dt>Type</dt><dd id="object-type">-</dd></div><div><dt>Request</dt><dd id="request-id">-</dd></div></dl><div class="commands"><button id="run" type="button" disabled><i data-lucide="file-input"></i><span>Import</span></button><button id="error" type="button" disabled><i data-lucide="circle-alert"></i><span>Invalid</span></button><button id="reset" type="button" disabled aria-label="Reset"><i data-lucide="rotate-ccw"></i></button></div></aside></section></main>`;
|
|
const style = document.createElement("style");
|
|
style.textContent = `:root{color-scheme:dark;font-family:Inter,ui-sans-serif,system-ui,sans-serif;background:#10161d;color:#e7edf3}*{box-sizing:border-box;letter-spacing:0}body{margin:0;min-width:320px;min-height:100vh;background:#10161d}button{align-items:center;background:#1d2a35;border:1px solid #435464;border-radius:6px;color:#e7edf3;cursor:pointer;display:inline-flex;font:inherit;gap:7px;justify-content:center;min-height:36px;padding:7px 10px}button:disabled{cursor:default;opacity:.42}button svg{height:16px;width:16px}.shell{display:flex;min-height:100vh;flex-direction:column}.topbar{align-items:center;background:#17212b;border-bottom:1px solid #2c3b48;display:flex;justify-content:space-between;min-height:54px;padding:10px 16px}.brand,.workbench{align-items:center;display:flex;gap:9px;font-size:14px;font-weight:650}.brand-mark{align-items:center;background:#3b9db0;border-radius:5px;color:#071219;display:inline-flex;font-size:12px;font-weight:800;height:28px;justify-content:center;width:28px}.workbench{color:#a9c0ca}.workbench svg{height:17px;width:17px}.workspace{display:grid;flex:1;grid-template-columns:240px minmax(360px,1fr) 300px;min-height:0}.tree,.properties{background:#17212b;padding:16px}.tree{border-right:1px solid #2c3b48}.properties{border-left:1px solid #2c3b48}.section-title{color:#8ea6b2;font-size:11px;font-weight:750;margin-bottom:14px;text-transform:uppercase}.tree-row{align-items:center;background:#21303b;border-left:2px solid #55aebe;display:flex;font-size:13px;gap:8px;min-height:34px;padding:7px 9px}.tree-row svg{height:16px;width:16px}.tree-meta{color:#718793;display:flex;font-size:11px;justify-content:space-between;margin:10px 8px}.canvas{align-items:center;background:#0d1319;display:flex;justify-content:center;min-height:520px;overflow:hidden;position:relative}.canvas:before{background-image:linear-gradient(#1b2730 1px,transparent 1px),linear-gradient(90deg,#1b2730 1px,transparent 1px);background-size:28px 28px;content:"";inset:0;opacity:.58;position:absolute;transform:perspective(620px) rotateX(58deg) scale(1.5);transform-origin:bottom}.model{height:280px;position:relative;transform:rotateX(58deg) rotateZ(-34deg);width:360px;z-index:1}.plate,.bracket,.bolt{background:#355b69;border:1px solid #70b1bd;box-shadow:inset 0 0 0 1px #18333d;display:block;position:absolute}.plate{height:92px;left:36px;top:112px;width:290px}.bracket{height:180px;left:76px;top:28px;width:72px}.bolt{background:#7594a0;border-radius:50%;height:36px;left:225px;top:140px;width:36px}.status-line{align-items:center;background:#17212be8;border:1px solid #31434f;bottom:18px;display:flex;font-size:12px;gap:8px;left:18px;min-height:34px;padding:8px 11px;position:absolute;z-index:2}#status-dot{background:#cfac62;border-radius:50%;height:7px;width:7px}#status-dot[data-level=ok]{background:#66c493}#status-dot[data-level=error]{background:#e47e7e}dl{margin:0}dl div{border-bottom:1px solid #2c3b48;display:flex;gap:10px;justify-content:space-between;padding:10px 0}dt{color:#8ea2ae;font-size:12px}dd{font-size:12px;margin:0;max-width:170px;overflow-wrap:anywhere;text-align:right}.commands{display:grid;gap:8px;grid-template-columns:1fr 1fr 36px;margin-top:18px}.commands button:last-child{padding:7px}@media(max-width:820px){.workspace{grid-template-columns:1fr}.tree{display:none}.canvas{min-height:58vh}.properties{border-left:0;border-top:1px solid #2c3b48}}`;
|
|
document.head.append(style);
|
|
createIcons({ icons: { CircleAlert, FileBox, FileInput, RotateCcw } });
|
|
|
|
const status = document.querySelector<HTMLElement>("#status")!;
|
|
const dot = document.querySelector<HTMLElement>("#status-dot")!;
|
|
const run = document.querySelector<HTMLButtonElement>("#run")!;
|
|
const invalid = document.querySelector<HTMLButtonElement>("#error")!;
|
|
const reset = document.querySelector<HTMLButtonElement>("#reset")!;
|
|
const worker = new Worker(new URL("./import.worker.ts", import.meta.url), { type: "module" });
|
|
let sequence = 0;
|
|
const actionParam = new URLSearchParams(location.search).get("action"); const createRecompute = actionParam === "create-recompute"; const saveReopen = actionParam === "save-reopen"; const inspect = actionParam === "inspect"; const pythonApi = actionParam === "python-api";
|
|
window.__freecadImportState = { stage: "starting", requestId: null, response: null, workerMessages: 0 };
|
|
window.__freecadImportDispatch = (requestId: string, invalidFixture = false) => {
|
|
if (saveReopen) worker.postMessage(createImportSaveReopenRequest(requestId, invalidFixture ? { ...IMPORT_SAVE_REOPEN_FIXTURE, fixtureSha256: "wrong" } : IMPORT_SAVE_REOPEN_FIXTURE));
|
|
else worker.postMessage(createImportStepRequest(requestId, invalidFixture ? { ...IMPORT_FIXTURE, fixturePath: "/fixtures/missing.step" } : IMPORT_FIXTURE));
|
|
};
|
|
window.__freecadImportCancel = (requestId: string) => worker.postMessage(createCancel(`cancel-${requestId}`, requestId));
|
|
function setStatus(message: string, level: "normal" | "ok" | "error" = "normal") { status.textContent = message; dot.dataset.level = level; }
|
|
worker.onmessage = (event: MessageEvent<ImportStepResponse | { kind: "ready"; action: string } | { kind: "reset"; requestId: string }>) => {
|
|
const state = window.__freecadImportState; state.workerMessages += 1; const message = event.data;
|
|
if (message.kind === "ready") { state.stage = "ready"; run.disabled = false; invalid.disabled = false; reset.disabled = false; setStatus("Import worker ready", "ok"); return; }
|
|
if (message.kind === "reset") { state.stage = "reset"; state.response = null; state.requestId = message.requestId; setStatus("Import state reset", "ok"); return; }
|
|
state.response = message; state.requestId = message.requestId; document.querySelector<HTMLElement>("#request-id")!.textContent = message.requestId;
|
|
if (message.kind === "error") { state.stage = ["invalid_fixture", "revision_mismatch"].includes(message.error.code) ? "expected-error" : "error"; setStatus(`${message.error.code}: ${message.error.message}`, state.stage === "expected-error" ? "ok" : "error"); return; }
|
|
state.stage = "success";
|
|
const payload = message.result as unknown as { summary?: { objects: number; parts: number; solids: number; volume: number; objectType: string }; before?: { objects: number; parts: number; solids: number; volume: number }; reopened?: { objects: number; parts: number; solids: number; volume: number }; object?: { count: number; parts: number; solids: number; type: string }; volume?: number; sourceEntry?: string; pythonApiCount?: number; formatExtensions?: string[] };
|
|
if (saveReopen && payload.before && payload.reopened) { payload.summary = { ...payload.reopened, objectType: "Part::Feature" }; }
|
|
const summary = payload.summary ?? (payload.object ? { objects: payload.object.count, parts: payload.object.parts, solids: payload.object.solids, volume: payload.volume!, objectType: payload.object.type } : { objects: 0, parts: 0, solids: 0, volume: 0, objectType: `read-only ${payload.sourceEntry ?? "inspect"} (${payload.pythonApiCount ?? 0} APIs)` });
|
|
document.querySelector<HTMLElement>("#objects")!.textContent = String(summary.objects); document.querySelector<HTMLElement>("#parts")!.textContent = String(summary.parts); document.querySelector<HTMLElement>("#solids")!.textContent = String(summary.solids); document.querySelector<HTMLElement>("#volume")!.textContent = summary.volume.toFixed(6); document.querySelector<HTMLElement>("#object-type")!.textContent = summary.objectType; setStatus("STEP import completed", "ok");
|
|
};
|
|
run.onclick = () => worker.postMessage(pythonApi ? createImportPythonRequest(`import-python-${++sequence}`) : inspect ? createImportInspectRequest(`import-inspect-${++sequence}`) : createRecompute ? createImportCreateRecomputeRequest(`import-create-${++sequence}`) : saveReopen ? createImportSaveReopenRequest(`import-save-reopen-${++sequence}`) : createImportStepRequest(`import-${++sequence}`));
|
|
invalid.onclick = () => worker.postMessage(pythonApi ? createImportPythonRequest(`import-python-error-${++sequence}`, { ...IMPORT_PYTHON_FIXTURE, sourceRevision: "wrong" }) : inspect ? createImportInspectRequest(`import-inspect-error-${++sequence}`, { ...IMPORT_INSPECT_FIXTURE, sourceRevision: "wrong" }) : createRecompute ? createImportCreateRecomputeRequest(`import-create-error-${++sequence}`, { ...IMPORT_CREATE_RECOMPUTE_FIXTURE, fixtureSha256: "wrong" }) : saveReopen ? createImportSaveReopenRequest(`import-save-reopen-error-${++sequence}`, { ...IMPORT_SAVE_REOPEN_FIXTURE, fixtureSha256: "wrong" }) : createImportStepRequest(`import-error-${++sequence}`, { ...IMPORT_FIXTURE, fixturePath: "/fixtures/missing.step" }));
|
|
reset.onclick = () => worker.postMessage({ kind: "reset", requestId: `import-reset-${++sequence}` });
|