import React, { useEffect, useMemo, useReducer, useState } from "react"; import { createRoot } from "react-dom/client"; import { createSuccess } from "./facade-envelope.js"; import { PART_FIXTURE } from "./part-facade.js"; import { PARTDESIGN_FIXTURE } from "./partdesign-facade.js"; import { SKETCHER_FIXTURE } from "./sketcher-facade.js"; import { TECHDRAW_NATIVE_FIXTURE } from "./techdraw-native-facade.js"; import { IMPORT_FIXTURE } from "./import-facade.js"; import { WebModuleRouter } from "./module-router.js"; import { ObjectDispatcher } from "./object-dispatcher.js"; import { FormatDispatcher } from "./format-dispatcher.js"; import { PythonFamilyDispatcher } from "./python-family-dispatcher.js"; import { FcstdDispatcher } from "./fcstd-dispatcher.js"; import { StatefulFamilyDispatcher } from "./stateful-family-dispatcher.js"; import { WEB_MODULE_REGISTRY } from "./module-registry.generated.js"; import type { FacadeResponse } from "./facade-envelope.js"; import { createDocumentSession, documentSessionReducer, latestDocumentObject } from "./document-session.js"; import { DocumentStateBatch } from "./document-state-batch.js"; import { ProjectSessionStore } from "./project-session.js"; import { ProjectCheckpointStore } from "./session-persistence.js"; import { installNavigation, readNavigationContext, updateNavigationContext } from "./navigation.js"; import { installUiAccessibility } from "./ui-accessibility.js"; const ACTIONS = [ { id: "part", label: "Part 基础几何", module: "Part", command: "part.createRecompute", payload: PART_FIXTURE }, { id: "partdesign", label: "PartDesign 特征", module: "PartDesign", command: "partdesign.createRecompute", payload: PARTDESIGN_FIXTURE }, { id: "sketcher", label: "Sketcher 约束", module: "Sketcher", command: "sketcher.createRecompute", payload: SKETCHER_FIXTURE }, { id: "techdraw", label: "TechDraw native 投影", module: "TechDraw", command: "techdraw.nativeImportExport", payload: TECHDRAW_NATIVE_FIXTURE }, { id: "import", label: "STEP 导入", module: "Import", command: "import.step", payload: IMPORT_FIXTURE }, ] as const; const css = `:root{color-scheme:dark;font-family:Inter,system-ui,sans-serif;background:#10161d;color:#e7edf3}*{box-sizing:border-box}body{margin:0;min-width:320px}.top{align-items:center;background:#17212b;border-bottom:1px solid #2c3b48;display:flex;gap:14px;padding:14px 20px}.brand{background:#3b9db0;border-radius:5px;color:#071219;font-weight:800;padding:7px}.title{margin-left:auto}.layout{display:grid;grid-template-columns:300px minmax(0,1fr) 320px;min-height:calc(100vh - 56px)}aside{background:#17212b;padding:18px}aside:first-child{border-right:1px solid #2c3b48}aside:last-child{border-left:1px solid #2c3b48}.stage{background:#0d1319;padding:28px}.panel{border:1px solid #334854;max-width:900px;padding:22px}.status{color:#9cb6c2;line-height:1.5}.status.ok{color:#9ce0bd}.status.error{color:#ff9e9e}.action{background:#1d2a35;border:1px solid #435464;border-radius:5px;color:#e7edf3;cursor:pointer;display:block;font:inherit;margin:7px 0;padding:9px 11px;text-align:left;width:100%}.action:hover:not(:disabled){background:#243b47;border-color:#66b7c8}.action:disabled{opacity:.45}.route{border-bottom:1px solid #2c3b48;padding:9px 0}.route strong{display:block;font-size:13px}.route small{color:#8ea2ae;display:block;font-size:11px;margin-top:4px;overflow-wrap:anywhere}.result{font-family:ui-monospace,monospace;font-size:11px;line-height:1.45;max-height:65vh;overflow:auto;white-space:pre-wrap;word-break:break-word}@media(max-width:960px){.layout{grid-template-columns:240px minmax(0,1fr)}aside:last-child{display:none}}@media(max-width:720px){.layout{grid-template-columns:1fr}aside:first-child{border-bottom:1px solid #2c3b48;border-right:0}.stage{padding:16px}}`; function App() { const navigationContext = useMemo(() => readNavigationContext(), []); const router = useMemo(() => new WebModuleRouter(), []); const [busy, setBusy] = useState(false); const [status, setStatus] = useState("主链路由就绪,选择一个真实 native 动作"); const [response, setResponse] = useState(null); const [selected, setSelected] = useState(null); const documentId = navigationContext.documentId ?? "main-chain-document"; const stateBatch = useMemo(() => new DocumentStateBatch(documentId), [documentId]); const projectStore = useMemo(() => { const store = new ProjectSessionStore(); store.open(documentId); new ProjectCheckpointStore().restore(store); return store; }, [documentId]); const checkpointStore = useMemo(() => new ProjectCheckpointStore(), []); const [session, dispatchSession] = useReducer(documentSessionReducer, undefined, () => projectStore.active() ?? createDocumentSession()); useEffect(() => () => router.dispose(), [router]); useEffect(() => { projectStore.set(session); checkpointStore.save(projectStore); }, [checkpointStore, projectStore, session]); const run = async (action: typeof ACTIONS[number]) => { setBusy(true); setSelected(action.id); setStatus(`正在执行 ${action.module} / ${action.command}...`); setResponse(null); updateNavigationContext({ documentId: session.id, module: action.module, command: action.command }); try { const result = await router.request(action.module, action.command, action.payload, "main-chain-document"); setResponse(result); dispatchSession({ type: "native.response", module: action.module, command: action.command, response: result }); setStatus(result.kind === "result" ? `${action.module} native 动作完成` : `${action.module}: ${result.error.code}`); } catch (error) { setStatus(error instanceof Error ? error.message : String(error)); } finally { setBusy(false); } }; const runObjectChain = async () => { setBusy(true); setStatus("正在按对象类型调度 native family..."); setResponse(null); try { const response = await fetch("/api/object-runtime-index.json"); if (!response.ok) throw new Error(`Object runtime index failed: HTTP ${response.status}`); const dispatcher = new ObjectDispatcher(router, await response.json()); const objectIds = ["obj:Part:Part::Box", "obj:PartDesign:PartDesign::AdditiveBox", "obj:Sketcher:Sketcher::SketchObject", "obj:TechDraw:TechDraw::DrawPage", "obj:CAM:Path::Feature", "obj:Inspection:Inspection::Feature", "obj:Points:Points::Feature", "obj:Robot:Robot::Waypoint", "obj:Sandbox:SandboxObject", "obj:Spreadsheet:Spreadsheet::Sheet", "obj:Surface:Surface::Cut", "obj:Mesh:Mesh::Feature"]; const results: FacadeResponse[] = []; for (const objectId of objectIds) { const result = await dispatcher.dispatch(objectId); results.push(result); const action = ACTIONS.find((entry) => entry.command === (result.kind === "result" && typeof result.result === "object" && result.result ? (result.result as Record).action === "import/export" ? "techdraw.nativeImportExport" : `${String((result.result as Record).module).toLowerCase()}.createRecompute` : "")); if (action) dispatchSession({ type: "native.response", module: action.module, command: action.command, response: result }); } const aggregate = createSuccess(`object-chain-${Date.now()}`, { schema: "freecad-web-object-dispatch-result/v1", count: results.length, results, resourcesReleased: results.every((item) => item.kind === "result") }); setResponse(aggregate); setStatus("对象 family 主链完成"); } catch (error) { setStatus(error instanceof Error ? error.message : String(error)); } finally { setBusy(false); } }; const runFormatChain = async () => { setBusy(true); setStatus("正在按扩展名调度 native format..."); setResponse(null); try { const response = await fetch("/api/format-runtime-index.json"); if (!response.ok) throw new Error(`Format runtime index failed: HTTP ${response.status}`); const dispatcher = new FormatDispatcher(router, await response.json()); const formatIds = ["fmt:Import:.step:import", "fmt:Part:.fcstd:unknown", "fmt:Mesh:.stl:unknown", "fmt:TechDraw:.svg:unknown"]; const results: FacadeResponse[] = []; for (const formatId of formatIds) { const item = await dispatcher.dispatch(formatId); results.push(item); if (item.kind === "result" && typeof item.result === "object" && item.result) { const native = item.result as Record; const module = typeof native.module === "string" ? native.module : ""; const action = typeof native.action === "string" ? native.action : ""; dispatchSession({ type: action === "save/reopen" ? "persistence.response" : "native.response", module, command: action, response: item }); } } const aggregate = createSuccess(`format-chain-${Date.now()}`, { schema: "freecad-web-format-dispatch-result/v1", count: results.length, results, resourcesReleased: results.every((item) => item.kind === "result") }); setResponse(aggregate); setStatus("格式 native 主链完成"); } catch (error) { setStatus(error instanceof Error ? error.message : String(error)); } finally { setBusy(false); } }; const runPythonChain = async () => { setBusy(true); setStatus("正在按模块聚合 exposed Python API..."); setResponse(null); try { const response = await fetch("/api/python-api-index.json"); if (!response.ok) throw new Error(`Python API index failed: HTTP ${response.status}`); const index = await response.json(); const apiIds = index.records.filter((row: [string, number, number, string, number, number, number]) => index.fields.dispositions[row[6]] === "exposed").map((row: [string]) => row[0]); const result = await new PythonFamilyDispatcher(router, index).dispatchBatch(apiIds); setResponse(createSuccess(`python-family-${Date.now()}`, result)); setStatus(`Python exposed family 完成:${result.count} API ID`); } catch (error) { setStatus(error instanceof Error ? error.message : String(error)); } finally { setBusy(false); } }; const runFcstdChain = async () => { setBusy(true); setStatus("正在执行 FCStd 保存/重开主链..."); setResponse(null); try { const modules = ["Part", "PartDesign", "Sketcher", "TechDraw", "Import", "Measure", "Mesh", "Spreadsheet", "Surface"] as const; for (const module of modules) projectStore.enqueue({ kind: "save", module, payload: null, documentId: "main-chain-document" }); const queued = await projectStore.drain((job) => new FcstdDispatcher(router).save(job.module, job.documentId)); const result = { schema: "freecad-web-fcstd-dispatch-result/v1", results: queued.map((job) => job.result as FacadeResponse), resourcesReleased: queued.every((job) => (job.result as FacadeResponse).kind === "result") }; for (const item of result.results) { if (item.kind !== "result" || typeof item.result !== "object" || !item.result) continue; const value = item.result as Record; dispatchSession({ type: "persistence.response", module: typeof value.module === "string" ? value.module : "", command: typeof value.action === "string" ? value.action : "save/reopen", response: item }); } setResponse(createSuccess(`fcstd-chain-${Date.now()}`, result)); setStatus(`FCStd 保存/重开链完成:${result.results.length} 模块结果`); } catch (error) { setStatus(error instanceof Error ? error.message : String(error)); } finally { setBusy(false); } }; const runStatefulFamily = async () => { setBusy(true); setStatus("正在按 Tier-3 family 聚合 stateful API..."); setResponse(null); try { const response = await fetch("/api/python-api-index.json"); if (!response.ok) throw new Error(`Python API index failed: HTTP ${response.status}`); const index = await response.json(); const result = await new StatefulFamilyDispatcher(router, index).dispatch(index, "main-chain-document"); setResponse(createSuccess(`tier3-stateful-${Date.now()}`, result)); setStatus(`Tier-3 stateful family 完成:${result.count} API ID`); } catch (error) { setStatus(error instanceof Error ? error.message : String(error)); } finally { setBusy(false); } }; const reset = () => { router.reset(); stateBatch.apply([{ type: "reset" }]); dispatchSession({ type: "reset" }); setResponse(null); setSelected(null); setStatus("主链 Document worker 已请求重置"); }; const latest = latestDocumentObject(session); const objectIds = Object.keys(session.objects); const updateProperty = (name: string, value: unknown) => { if (!latest) return; stateBatch.apply([{ type: "property.set", objectId: latest.id, name, value }]); dispatchSession({ type: "property.set", objectId: latest.id, name, value }); }; return
FCFreeCAD 1.2 Web主链模块工作区
跨模块功能链

{status}

{response ? JSON.stringify(response, null, 2) : "等待 native Worker 结果"}
; } installNavigation("main-chain", readNavigationContext()); installUiAccessibility(); createRoot(document.querySelector("#app")!).render();