Files
FreeCADWasmDev/web/src/main-chain-shell.tsx
wangdequan 9a7c4f0335 Save FreeCAD Web workspace sources and user validation evidence
IFC native binding remains incomplete; preserve accurate test results.
2026-09-15 22:01:58 +08:00

155 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<FacadeResponse | null>(null);
const [selected, setSelected] = useState<string | null>(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<string, unknown>).action === "import/export" ? "techdraw.nativeImportExport" : `${String((result.result as Record<string, unknown>).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<string, unknown>;
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<string, unknown>;
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 <div><style>{css}</style><header className="top"><span className="brand">FC</span><span>FreeCAD 1.2 Web</span><strong className="title">主链模块工作区</strong></header><div className="layout"><aside><h2>主链动作</h2><p>所有动作共享 main-chain-document,并通过统一 Facade envelope 路由到真实模块 Worker。</p>{ACTIONS.map((action) => <button className="action" key={action.id} disabled={busy} onClick={() => void run(action)}>{action.label}<small>{action.command}</small></button>)}<button className="action" disabled={busy} onClick={() => void runObjectChain()}>执行对象 family 主链</button><button className="action" disabled={busy} onClick={() => void runFormatChain()}>执行格式 native 主链</button><button className="action" disabled={busy} onClick={() => void runFcstdChain()}>执行 FCStd 保存/重开链</button><button className="action" disabled={busy} onClick={() => void runPythonChain()}>执行 Python exposed family</button><button className="action" disabled={busy} onClick={() => void runStatefulFamily()}>执行 Tier-3 stateful family</button><button className="action" disabled={busy} onClick={reset}>重置主链 Worker</button><h2>Document 状态</h2><button className="action" disabled={busy} onClick={() => { stateBatch.apply([{ type: "transaction.begin" }]); dispatchSession({ type: "transaction.begin" }); }}>开始事务</button><button className="action" disabled={busy} onClick={() => { stateBatch.apply([{ type: "transaction.commit" }]); dispatchSession({ type: "transaction.commit" }); }}>提交事务</button><button className="action" disabled={busy} onClick={() => { stateBatch.apply([{ type: "undo" }]); dispatchSession({ type: "undo" }); }}>撤销</button><button className="action" disabled={busy} onClick={() => { stateBatch.apply([{ type: "redo" }]); dispatchSession({ type: "redo" }); }}>重做</button><button className="action" disabled={busy || !latest} onClick={() => updateProperty("Label", `${latest?.module ?? ""} ${latest?.type ?? ""}`)}>设置最近对象 Label</button><button className="action" disabled={busy || !latest} onClick={() => updateProperty("Visibility", !(latest?.properties.Visibility ?? true))}>切换可见性</button><button className="action" disabled={busy || !latest} onClick={() => updateProperty("Color", "#73c6d4")}>设置显示颜色</button><button className="action" disabled={busy || !latest} onClick={() => updateProperty("Placement", [0, 0, 0])}>重置 Placement</button><button className="action" disabled={busy || objectIds.length < 2} onClick={() => { stateBatch.apply([{ type: "link.add", from: objectIds.at(-1)!, to: objectIds.at(-2)!, kind: "dependency" }]); dispatchSession({ type: "link.add", from: objectIds.at(-1)!, to: objectIds.at(-2)!, kind: "dependency" }); }}>链接最近两个对象</button></aside><main className="stage"><section className="panel"><strong>跨模块功能链</strong><p className={`status ${response?.kind === "error" ? "error" : response?.kind === "result" ? "ok" : ""}`}>{status}</p><div className="result">{response ? JSON.stringify(response, null, 2) : "等待 native Worker 结果"}</div></section></main><aside><h2>Document session</h2><div className="route"><strong>{session.id}</strong><small>revision {session.revision} · {session.dirty ? "dirty" : "clean"} · transaction {session.transactionDepth}</small></div><div className="route"><strong>Objects: {objectIds.length}</strong><small>Links: {session.links.length} · selected: {selected ?? "-"}</small></div>{latest ? <div className="route"><strong>{latest.type}</strong><small>{latest.module} / {latest.command}</small><div className="result">{JSON.stringify(latest.properties, null, 2)}</div></div> : null}<h2>模块注册表</h2>{WEB_MODULE_REGISTRY.map((route) => <div className="route" key={route.module}><strong>{route.module}</strong><small>{route.objectTypeCount} objects · {route.pythonApiCount} Python APIs · {route.workers.length} workers</small></div>)}</aside></div></div>;
}
installNavigation("main-chain", readNavigationContext());
installUiAccessibility();
createRoot(document.querySelector<HTMLDivElement>("#app")!).render(<App />);