155 lines
17 KiB
TypeScript
155 lines
17 KiB
TypeScript
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 />);
|