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("#app"); if (!root) throw new Error("Import root element is missing"); root.innerHTML = `
FCFreeCAD 1.2 Web
Import
Starting Import worker
`; 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("#status")!; const dot = document.querySelector("#status-dot")!; const run = document.querySelector("#run")!; const invalid = document.querySelector("#error")!; const reset = document.querySelector("#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) => { 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("#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("#objects")!.textContent = String(summary.objects); document.querySelector("#parts")!.textContent = String(summary.parts); document.querySelector("#solids")!.textContent = String(summary.solids); document.querySelector("#volume")!.textContent = summary.volume.toFixed(6); document.querySelector("#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}` });