feat: advance FreeCAD parity and native OCCT history
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -3,6 +3,8 @@ dist/
|
|||||||
.vite/
|
.vite/
|
||||||
.runtime/
|
.runtime/
|
||||||
.cache/
|
.cache/
|
||||||
|
native/occt-history/dist/
|
||||||
|
public/native/occt-history/
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.log
|
*.log
|
||||||
.env
|
.env
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ FreeCAD-aligned web CAD frontend baseline for the BitBybit runtime boundary.
|
|||||||
- FreeCAD-style application menus, workbench navigation, Combo View, Task Dock, viewport and Report view
|
- FreeCAD-style application menus, workbench navigation, Combo View, Task Dock, viewport and Report view
|
||||||
- Machine-readable workbench and command manifest in `src/freecadManifest.ts`
|
- Machine-readable workbench and command manifest in `src/freecadManifest.ts`
|
||||||
- Chinese implementation plan in `docs/web-cad-implementation-plan.zh-CN.md`
|
- Chinese implementation plan in `docs/web-cad-implementation-plan.zh-CN.md`
|
||||||
|
- Full 34-module FreeCAD 1.1.1 scope, Bitbybit/OCCT source-build track and executable task ledger in [docs/freecad-complete-parity-backlog.zh-CN.md](docs/freecad-complete-parity-backlog.zh-CN.md)
|
||||||
|
- Executable reference-oracle and R0-R9 delivery order in [docs/freecad-reference-execution-roadmap.zh-CN.md](docs/freecad-reference-execution-roadmap.zh-CN.md)
|
||||||
|
|
||||||
The current milestone is a facade-first integration slice. Part Design document transactions and history are executable in the mock domain adapter; the internal SQLite WASM + OPFS Persistence Worker, schema and in-memory fallback are bundled behind `facade.project`. FreeCAD/OCCT geometry, solver behavior, FCStd interoperability and full workbench semantics remain staged behind the documented P3-P7 gates.
|
The current milestone is a facade-first integration slice. Part Design document transactions and history are executable in the mock domain adapter; the internal SQLite WASM + OPFS Persistence Worker, schema and in-memory fallback are bundled behind `facade.project`. FreeCAD/OCCT geometry, solver behavior, FCStd interoperability and full workbench semantics remain staged behind the documented P3-P7 gates.
|
||||||
|
|
||||||
@@ -29,4 +31,6 @@ Build verification:
|
|||||||
|
|
||||||
The locked FreeCAD `1.1.1` source build and desktop-oracle replay workflow is documented in [docs/freecad-baseline-and-golden.zh-CN.md](docs/freecad-baseline-and-golden.zh-CN.md). All project Node commands in that workflow use `./npmw`.
|
The locked FreeCAD `1.1.1` source build and desktop-oracle replay workflow is documented in [docs/freecad-baseline-and-golden.zh-CN.md](docs/freecad-baseline-and-golden.zh-CN.md). All project Node commands in that workflow use `./npmw`.
|
||||||
|
|
||||||
|
Geometry work follows a Bitbybit-first policy. Existing Bitbybit OCCT Worker, modeling, IO, triangulation and cache APIs are reused first; missing capabilities may be built from locked Bitbybit/OCCT sources as an Emscripten WASM provider behind the Facade. Source commits, patches, ABI, SBOM, hashes and FreeCAD differential fixtures are required before a capability can be promoted.
|
||||||
|
|
||||||
The SQLite OPFS worker requires cross-origin isolation headers. Vite dev and preview apply `Cross-Origin-Opener-Policy: same-origin` and `Cross-Origin-Embedder-Policy: require-corp`; deployments must preserve them.
|
The SQLite OPFS worker requires cross-origin isolation headers. Vite dev and preview apply `Cross-Origin-Opener-Policy: same-origin` and `Cross-Origin-Embedder-Policy: require-corp`; deployments must preserve them.
|
||||||
|
|||||||
@@ -14,14 +14,102 @@
|
|||||||
"FEM": { "status": "ui-manifest", "level": "unsupported", "commands": ["new-analysis", "material", "mesh", "solve", "results"] },
|
"FEM": { "status": "ui-manifest", "level": "unsupported", "commands": ["new-analysis", "material", "mesh", "solve", "results"] },
|
||||||
"Mesh": { "status": "ui-prototype", "level": "read-only", "commands": ["mesh-import", "analyze-mesh", "repair-mesh", "fill-holes", "mesh-export"] },
|
"Mesh": { "status": "ui-prototype", "level": "read-only", "commands": ["mesh-import", "analyze-mesh", "repair-mesh", "fill-holes", "mesh-export"] },
|
||||||
"Surface": { "status": "ui-manifest", "level": "unsupported", "commands": ["bspline", "bezier", "fill-surface", "loft-surface", "trim-surface"] },
|
"Surface": { "status": "ui-manifest", "level": "unsupported", "commands": ["bspline", "bezier", "fill-surface", "loft-surface", "trim-surface"] },
|
||||||
"Inspection": { "status": "ui-manifest", "level": "unsupported", "commands": ["measure-distance", "measure-angle", "measure-area", "section", "check-dependencies"] }
|
"Inspection": { "status": "ui-manifest", "level": "unsupported", "commands": ["measure-distance", "measure-angle", "measure-area", "section", "check-dependencies"] },
|
||||||
|
"AddonManager": { "status": "scope-only", "level": "unsupported", "commands": ["addon-list", "addon-install", "addon-update", "addon-remove"] },
|
||||||
|
"Cloud": { "status": "scope-only", "level": "unsupported", "commands": ["cloud-login", "cloud-open", "cloud-save", "cloud-share"] },
|
||||||
|
"Fem": { "status": "scope-only", "level": "unsupported", "commands": ["new-analysis", "material", "mesh", "solve", "results"] },
|
||||||
|
"Help": { "status": "scope-only", "level": "unsupported", "commands": ["help-contents", "help-search", "help-about"] },
|
||||||
|
"Idf": { "status": "scope-only", "level": "unsupported", "commands": ["idf-import", "idf-export"] },
|
||||||
|
"Import": { "status": "scope-only", "level": "unsupported", "commands": ["import-model", "import-batch", "import-options"] },
|
||||||
|
"JtReader": { "status": "scope-only", "level": "unsupported", "commands": ["jt-import", "jt-assembly", "jt-export"] },
|
||||||
|
"Material": { "status": "scope-only", "level": "unsupported", "commands": ["material-card", "material-library", "material-assign"] },
|
||||||
|
"Measure": { "status": "scope-only", "level": "unsupported", "commands": ["measure-distance", "measure-angle", "measure-area", "measure-volume"] },
|
||||||
|
"MeshPart": { "status": "scope-only", "level": "unsupported", "commands": ["mesh-to-shape", "shape-to-mesh", "mesh-section"] },
|
||||||
|
"OpenSCAD": { "status": "scope-only", "level": "unsupported", "commands": ["openscad-import", "openscad-export", "openscad-preview"] },
|
||||||
|
"Plot": { "status": "scope-only", "level": "unsupported", "commands": ["plot-create", "plot-style", "plot-export"] },
|
||||||
|
"Points": { "status": "scope-only", "level": "unsupported", "commands": ["points-import", "points-filter", "points-export"] },
|
||||||
|
"ReverseEngineering": { "status": "scope-only", "level": "unsupported", "commands": ["fit-curve", "fit-surface", "reverse-engineer"] },
|
||||||
|
"Robot": { "status": "scope-only", "level": "unsupported", "commands": ["robot-model", "robot-trajectory", "robot-simulate", "robot-export"] },
|
||||||
|
"Show": { "status": "scope-only", "level": "unsupported", "commands": ["show-object", "show-share", "show-presentation"] },
|
||||||
|
"Start": { "status": "scope-only", "level": "unsupported", "commands": ["start-page", "recent-projects", "templates"] },
|
||||||
|
"Web": { "status": "scope-only", "level": "unsupported", "commands": ["web-export", "web-view", "web-share"] },
|
||||||
|
"Sandbox": { "status": "scope-only", "level": "unsupported", "commands": [] },
|
||||||
|
"TemplatePyMod": { "status": "scope-only", "level": "unsupported", "commands": [] },
|
||||||
|
"Test": { "status": "scope-only", "level": "unsupported", "commands": [] },
|
||||||
|
"Tux": { "status": "scope-only", "level": "unsupported", "commands": [] }
|
||||||
|
},
|
||||||
|
"sourceScope": {
|
||||||
|
"baseline": "FreeCAD 1.1.1 src/Mod",
|
||||||
|
"moduleCount": 34,
|
||||||
|
"modules": ["AddonManager", "Assembly", "BIM", "CAM", "Cloud", "Draft", "Fem", "Help", "Idf", "Import", "Inspection", "JtReader", "Material", "Measure", "Mesh", "MeshPart", "OpenSCAD", "Part", "PartDesign", "Plot", "Points", "ReverseEngineering", "Robot", "Sandbox", "Show", "Sketcher", "Spreadsheet", "Start", "Surface", "TechDraw", "TemplatePyMod", "Test", "Tux", "Web"],
|
||||||
|
"coreDirectories": ["App", "Base", "Gui", "Main"],
|
||||||
|
"coverageRule": "Every module, command, TypeId, Property, file format, script/addon entry and browser limitation must have a task ID and a compatibility level before G9. Scope-only means registered but not implemented."
|
||||||
|
},
|
||||||
|
"sourceModuleAliases": {
|
||||||
|
"Fem": "FEM",
|
||||||
|
"PartDesign": "Part Design",
|
||||||
|
"TechDraw": "TechDraw",
|
||||||
|
"MeshPart": "Mesh"
|
||||||
|
},
|
||||||
|
"geometryBuildStrategy": {
|
||||||
|
"primary": "reuse-bitbybit",
|
||||||
|
"fallback": ["extend-bitbybit-binding", "custom-occt-binding"],
|
||||||
|
"sourceTasks": ["BYB-01", "BYB-02", "BYB-03", "BYB-04", "BYB-05", "BYB-06", "BYB-07", "BYB-08"],
|
||||||
|
"constraints": ["Facade-only React boundary", "Emscripten reproducible build", "ABI and protocol versioning", "SBOM/license/hash evidence", "Bitbybit-FreeCAD golden differential"]
|
||||||
|
},
|
||||||
|
"bitbybitSource": {
|
||||||
|
"repository": "https://github.com/bitbybit-dev/bitbybit",
|
||||||
|
"localSnapshot": ".cache/bitbybit/bitbybit",
|
||||||
|
"commit": "d3f52bd1fcf78148b60800d69ed6b42e53f4ad06",
|
||||||
|
"version": "1.1.1",
|
||||||
|
"occtPackage": "packages/dev/occt",
|
||||||
|
"workerPackage": "packages/dev/occt-worker",
|
||||||
|
"sourceForm": "generated-wasm-and-typescript-only",
|
||||||
|
"missingBuildInputs": ["OCCT C++ source checkout", "Embind generation rules", "Emscripten link/export manifest"],
|
||||||
|
"evidence": "./npmw run check:bitbybit-history"
|
||||||
|
},
|
||||||
|
"nativeOcctHistory": {
|
||||||
|
"status": "built-provider-not-default-runtime",
|
||||||
|
"sourceCheckout": "OCCT_SOURCE_DIR",
|
||||||
|
"sourceCommit": "bb75017e915f6509a4bfa8b551f853352754c2da",
|
||||||
|
"occtVersion": "8.0.0",
|
||||||
|
"toolchain": "Emscripten 3.1.69 + CMake 3.31.6 + Ninja 1.12.1",
|
||||||
|
"library": ["TKBO", "TKDESTEP", "TKXSBase", "TKDE"],
|
||||||
|
"bindings": ["BRepAlgoAPI_Fuse", "BRepAlgoAPI_Cut", "BRepAlgoAPI_Common", "Generated", "Modified", "IsDeleted", "TopExp_Explorer", "STEPControl_Reader", "STEPControl_Writer", "shapeToStep", "booleanHistoryFromStep"],
|
||||||
|
"buildOptimization": "OCCT dependencies -O0 with Emscripten -fexceptions; Embind wrapper -O2 with -fexceptions",
|
||||||
|
"artifact": {
|
||||||
|
"javascript": "native/occt-history/dist/bitbybit-occt-history.js",
|
||||||
|
"wasm": "native/occt-history/dist/bitbybit-occt-history.wasm",
|
||||||
|
"javascriptSha256": "e2feca5a8bab431e3351e6af3972beef4f9ce47fe02de2d0ac7ba9de91882311",
|
||||||
|
"wasmSha256": "6fac28102132ddeb8e7f1f254c76643d595cc60a26b2adf80fe3d6407b18b675"
|
||||||
|
},
|
||||||
|
"protocol": "native result and history records stay inside the provider; cross-WASM handles are forbidden; STEP text is the explicit transport boundary",
|
||||||
|
"historyProtocol": {
|
||||||
|
"version": 1,
|
||||||
|
"request": "NativeOcctHistoryRequest",
|
||||||
|
"response": "NativeOcctHistoryProtocolResponse",
|
||||||
|
"transport": "step-text",
|
||||||
|
"cancellation": "AbortSignal",
|
||||||
|
"timeout": "coordinator-configured",
|
||||||
|
"staleGuard": "documentId + documentVersion + operationId"
|
||||||
|
},
|
||||||
|
"browserWorker": {
|
||||||
|
"entry": "src/facade/nativeHistoryWorkerEntry.ts",
|
||||||
|
"client": "src/facade/nativeHistoryWorkerClient.ts",
|
||||||
|
"moduleUrl": "/native/occt-history/bitbybit-occt-history.js",
|
||||||
|
"runtimeConfiguration": "BitbybitGeometryRuntime.configureNativeHistory(provider, timeoutMs)",
|
||||||
|
"artifactPublishCommand": "./npmw run build:occt-history",
|
||||||
|
"status": "worker-provider-configurable-not-default-runtime"
|
||||||
|
},
|
||||||
|
"buildCommand": "./npmw run build:occt-history",
|
||||||
|
"testCommand": "./npmw run test:occt-history"
|
||||||
},
|
},
|
||||||
"facadeCapabilities": {
|
"facadeCapabilities": {
|
||||||
"geometry": { "level": "experimental", "provider": "BitBybit OCCT 1.1.1", "operations": ["box", "cylinder", "sphere", "cone", "placement", "object-placement-recompute", "placement-validation", "mirror-plane", "mirrored-whole-shape", "multi-transform-whole-shape", "union", "cut", "intersection", "part-primitive-recompute", "part-boolean-recompute", "shape-check", "pad", "pocket", "pocket-through-all", "revolution", "fillet", "chamfer", "linear-pattern-whole-shape", "polar-pattern-whole-shape", "hole-basic-origin-normal", "feature-shape-cache", "face-edge-vertex-topology", "step-export", "stl-export"] },
|
"geometry": { "level": "experimental", "provider": "BitBybit OCCT 1.1.1", "operations": ["box", "cylinder", "sphere", "cone", "placement", "object-placement-recompute", "placement-validation", "mirror-plane", "mirrored-whole-shape", "multi-transform-whole-shape", "union", "cut", "intersection", "part-primitive-recompute", "part-boolean-recompute", "shape-check", "pad", "pocket", "pocket-through-all", "revolution", "fillet", "chamfer", "linear-pattern-whole-shape", "polar-pattern-whole-shape", "hole-basic-origin-normal", "feature-shape-cache", "face-edge-vertex-topology", "step-export", "stl-export"] },
|
||||||
"document": { "level": "experimental", "operations": ["typed-properties", "property-vector", "property-placement", "property-multi-transform", "property-link-list", "property-string-list", "property-link-sub", "expressions", "units", "dependency-dag", "parallel-level-recompute", "sync-recompute", "async-generation-recompute", "root-cause-diagnostics", "repair-branch-recompute", "versioned-toporef", "toporef-migration", "generation-topology-snapshot", "linksub-external-geometry-migration", "ambiguous-deleted-topology-diagnostics", "toporef-candidate-replacement-transaction", "signature-topology-history-fallback", "undo-redo"] },
|
"document": { "level": "experimental", "operations": ["typed-properties", "property-vector", "property-placement", "property-multi-transform", "property-link-list", "property-string-list", "property-link-sub", "expressions", "units", "dependency-dag", "parallel-level-recompute", "sync-recompute", "async-generation-recompute", "root-cause-diagnostics", "repair-branch-recompute", "versioned-toporef", "toporef-migration", "generation-topology-snapshot", "linksub-external-geometry-migration", "ambiguous-deleted-topology-diagnostics", "toporef-candidate-replacement-transaction", "signature-topology-history-fallback", "native-topology-history-adapter", "undo-redo"] },
|
||||||
"persistence": { "level": "experimental", "provider": "SQLite WASM + OPFS with transient fallback", "operations": ["schema-v6", "object-topology-snapshots", "transactional-migrations", "migration-rollback", "serialized-write-queue", "five-version-checkpoints", "checkpoint-load", "recovery-report", "content-addressed-resources", "quota-preflight", "resource-sweep-plan", "opfs-orphan-sweep", "cross-tab-lock-notice"] },
|
"persistence": { "level": "experimental", "provider": "SQLite WASM + OPFS with transient fallback", "operations": ["schema-v6", "object-topology-snapshots", "transactional-migrations", "migration-rollback", "serialized-write-queue", "five-version-checkpoints", "checkpoint-load", "recovery-report", "content-addressed-resources", "quota-preflight", "resource-sweep-plan", "opfs-orphan-sweep", "cross-tab-lock-notice"] },
|
||||||
"sketcher": { "level": "experimental", "provider": "typescript-basic; planegcs-wasm unavailable", "operations": ["point-line-circle-arc-model", "ellipse-bspline-model-only", "construction-geometry", "stable-external-geometry-model", "basic-constraints", "diameter-symmetric-tangent", "basic-solver", "versioned-solver-protocol", "solver-capability-probe", "cancel-stale-isolation", "replay-contract", "persistence"] },
|
"sketcher": { "level": "experimental", "provider": "typescript-basic; planegcs-wasm unavailable", "operations": ["point-line-circle-arc-model", "ellipse-bspline-model-only", "construction-geometry", "stable-external-geometry-model", "basic-constraints", "diameter-symmetric-tangent", "basic-solver", "versioned-solver-protocol", "solver-capability-probe", "cancel-stale-isolation", "replay-contract", "persistence"] },
|
||||||
"fcstd": { "level": "read-only", "operations": ["zip-preflight", "document-xml-metadata", "proxy-report", "script-isolation"] },
|
"fcstd": { "level": "read-only", "operations": ["zip-preflight", "document-xml-metadata", "document-property-projection", "proxy-report", "script-isolation"] },
|
||||||
"verification": { "level": "experimental", "provider": "FreeCADCmd 1.1.1 native headless Part oracle", "operations": ["locked-source-commit", "reproducible-native-build", "declarative-golden-contract", "part-geometry-replay", "numeric-tolerance-comparison"] }
|
"verification": { "level": "experimental", "provider": "FreeCADCmd 1.1.1 native headless Part oracle", "operations": ["locked-source-commit", "reproducible-native-build", "declarative-golden-contract", "part-geometry-replay", "numeric-tolerance-comparison"] }
|
||||||
},
|
},
|
||||||
"rules": [
|
"rules": [
|
||||||
|
|||||||
@@ -54,7 +54,7 @@
|
|||||||
- Hole 固定原点/法向,尚无面定位、沉孔、锪孔、螺纹和完整孔标准。
|
- Hole 固定原点/法向,尚无面定位、沉孔、锪孔、螺纹和完整孔标准。
|
||||||
- 拓扑历史来自签名保守回退,不是 OCCT/FreeCAD 原生 Generated/Modified/Deleted 历史。
|
- 拓扑历史来自签名保守回退,不是 OCCT/FreeCAD 原生 Generated/Modified/Deleted 历史。
|
||||||
- Placement 已进入文档属性,并接入 Facade 的对象级 Shape 重算;尚未完整映射到 Body/Tip、Support、AttachmentOffset 或所有特征执行器的 FreeCAD 局部坐标链语义。
|
- Placement 已进入文档属性,并接入 Facade 的对象级 Shape 重算;尚未完整映射到 Body/Tip、Support、AttachmentOffset 或所有特征执行器的 FreeCAD 局部坐标链语义。
|
||||||
- FCStd 仍以安全预检、metadata/proxy 和只读报告为主,尚未双向原生往返。
|
- FCStd 仍以安全预检、metadata/proxy 和只读报告为主;本轮新增对象属性名、FreeCAD 类型、XML 元素和值的字段级 `Document.xml` 投影,但尚未双向原生往返。
|
||||||
|
|
||||||
## 5. 下一步执行顺序
|
## 5. 下一步执行顺序
|
||||||
|
|
||||||
@@ -154,3 +154,52 @@ Placement 里程碑涉及实现、测试、兼容矩阵和三份状态/方案文
|
|||||||
- 全部 Node/npm 操作均使用 `./npmw`;依赖、构建、回放和 CI 边界已写入 `docs/freecad-baseline-and-golden.zh-CN.md`。
|
- 全部 Node/npm 操作均使用 `./npmw`;依赖、构建、回放和 CI 边界已写入 `docs/freecad-baseline-and-golden.zh-CN.md`。
|
||||||
|
|
||||||
本轮只完成 P0-01/P0-03 的首批可复现基线。依赖容器 digest、完整 FreeCAD 工作台构建、100 个黄金文件、50 个错误文件、GUI 行为和 Web OCCT 双端差分尚未完成,F0/G0 保持进行中。下一执行点是读取 FreeCAD/OCCT 原生 Generated/Modified/Deleted 历史接口并形成可供 Web `TopoRef` 消费的类型化历史合同。
|
本轮只完成 P0-01/P0-03 的首批可复现基线。依赖容器 digest、完整 FreeCAD 工作台构建、100 个黄金文件、50 个错误文件、GUI 行为和 Web OCCT 双端差分尚未完成,F0/G0 保持进行中。下一执行点是读取 FreeCAD/OCCT 原生 Generated/Modified/Deleted 历史接口并形成可供 Web `TopoRef` 消费的类型化历史合同。
|
||||||
|
|
||||||
|
## 14. 2026-08-03 全功能范围账本与 Bitbybit 构建轨道
|
||||||
|
|
||||||
|
新增 [FreeCAD 1.1.1 全功能对标主任务账本](freecad-complete-parity-backlog.zh-CN.md),以锁定源码 `src/Mod` 的 34 个模块和 `App/Base/Gui/Main` 通用能力为完整范围。账本把任务拆成 `INV`、`BASE`、`BYB`、`CORE`、`TSN`、`SK`、`PART`、`PD`、`FC`、各工作台任务和 `QA/REL` 门禁;源码中的 Sandbox/模板/测试设施以及 Python、宏、插件、偏好、打印和剪贴板均必须有明确能力等级,不得隐藏在“其他”中。
|
||||||
|
|
||||||
|
几何实现改为 Bitbybit 优先:先复用 Bitbybit 的 OCCT Worker、建模、IO、三角化、缓存和资源生命周期;公开 API 不足时,从锁定的 Bitbybit/OCCT 源码扩展绑定并用 Emscripten 构建自有 WASM provider。对应 `BYB-01..08` 要求来源提交、补丁、ABI/消息协议、SBOM/许可证、产物 hash、句柄回收和 Bitbybit↔自构建 provider↔FreeCAD 三方黄金差分。React 仍只能通过 `BitBybitWebCadFacade` 访问几何。
|
||||||
|
|
||||||
|
兼容矩阵新增 `sourceScope`(34 个模块)和 `geometryBuildStrategy`,并把 AddonManager、Cloud、Fem、Help、Idf、Import、JtReader、Material、Measure、MeshPart、OpenSCAD、Plot、Points、ReverseEngineering、Robot、Show、Start、Web 及开发/测试目录登记为 `scope-only/unsupported`,直到各自任务与证据完成。现有 Part/PartDesign/Sketcher/持久化/FCStd 等状态不提升,仍按实验或只读边界执行。
|
||||||
|
|
||||||
|
本机 `FreeCADCmd` reference probe 已接入 `./npmw run probe:freecad-reference`:实测报告为 FreeCAD 1.1.1、commit `0108fd4b4850cc46e625b60e53cea7a7bbe69f8d`、`guiUp=false`;当前构建可加载 `Part`、`Material`、`Measure`,Sketcher/PartDesign/TechDraw/Spreadsheet/Draft/Mesh/Fem/CAM/Assembly 明确 unavailable。探针会采集核心 TypeId 和 Property 元数据,作为 `INV-02` 和后续黄金夹具的输入;完整桌面工作台行为仍需启用对应 FreeCAD 原生模块后再回放。
|
||||||
|
|
||||||
|
本轮已把 `geometry.topologyHistory()` provider 接入 `createFacadeGeometryRecomputeExecutor`:对有 Base/Tool/Profile 拓扑输入的特征,若 Worker 提供原生记录,则使用 `captureNativeTopologyHistory()` 按 kind 和 source/result index 生成 `occt-native` 历史;provider 缺失、无输入或不支持时继续使用 `signature-fallback`。新增原生关系映射、删除、重复记录和索引越界测试,完整门禁 68/68 通过。该接线不等于 OCCT 原生历史已可用,真实 Bitbybit/OCCT 自构建绑定仍由 `BYB-04..07` 完成。
|
||||||
|
|
||||||
|
新增 `check:bitbybit-history` 绑定面审计并纳入 `./npmw run verify`。审计结果明确记录当前 npm 绑定的原生历史缺口,防止 `occt-native` 合同在没有 Generated/Modified/IsDeleted 实现时被误报为完成。
|
||||||
|
|
||||||
|
BYB-01/02 证据已锁定:本地 Bitbybit 源码快照为 `d3f52bd1fcf78148b60800d69ed6b42e53f4ad06`(版本 `1.1.1`),记录于 `config/compatibility-matrix.json`;门禁会同时校验源码提交与安装包版本。
|
||||||
|
|
||||||
|
本轮已完成 BYB-04 的第一版实际编译:`native/occt-history` 从缓存 OCCT `bb75017e915f6509a4bfa8b551f853352754c2da`(8.0.0)构建 `TKBO`、`TKDESTEP`、`TKXSBase`、`TKDE` 和 Embind WASM,`./npmw run test:occt-history` 通过,`shapeToStep -> booleanHistoryFromStep(cut)` smoke case 返回 42 条原生关系记录。`mapNativeOcctHistoryRecords` 与 `createNativeOcctStepHistoryBridge` 已把 provider 协议接入 Facade 类型边界。该 provider 当前仍是独立 WASM 实例;跨 Bitbybit 主 WASM 只能通过 STEP 文本序列化交换,不能直接传递指针。
|
||||||
|
|
||||||
|
## 15. 2026-08-03 原生 OCCT 历史 + STEP 跨 WASM 桥接
|
||||||
|
|
||||||
|
- `native/occt-history` 现从 OCCT `bb75017e...` 编译 `TKBO`、`TKDESTEP`、`TKXSBase` 和 `TKDE`,Embind 暴露 `Generated`、`Modified`、`IsDeleted`、`STEPControl_Reader/Writer`、`shapeToStep` 与 `booleanHistoryFromStep`。
|
||||||
|
- 原生 smoke 已验证 `shapeToStep(makeBox()) -> STEP text -> booleanHistoryFromStep()`,`cut` 返回 42 条历史记录;最终 wasm 在 Node 22.23.2 可实例化。
|
||||||
|
- 由于当前 Emscripten/OCCT 大型静态链接的 wasm 异常控制流限制,依赖库固定 `-O0 -fexceptions`,Embind wrapper 固定 `-O2 -fexceptions`;产物 hash 记录在兼容矩阵并由 `check:bitbybit-history` 校验。
|
||||||
|
- `createNativeOcctStepHistoryBridge()` 已加入 Facade:它只把 Bitbybit `exportStep()` 的文本交给原生 provider,禁止传递跨 WASM ShapeHandle;重算器仅对 `Part::Fuse/Cut/Common` 注入明确 operation,其余特征继续签名回退。
|
||||||
|
|
||||||
|
该里程碑仍不是默认 GeometryRuntime,也没有把所有 PartDesign 特征切换到原生 provider;本轮已补齐 `BitbybitGeometryRuntime.configureNativeHistory()`,显式配置后 Boolean 重算会通过 STEP 和协调器消费原生记录。浏览器真实 WASM 实例化、STEP 精度/单位策略和 Bitbybit↔原生↔FreeCAD 三方黄金差分仍是 `BYB-05..08`、`TSN-03..10` 后续门禁。
|
||||||
|
|
||||||
|
## 16. 2026-08-03 原生历史协议与故障隔离
|
||||||
|
|
||||||
|
- 新增 `NATIVE_OCCT_HISTORY_PROTOCOL_VERSION = 1`,请求携带 `documentId/documentVersion/operationId` 和两份 ISO-10303-21 STEP 文本,响应携带 provider 能力、原生历史和上下文回显。
|
||||||
|
- `DirectNativeOcctHistoryProvider` 绑定已编译 Embind 模块;`NativeOcctHistoryCoordinator` 统一处理 AbortSignal、超时、取消、stale 文档版本和并发 generation,未把任何 ShapeHandle 放进消息协议。
|
||||||
|
- Facade 测试新增协议上下文、STEP 格式拒绝、timeout/cancel/stale 覆盖;完整测试数更新为 72/72。
|
||||||
|
|
||||||
|
该协议已完成 BYB-05 的类型/合同层,并新增 `NativeOcctHistoryWorkerProvider`/Worker 入口;构建脚本会把产物发布到 `/native/occt-history/`,`BitbybitGeometryRuntime` 可显式配置该 provider 并把 STEP 历史接入 Facade 重算。跨浏览器真实 WASM 实例化、Worker 崩溃恢复和三方黄金差分仍需单独验证,不能把 Node 原生 smoke 当作浏览器证据。
|
||||||
|
|
||||||
|
本轮文档变更验证必须使用项目包装器:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./npmw run verify
|
||||||
|
git diff --check
|
||||||
|
jq empty config/compatibility-matrix.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## 17. 2026-08-03 本机 FreeCAD 参考与后续安排
|
||||||
|
|
||||||
|
再次执行 `./npmw run probe:freecad-reference` 已确认锁定 oracle 为 FreeCAD `1.1.1`、提交 `0108fd4b4850cc46e625b60e53cea7a7bbe69f8d`。当前缓存构建 `guiUp=false`,可加载 `Part/Material/Measure`;其余工作台是构建缺失证据,不是对应 Web 功能的行为 oracle。
|
||||||
|
|
||||||
|
新增 [FreeCAD 1.1.1 全功能对标执行路线图](freecad-reference-execution-roadmap.zh-CN.md),按 R0 参考环境、R1 几何/拓扑、R2 Sketcher、R3 参数化核心、R4 FCStd、R5-R8 工作台与平台长尾、R9 发布门禁连续执行。近期先完成完整桌面 O2 oracle、机器清册差分、真实浏览器原生历史三方黄金,再进入 planegcs 与 PartDesign 完整闭环。
|
||||||
|
|||||||
@@ -12,6 +12,8 @@
|
|||||||
|
|
||||||
源码、构建和安装目录均在 `.cache/` 下,不进入 Git。仓库保存精确提交、构建参数、工具版本、产物哈希和回放契约;当前产物是用于几何对照的无界面 Part oracle,不是完整 FreeCAD 桌面发行版。
|
源码、构建和安装目录均在 `.cache/` 下,不进入 Git。仓库保存精确提交、构建参数、工具版本、产物哈希和回放契约;当前产物是用于几何对照的无界面 Part oracle,不是完整 FreeCAD 桌面发行版。
|
||||||
|
|
||||||
|
本机 oracle 可作为锁定版本的行为参考。`./npmw run probe:freecad-reference` 会通过 `FreeCADCmd` 采集版本、Git 提交、GUI 能力、可导入 Python 模块和核心 TypeId/Property 元数据,输出机器可读报告;当前无界面构建只启用 `Part`、`Material`、`Measure`,未加载的 Sketcher、PartDesign、TechDraw 等模块会明确标记为 unavailable。完整桌面/工作台对照仍需单独构建 GUI oracle,不能从该报告推断已支持。
|
||||||
|
|
||||||
## 2. Debian trixie 依赖
|
## 2. Debian trixie 依赖
|
||||||
|
|
||||||
本次实测使用 GCC `14.2.0`、CMake `3.31.6`、Ninja `1.12.1`、Python `3.13.5`、Qt `6.8.2`、OCCT `7.8.1` 和 SWIG `4.3.0`。安装命令:
|
本次实测使用 GCC `14.2.0`、CMake `3.31.6`、Ninja `1.12.1`、Python `3.13.5`、Qt `6.8.2`、OCCT `7.8.1` 和 SWIG `4.3.0`。安装命令:
|
||||||
|
|||||||
267
docs/freecad-complete-parity-backlog.zh-CN.md
Normal file
267
docs/freecad-complete-parity-backlog.zh-CN.md
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
# FreeCAD 1.1.1 全功能对标主任务账本
|
||||||
|
|
||||||
|
> 本账本是全功能范围、依赖、任务 ID 和验收证据的唯一总索引。实现状态仍以 `config/compatibility-matrix.json` 为机器可读真值;详细技术设计见 `freecad-full-parity-plan.zh-CN.md`。任何 UI、类型或 Mock 行为都不能单独计为兼容。
|
||||||
|
|
||||||
|
## 1. 对标合同
|
||||||
|
|
||||||
|
- 基线固定为 FreeCAD `1.1.1`,提交 `0108fd4b4850cc46e625b60e53cea7a7bbe69f8d`。
|
||||||
|
- “全部功能”指基线源码中的 App/Base/Gui 公共行为、用户可见模块、命令、对象、属性、任务面板、文件格式、脚本/扩展入口和错误行为全部进入清册。
|
||||||
|
- 每项能力只能处于 `exact`、`compatible`、`read-only`、`proxy` 或 `unsupported`。`unsupported` 必须是经过评审的浏览器边界,有明确诊断和数据保留策略,不能代表遗漏。
|
||||||
|
- `exact`/`compatible` 必须有锁定 FreeCAD 的黄金输入、状态、输出、错误和 round-trip 报告;没有证据时最高只能是 `experimental`。
|
||||||
|
- Bitbybit 是 Web 几何、Worker、序列化、IO 和三角化能力的首选实现层。先审计和复用 Bitbybit 已有 API;若接口不足,允许从锁定的 Bitbybit 与 OCCT 源码扩展绑定并编译 WASM,不能因 npm 包暂未暴露接口而降低 FreeCAD 功能目标。
|
||||||
|
- 自构建 OCCT/Bitbybit 代码必须经过版本锁定、补丁清单、Emscripten 构建、ABI/消息 schema、许可证/SBOM、产物哈希和与 FreeCAD/现有 Bitbybit 的双端黄金测试。React 仍只能调用 Facade,不能直接导入 OCCT 或 Worker。
|
||||||
|
- 所有 Node/npm 命令必须通过项目包装器 `./npmw` 执行,禁止以系统 `npm` 产生验收证据。
|
||||||
|
|
||||||
|
## 2. 基线模块范围
|
||||||
|
|
||||||
|
本地锁定源码 `src/Mod` 共 34 个目录,按交付责任分组如下。P0 清册必须进一步枚举各模块的命令、对象、属性、偏好、文件格式和可选依赖,目录存在不等于用户功能存在。
|
||||||
|
|
||||||
|
| 分组 | FreeCAD 1.1.1 源码模块 | 对标处理 |
|
||||||
|
|---|---|---|
|
||||||
|
| 核心建模 | `Part`、`PartDesign`、`Sketcher`、`Surface` | 浏览器内参数化编辑与重算,目标至少 `compatible` |
|
||||||
|
| 文档与二维 | `Draft`、`TechDraw`、`Spreadsheet`、`Plot` | 创建、编辑、表达式、投影与输出闭环 |
|
||||||
|
| 工程 | `Assembly`、`BIM`、`CAM`、`Fem`、`Robot`、`Inspection`、`Measure` | 求解器/后处理器可端口则兼容,否则逐能力显式分级 |
|
||||||
|
| 数据与几何 | `Import`、`Mesh`、`MeshPart`、`Points`、`ReverseEngineering`、`OpenSCAD`、`Idf`、`JtReader`、`Material` | 格式、单位、颜色、拓扑、代理和往返差异可审计 |
|
||||||
|
| 应用体验 | `Start`、`Help`、`AddonManager`、`Cloud`、`Show`、`Web` | Web 等价工作流、离线帮助、扩展治理和安全边界 |
|
||||||
|
| 工具/开发 | `Sandbox`、`TemplatePyMod`、`Test`、`Tux` | 不伪装成最终用户工作台;保留开发/回归等价能力或明确 `unsupported` |
|
||||||
|
|
||||||
|
此外必须覆盖源码目录 `App`、`Base`、`Gui`、`Main` 的文档模型、Property、事务、选择、视图、参数、命令、Python 控制台、宏、偏好、快捷键、多文档、剪贴板、打印和帮助行为。
|
||||||
|
|
||||||
|
## 3. 单项完成定义
|
||||||
|
|
||||||
|
每个命令或对象的任务卡必须同时具备以下证据,缺任一项不得关闭:
|
||||||
|
|
||||||
|
1. 基线来源:FreeCAD 命令 ID、对象 TypeId、Property/枚举/默认值、选择前置、可选依赖和平台差异。
|
||||||
|
2. 合同:版本化 Facade 类型、JSON Schema、能力探测、结构化诊断、取消和进度。
|
||||||
|
3. 领域行为:创建、编辑、重算、删除、复制、可见性、事务、Undo/Redo、失败恢复和依赖传播。
|
||||||
|
4. 几何/求解:公差、单位、拓扑历史、确定性、资源释放和过期 generation 隔离。
|
||||||
|
5. 持久化:SQLite/OPFS 保存加载、schema migration、FCStd 或原始代理数据保留。
|
||||||
|
6. 交互:任务面板、Property editor、选择/预选、键盘、只读、窄屏和无障碍。
|
||||||
|
7. 互操作:相关导入/导出、FreeCAD→Web→FreeCAD round-trip 和差异报告。
|
||||||
|
8. 测试:最小成功、边界、错误、取消、黄金几何/状态、浏览器 E2E、性能和内存。
|
||||||
|
|
||||||
|
工作台只有在其范围内所有任务完成上述八项,并通过“创建→编辑→重算→撤销→保存→加载→导出”七步回放后,才能通过阶段门。
|
||||||
|
|
||||||
|
## 4. 执行波次
|
||||||
|
|
||||||
|
| 波次 | 结果 | 阻断关系 |
|
||||||
|
|---|---|---|
|
||||||
|
| W0 范围冻结 | 全模块清册、依赖、许可证、黄金 oracle、能力矩阵 | 阻断所有兼容声明 |
|
||||||
|
| W1 平台内核 | Facade、App/Gui 模型、表达式/单位、DAG、持久化、Viewport | 阻断所有工作台闭环 |
|
||||||
|
| W2 拓扑与 Sketcher | OCCT 原生历史、稳定 TopoRef、planegcs WASM、草图编辑器 | 阻断 PartDesign/TechDraw/附着 |
|
||||||
|
| W3 核心建模 | Part、PartDesign、Sketcher 参数化闭环 | 阻断生产和工程工作台 |
|
||||||
|
| W4 原生文件 | FCStd 双向往返、核心交换格式 | 阻断 G5 及后续工作台保存门禁 |
|
||||||
|
| W5 生产文档 | TechDraw、Spreadsheet、Draft、Plot | 阻断 G6 |
|
||||||
|
| W6 高级建模 | Assembly、BIM、Mesh/MeshPart、Surface | 阻断 G7 |
|
||||||
|
| W7 工程与长尾 | FEM、CAM、Robot、Inspection、数据/应用模块 | 阻断 G8 |
|
||||||
|
| W8 发布 | 全量回放、跨浏览器、性能、安全、离线、升级与回滚 | 阻断 G9 |
|
||||||
|
|
||||||
|
## 5. W0:范围、基线和黄金 oracle
|
||||||
|
|
||||||
|
| ID | 任务 | 退出条件 |
|
||||||
|
|---|---|---|
|
||||||
|
| INV-01 | 从 `src/Mod`、Workbench 初始化脚本和命令注册表生成模块/工作台/命令清册 | 34 个源码模块均有分类,不存在未登记目录 |
|
||||||
|
| INV-02 | 提取 App/Gui TypeId、Property、枚举、默认值、只读/隐藏/表达式能力 | 每个核心对象能追溯到源码与最小 FCStd fixture |
|
||||||
|
| INV-03 | 提取菜单、工具栏、上下文菜单、快捷键、任务面板和选择前置 | GUI 命令均有状态与前置条件记录 |
|
||||||
|
| INV-04 | 提取偏好、参数树、导航样式、颜色/显示和平台差异 | 每项标记 document/user/session 作用域 |
|
||||||
|
| INV-05 | 提取导入/导出格式、扩展名、MIME、单位、颜色和可选库 | 格式矩阵无未解释入口 |
|
||||||
|
| INV-06 | 提取 Python API、宏、控制台、扩展和 Addon 入口 | 每项有 sandbox/兼容/不支持决策 |
|
||||||
|
| BASE-01 | 固定构建容器 digest、编译器、依赖、补丁和产物哈希 | 干净环境重建结果可追溯 |
|
||||||
|
| BASE-02 | 构建完整无界面 oracle 与可自动化 GUI oracle | Part 之外的目标模块可被回放 |
|
||||||
|
| BASE-03 | 扩充到不少于 100 个成功、50 个错误黄金文件 | 覆盖核心对象、拓扑变化、单位和错误分类 |
|
||||||
|
| BASE-04 | 建立 Web OCCT/FreeCAD 双端比较器 | 树、Property、Shape、拓扑、质量属性和诊断可差分 |
|
||||||
|
| BASE-05 | 生成 SBOM、许可证、来源和已知漏洞报告 | 所有 WASM/Worker/求解器/字典可追溯 |
|
||||||
|
|
||||||
|
### 5.1 Bitbybit/OCCT 源码构建轨道
|
||||||
|
|
||||||
|
| ID | 任务 | 退出条件 |
|
||||||
|
|---|---|---|
|
||||||
|
| BYB-01 | 盘点 Bitbybit TypeScript API、OCCT Worker 命令、IO、缓存和三角化覆盖 | 每个 GEO/PART/PD/TSN/FC 几何任务标记 `reuse`/`extend`/`custom` |
|
||||||
|
| BYB-02 | 锁定 Bitbybit 源码提交、子模块、补丁和上游 OCCT 版本 | npm 包、源码提交、生成绑定和 WASM 来源链一致 |
|
||||||
|
| BYB-03 | 建立 Bitbybit + OCCT Emscripten 可复现构建 | 干净容器产出同版本、同导出符号和可追溯 hash |
|
||||||
|
| BYB-04 | 扩展 OCCT 绑定生成规则 | 暴露拓扑历史、BRepCheck、几何属性、导入导出及各工作台缺失算法,不泄漏原始指针 |
|
||||||
|
| BYB-05 | 建立版本化 Geometry Worker 协议和能力探测 | 现有 npm provider 与自构建 provider 可替换、可诊断、可取消 |
|
||||||
|
| BYB-06 | 建立 Shape 所有权、共享拓扑和细粒度释放策略 | Boolean/历史共享 Shape 长会话无悬空和不可控增长 |
|
||||||
|
| BYB-07 | 建立 Bitbybit、高层扩展绑定、原生 FreeCAD 三方差分 | 几何、拓扑、错误、公差和资源行为差异可审计 |
|
||||||
|
| BYB-08 | 上游同步和回归策略 | 更新 Bitbybit/OCCT 时自动生成 API/ABI/黄金差异,不静默漂移 |
|
||||||
|
|
||||||
|
## 6. W1:平台与领域内核
|
||||||
|
|
||||||
|
| ID 范围 | 任务组 | 必须补齐的能力 |
|
||||||
|
|---|---|---|
|
||||||
|
| CORE-01..04 | App 文档内核 | 多文档、DocumentObject/Extension、动态 Property、Group/Link/LinkGroup、UUID/InternalName/Label、生命周期 |
|
||||||
|
| CORE-05..08 | 事务和状态 | 嵌套事务、Undo/Redo、剪切复制粘贴、删除传播、保存点、恢复、只读、最近有效 Shape 回滚 |
|
||||||
|
| CORE-09..12 | Body/容器 | Body/Tip、Feature 顺序、Origin、Datum、Part/Group、Support、MapMode、AttachmentOffset、局部坐标链、单实体规则 |
|
||||||
|
| API-01..05 | Facade/Worker | 版本化 schema、事件、取消、进度、诊断、Bitbybit/自构建 provider 能力探测、Worker 崩溃恢复、并发与句柄所有权 |
|
||||||
|
| EXU-01..08 | Quantity/Expression | 全单位 schema、完整 parser/函数、维度/舍入、对象与 Spreadsheet 引用、循环、locale、自动完成、迁移黄金 |
|
||||||
|
| DAG-01..11 | 依赖与重算 | Link/Expression/TopoRef 统一边、SCC、稳定调度、并行层、抑制、错误传播、最小闭包、性能计数和 1000 对象基准 |
|
||||||
|
| DB-01..08 | SQLite/OPFS | 真实迁移、单写者、配额、资源 GC、检查点、崩溃注入、多标签页、备份恢复和无 OPFS 降级 |
|
||||||
|
| VIEW-01..10 | Gui/Viewport | 相机/导航、显示模式、对象/面/边/点拾取、预选、稳定 TopoRef、剖切、裁剪、测量、网格、增量缓存、LOD、GPU 回收 |
|
||||||
|
| UI-01..10 | 应用交互 | 菜单/工具栏、Combo/Task/Property、命令状态、状态栏、报告视图、多选 mixed、搜索、键盘、a11y、窄屏、主题/语言/偏好 |
|
||||||
|
|
||||||
|
## 7. W2:稳定拓扑命名与 Sketcher
|
||||||
|
|
||||||
|
### 7.1 稳定拓扑命名
|
||||||
|
|
||||||
|
| ID | 任务 | 退出条件 |
|
||||||
|
|---|---|---|
|
||||||
|
| TSN-01 | 采集 30 个基础和复杂模型的 FreeCAD 命名/错误行为 | 输入、逐代历史和选择结果齐全 |
|
||||||
|
| TSN-02 | 冻结版本化 `TopoRef` schema 和迁移 | stable/ambiguous/deleted 可往返 |
|
||||||
|
| TSN-03 | 自构建 OCCT 绑定暴露 Generated/Modified/IsDeleted | Pad/Pocket/Dress-up/Boolean 返回逐输入历史 |
|
||||||
|
| TSN-04 | 建立曲面、曲线、邻接、方向和几何签名 | 不依赖三角网格精度或瞬时索引 |
|
||||||
|
| TSN-05 | 一对一/一对多评分、置信度和迁移 | 参数编辑和平移保持安全引用 |
|
||||||
|
| TSN-06 | 对称、重复、分裂、合并的歧义策略 | 低置信度不强绑,保留候选 |
|
||||||
|
| TSN-07 | Boolean、多实体、Pattern 和跨 Body 历史 | 每个源对象均有可解释映射 |
|
||||||
|
| TSN-08 | LinkSub/外部几何/数据库/FCStd 持久化 | 保存加载和版本升级引用稳定 |
|
||||||
|
| TSN-09 | 3D 拾取、预选、高亮、候选替换和附着 API | 全链路只通过 Facade |
|
||||||
|
| TSN-10 | 100 模型、1000 参数变异和随机拓扑回放 | 无静默错绑,差异可审计 |
|
||||||
|
|
||||||
|
当前工作区仅在增加 `occt-native` 类型化历史合同,尚未连接自构建 OCCT Worker,不满足 TSN-03。
|
||||||
|
|
||||||
|
当前 Bitbybit 声明面的可重复证据由 `./npmw run check:bitbybit-history` 固化:布尔 API 仍只有 `HasGenerated()`,`BOPAlgo_Builder.Modified(shape)` 属于部分能力,且列表没有稳定索引读取;该门禁会在绑定升级后要求重新审计。
|
||||||
|
|
||||||
|
Bitbybit npm 快照仍锁定为 `.cache/bitbybit/bitbybit` 提交 `d3f52bd1fcf78148b60800d69ed6b42e53f4ad06`、版本 `1.1.1`;它本身是生成 WASM/TypeScript 产物。现在已补充独立原生 provider:使用缓存 OCCT 提交 `bb75017e915f6509a4bfa8b551f853352754c2da`(8.0.0)和 `TKBO` 编译完整历史 Embind。由于两个 WASM 实例的 `TopoDS_Shape*` 地址空间不同,provider 内部句柄不得直接传给 Bitbybit;跨 provider 接入必须走版本化序列化协议。
|
||||||
|
|
||||||
|
### 7.2 Sketcher
|
||||||
|
|
||||||
|
| ID 范围 | 任务组 | 必须补齐的能力 |
|
||||||
|
|---|---|---|
|
||||||
|
| SK-01..04 | 清册与求解器 | 全命令/约束 manifest、planegcs 最小依赖、Emscripten 构建、确定性 Worker、取消/过期隔离 |
|
||||||
|
| SK-05 | 几何 | 点、线、折线、圆、圆弧、椭圆/椭圆弧、B-spline、周期曲线、构造几何、内部/外部几何 |
|
||||||
|
| SK-06..07 | 约束 | 几何、尺寸、驱动/参考、Block、表达式;DOF、冗余、冲突、部分冗余与诊断分类 |
|
||||||
|
| SK-08 | 支持与外部几何 | 基准面/平面/面附着、投影、CarbonCopy、跨对象 TopoRef 迁移 |
|
||||||
|
| SK-09 | 编辑器 | 创建/修剪/延伸/分割、拖拽、自动约束、约束列表、虚拟空间、键鼠回放 |
|
||||||
|
| SK-10 | B-spline 专项 | 控制点、节点、权重、次数、周期、约束限制和编辑器 |
|
||||||
|
| SK-11 | 特征轮廓 | 闭合/开放/多环/自交验证,驱动 Pad/Pocket/Revolution/Pipe/Loft |
|
||||||
|
| SK-12 | 黄金与压力 | 500 模型,欠约束/全约束/冲突/冗余/拖拽/外部几何覆盖 |
|
||||||
|
|
||||||
|
## 8. W3:Part 与 Part Design 参数化闭环
|
||||||
|
|
||||||
|
| ID 范围 | 任务组 | 必须补齐的能力 |
|
||||||
|
|---|---|---|
|
||||||
|
| PART-01..04 | 基础 Shape | 全基本体、Placement、ShapeBuilder、Line/Plane/Helix/Prism/Wedge、Compound/CompSolid/Solid/Shell/Wire |
|
||||||
|
| PART-05..08 | Boolean/检查 | Union/Cut/Common/Fragments、Section/Slice、Refine、BRepCheck、容差、自交、修复和错误语义 |
|
||||||
|
| PART-09..12 | 生成/修改 | Extrude/Revolve/Mirror/Scale/Offset/Thickness/Loft/Sweep/Pipe、Fillet/Chamfer/Draft |
|
||||||
|
| PART-13..16 | 拓扑/互操作 | ShapeBinder、SubShapeBinder、多实体历史、测量、颜色、STEP/IGES/BREP 与黄金套件 |
|
||||||
|
| PD-01..03 | Body/基准/附着 | Body/Tip/Origin、DatumPlane/Line/Point、Support/MapMode/AttachmentOffset、跨 Body 规则 |
|
||||||
|
| PD-04..07 | 基础特征 | Pad/Pocket/Revolution/Groove 的两长度、对称、反向、到面/到首个/到最后、Midplane、Taper |
|
||||||
|
| PD-08..10 | 放样与管道 | Additive/Subtractive Loft/Pipe、截面排序、路径、过渡、方向和失败恢复 |
|
||||||
|
| PD-11..13 | Dress-up | Fillet/Chamfer/Draft/Thickness 的稳定选边选面、变半径/模式、局部历史 |
|
||||||
|
| PD-14..16 | 变换与孔 | Mirrored/Linear/Polar/MultiTransform 的特征列表/轴面/单实体;Hole 轮廓、定位、沉孔、锪孔、钻尖、螺纹和标准 |
|
||||||
|
| PD-17..18 | 高级链与验收 | Boolean、ShapeBinder、Clone、抑制/Tip 重定向、多配置;完整参数编辑/Undo/FCStd 回放 |
|
||||||
|
|
||||||
|
## 9. W4:文件、项目和数据交换
|
||||||
|
|
||||||
|
| ID | 任务 | 退出条件 |
|
||||||
|
|---|---|---|
|
||||||
|
| FC-01..03 | FCStd 样例与安全容器 | 核心/扩展对象样例;ZIP/XML/资源限制和恶意文件拒绝 |
|
||||||
|
| FC-04 | `Document.xml`/`GuiDocument.xml` 双向映射 | 树、Property、Extension、视图状态可往返 |
|
||||||
|
| FC-05 | BRep/Shape/缩略图资源读写 | 精度、拓扑数、颜色和资源引用有差异报告 |
|
||||||
|
| FC-06 | Expression/Unit/Link/TopoRef/Sketch 序列化 | 保存后可由 FreeCAD 重算或明确 proxy |
|
||||||
|
| FC-07 | 未知对象/属性/扩展原样保留 | 读写后无静默丢失,脚本永不自动执行 |
|
||||||
|
| FC-08 | STEP/IGES/BREP 导入导出 | 100 个模型的单位、颜色、几何和拓扑黄金 |
|
||||||
|
| FC-09 | STL/OBJ/PLY/DXF/SVG/PDF/glTF/IFC/CSV/JT/IDF | 每种格式有能力、精度、失败和 round-trip 报告 |
|
||||||
|
| FC-10 | FreeCAD↔WebCAD↔FreeCAD 双向回放 | 核心对象可编辑,代理对象字节/语义保留 |
|
||||||
|
| FC-11 | 大文件/损坏/中断/恢复 | 能力上限内不崩溃,超限明确拒绝或流式降级 |
|
||||||
|
| PKG-01..04 | `.webcad` 项目包 | manifest/hash/schema、导入导出、备份、迁移回滚、干净浏览器恢复 |
|
||||||
|
|
||||||
|
## 10. W5-W7:工作台任务
|
||||||
|
|
||||||
|
### 10.1 生产文档与二维
|
||||||
|
|
||||||
|
| ID 范围 | 模块 | 任务范围 |
|
||||||
|
|---|---|---|
|
||||||
|
| TD-01..15 | TechDraw | Page/Template、正投影视图、投影组、剖视/局部/辅助/断开、隐藏线、尺寸/公差/GD&T、中心线、气泡、注释、SVG/PDF/打印比例、TopoRef 更新 |
|
||||||
|
| SS-01..10 | Spreadsheet | Sheet/Cell、公式、alias、样式/合并、行列操作、对象绑定、DAG 循环、CSV、XLSX 能力决策、大表性能 |
|
||||||
|
| DRAFT-01..14 | Draft | Working Plane、网格/捕捉、Line/Wire/Arc/Circle/Ellipse/Rectangle/Polygon/BSpline/BezCurve、Text/Dimension、Move/Rotate/Scale/Offset/Trim/Array/Clone、Layer 与参数化 |
|
||||||
|
| PLOT-01..06 | Plot | 数据序列、轴/图例/样式、Spreadsheet/结果绑定、更新、图片/SVG/CSV 输出 |
|
||||||
|
|
||||||
|
### 10.2 高级建模和工程
|
||||||
|
|
||||||
|
| ID 范围 | 模块 | 任务范围 |
|
||||||
|
|---|---|---|
|
||||||
|
| ASM-01..16 | Assembly | Assembly/Component/Link、LCS/Joint、固定/距离/角度/滑动/转动/球等连接、Ondsel solver WASM、变体、运动、碰撞、爆炸图、BOM、FCStd |
|
||||||
|
| BIM-01..20 | BIM | Site/Building/Level/Space、Wall/Structure/Slab/Roof/Stairs/Window/Door/Equipment、材料/属性集、Draft 联动、IFC2x3/IFC4、分类、数量/清单、代理 |
|
||||||
|
| MESH-01..14 | Mesh/MeshPart | STL/OBJ/PLY、生成/转换、分析、法向、非流形、自交、孔修补、平滑、简化、布尔、截面、大网格 Worker/LOD |
|
||||||
|
| SURF-01..14 | Surface | Bezier/BSpline、Ruled/Loft/Sweep、Fill、Sections、Offset、Extend、Trim、Split、Join/Sew、连续性和容差、TopoRef |
|
||||||
|
| FEM-01..18 | Fem | Analysis、Material、Geometry/element sets、网格器、边界/载荷、CalculiX/Elmer 等求解器能力、WASM/远端策略、求解、结果场、单位、VTK/导出 |
|
||||||
|
| CAM-01..20 | CAM | Job/Stock、ToolBit/Controller、SetupSheet、Profile/Pocket/Contour/Drill/Adaptive/Surface、dress-up、仿真、碰撞、post sandbox、G-code 黄金 |
|
||||||
|
| ROBOT-01..12 | Robot | 机器人模型、轴/姿态、轨迹、工作空间、运动学、碰撞、程序与控制器导出;外部设备能力分级 |
|
||||||
|
| INSP-01..10 | Inspection/Measure | 距离/角度/面积/体积/曲率、剖切、偏差、几何检查、TopoRef 标注、报告、单位/精度和导出 |
|
||||||
|
|
||||||
|
### 10.3 数据、应用与长尾模块
|
||||||
|
|
||||||
|
| ID 范围 | 模块 | 任务范围 |
|
||||||
|
|---|---|---|
|
||||||
|
| PTS-01..08 | Points | 点云导入、显示、裁剪、变换、下采样、曲面/网格转换、大数据 Worker |
|
||||||
|
| RE-01..10 | ReverseEngineering | 点/网格采样、曲线/曲面拟合、截面、识别、误差可视化和可编辑结果 |
|
||||||
|
| OSC-01..08 | OpenSCAD | `.scad` 导入/导出、CSG 映射、外部 OpenSCAD 可用性、沙箱、颜色/单位和代理 |
|
||||||
|
| IDF-01..06 | IDF | PCB/元件/孔/板框导入导出、单位、层/颜色、往返 |
|
||||||
|
| JT-01..06 | JtReader | JT 版本/压缩能力、装配树、几何/PMI/颜色、流式读取、代理 |
|
||||||
|
| MAT-01..08 | Material | 材料卡、物理/外观属性、继承、库、单位、BIM/FEM/CAM 共享和 FCStd |
|
||||||
|
| IMP-01..08 | Import | 格式注册、探测、单位、颜色、进度、取消、错误和批量导入统一层 |
|
||||||
|
| APPMOD-01..08 | Start/Help/Show/Web/Cloud | 最近项目、模板、示例、离线帮助、对象展示/分享、URL 安全、云连接器能力和隐私 |
|
||||||
|
| ADDON-01..10 | AddonManager | 插件目录、签名/来源、依赖、安装/更新/卸载、权限、隔离、回滚、离线包;不可执行桌面 Python 时明确代理 |
|
||||||
|
| SCRIPT-01..10 | Python/宏/控制台 | FreeCAD API 兼容面、Pyodide/受限解释器决策、文件/网络权限、录制/回放、超时、资源配额、不可端口 API 诊断 |
|
||||||
|
| GUI-REST-01..10 | 桌面通用功能 | 多窗口/MDI 等价、剪贴板、打印、参数编辑器、自定义工具栏/快捷键、主题、语言、导航设备能力 |
|
||||||
|
| DEV-01..06 | Sandbox/TemplatePyMod/Test/Tux | 开发模板、测试运行器、GUI 自动化入口和明确非产品能力矩阵 |
|
||||||
|
|
||||||
|
## 11. W8:质量与发布门禁
|
||||||
|
|
||||||
|
| ID | 任务 | 退出条件 |
|
||||||
|
|---|---|---|
|
||||||
|
| QA-01 | 单元、属性、parser、图算法与领域状态覆盖 | 核心错误分支和不变量有自动测试 |
|
||||||
|
| QA-02 | FreeCAD CLI/GUI 对照器 | 每个兼容命令至少一个成功和一个失败 fixture |
|
||||||
|
| QA-03 | Chrome/Firefox/Safari E2E 与视觉基线 | 目标版本矩阵可审计,无 P0/P1 回归 |
|
||||||
|
| QA-04 | 几何/求解确定性与 fuzzing | 公差内稳定,随机输入不崩溃/错绑 |
|
||||||
|
| QA-05 | 性能和内存 | 1000 对象、百万三角形、大装配、大表、长会话 P95/峰值达预算 |
|
||||||
|
| QA-06 | Worker/WASM/OPFS 故障注入 | 取消、崩溃、配额、断电点、过期结果和恢复行为通过 |
|
||||||
|
| QA-07 | 文件与消息安全 | zip bomb、路径穿越、XML、表达式、脚本、post、插件和 Worker 消息 fuzzing 通过 |
|
||||||
|
| QA-08 | 可访问性/国际化/移动 | 键盘核心流、焦点、读屏、locale、长文本和目标视口通过 |
|
||||||
|
| REL-01 | PWA/离线/cache/COOP/COEP/CSP | 首装、升级、离线和缓存失效可恢复 |
|
||||||
|
| REL-02 | 数据迁移与回滚演练 | 最近支持版本可升级,可回滚或无损导出 |
|
||||||
|
| REL-03 | SBOM/许可证/漏洞/产物签名 | 无未知二进制和未处置高危问题 |
|
||||||
|
| REL-04 | 能力矩阵和差异报告 | 每项功能有准确等级,无未解释 `unsupported` |
|
||||||
|
| REL-05 | 发布 runbook/遥测/支持流程 | 可部署、监控、诊断、恢复和撤回 |
|
||||||
|
|
||||||
|
## 12. BYB-04 原生拓扑历史与跨 WASM 传输增量
|
||||||
|
|
||||||
|
本轮已完成 BYB-04 的第一版实际编译和 BYB-05 的协议合同层:`native/occt-history` 从缓存 OCCT `bb75017e915f6509a4bfa8b551f853352754c2da`(8.0.0)构建 `TKBO`、`TKDESTEP`、`TKXSBase`、`TKDE` 和 Embind WASM,`./npmw run test:occt-history` 通过,`shapeToStep -> booleanHistoryFromStep(cut)` smoke case 返回 42 条原生关系记录。`mapNativeOcctHistoryRecords`、`createNativeOcctStepHistoryBridge` 和版本化 `NativeOcctHistoryCoordinator` 已把 provider 协议接入 Facade 类型边界,覆盖 STEP 校验、取消、超时和 stale 文档版本。该 provider 当前仍是独立 WASM 实例;跨 Bitbybit 主 WASM 只能通过 STEP 文本序列化交换,不能直接传递指针。
|
||||||
|
|
||||||
|
该增量不提升完整 FreeCAD 兼容等级:原生 provider 现在可通过 Facade `geometry.configureNativeHistory()` 显式配置并进入 Boolean 重算,但仍不是默认 GeometryRuntime,也未覆盖全部 PartDesign/工作台操作;浏览器真实 WASM 实例化、Worker 崩溃恢复、STEP 精度/单位策略、BRep/IGES 和 Bitbybit↔原生↔FreeCAD 三方黄金差分仍由 `BYB-05..08`、`TSN-03..10`、`FC-08` 和发布门禁负责。
|
||||||
|
|
||||||
|
## 13. 当前真实状态与最近执行队列
|
||||||
|
|
||||||
|
当前仅 `Part Design`、`Part`、`Sketcher` 是 `experimental`;Draft、TechDraw、Spreadsheet、Assembly、Mesh 只有 UI/只读原型,其余多数模块未实现。FCStd 已有安全预检、元数据、对象属性字段投影和 proxy 报告,但仍不可编辑/双向 round-trip。基础 oracle 只有 5 个 Part 场景。
|
||||||
|
|
||||||
|
本机参考环境、R0-R9 执行批次、立即队列和单项完成定义见 [FreeCAD 1.1.1 全功能对标执行路线图](freecad-reference-execution-roadmap.zh-CN.md)。当前缓存 oracle 是 headless `Part/Material/Measure` 子集;完整桌面工作台行为必须由 O2 oracle 单独采集。
|
||||||
|
|
||||||
|
最近执行队列不得跳过依赖:
|
||||||
|
|
||||||
|
1. 完成 `BASE-01..04`、`INV-01..06` 和 `BYB-01..03`,生成全命令/对象/属性清册、完整 oracle 和可复现 Bitbybit/OCCT 源码构建。
|
||||||
|
2. 完成 `BYB-04..08` 与 `TSN-03..10`:扩展真正 OCCT 历史、稳定签名、跨 Boolean、3D 选择与三方回放。
|
||||||
|
3. 完成 `SK-03..12`:planegcs WASM、全约束、编辑器、外部几何和压力套件。
|
||||||
|
4. 完成 `CORE-09..12`、`PART-*`、`PD-*`,通过 Part Design 参数化闭环。
|
||||||
|
5. 完成 `FC-04..11` 和 `PKG-*`,通过 FCStd 双向 round-trip。
|
||||||
|
6. 依次完成 `TD-*`、`SS-*`、`DRAFT-*`、`PLOT-*`。
|
||||||
|
7. 依次完成 `ASM-*`、`BIM-*`、`MESH-*`、`SURF-*`。
|
||||||
|
8. 完成 FEM/CAM/Robot/Inspection、数据模块、应用模块、脚本/扩展和桌面通用能力。
|
||||||
|
9. 完成 `QA-*`、`REL-*`,F0-F9/G0-G9 全部通过后才允许宣称“完整对标”。
|
||||||
|
|
||||||
|
## 14. 里程碑维护规则
|
||||||
|
|
||||||
|
每个独立里程碑必须同步实现、聚焦测试、本账本状态、`config/compatibility-matrix.json`、总体方案和接续状态。统一执行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./npmw run verify
|
||||||
|
git diff --check
|
||||||
|
jq empty config/compatibility-matrix.json
|
||||||
|
```
|
||||||
|
|
||||||
|
涉及原生 oracle 的里程碑另执行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./npmw run test:golden:freecad
|
||||||
|
```
|
||||||
|
|
||||||
|
提交信息、差异报告和兼容等级必须准确描述边界;不得把 facade 合同、类型、UI 或 mock 测试写成 FreeCAD 几何/文件兼容已完成。
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
# 完整 FreeCAD 对标实施方案
|
# 完整 FreeCAD 对标实施方案
|
||||||
|
|
||||||
> 本文是“功能完全对标 FreeCAD”的工程基线和任务分解,不把当前已有的页面、MockFacade 或少量 OCCT 特征误报为完成。所有“支持”都必须经过锁定版本 FreeCAD 的行为黄金测试和文件互操作测试。
|
> 本文是“功能完全对标 FreeCAD”的工程基线和任务分解,不把当前已有的页面、MockFacade 或少量 OCCT 特征误报为完成。所有“支持”都必须经过锁定版本 FreeCAD 的行为黄金测试和文件互操作测试。
|
||||||
|
>
|
||||||
|
> 全模块、逐领域任务 ID、执行波次和统一退出条件以 [FreeCAD 1.1.1 全功能对标主任务账本](freecad-complete-parity-backlog.zh-CN.md) 为准;本文保留架构决策、专项设计和阶段门。两者不一致时,能力状态以 `config/compatibility-matrix.json` 为机器可读真值,范围和待办以主任务账本为准。
|
||||||
|
|
||||||
## 1. 目标与边界
|
## 1. 目标与边界
|
||||||
|
|
||||||
@@ -12,6 +14,8 @@
|
|||||||
|
|
||||||
对 React 和页面公开的唯一业务入口是 `BitBybitWebCadFacade`。页面不得直接导入 Three.js、OCCT、OCCT Worker、SQLite 或 OPFS;这些实现只能由 Facade 适配器和 Worker 使用。新增工作台、命令、属性、文件格式和诊断,必须先扩展版本化 Facade 合同,再接入页面。
|
对 React 和页面公开的唯一业务入口是 `BitBybitWebCadFacade`。页面不得直接导入 Three.js、OCCT、OCCT Worker、SQLite 或 OPFS;这些实现只能由 Facade 适配器和 Worker 使用。新增工作台、命令、属性、文件格式和诊断,必须先扩展版本化 Facade 合同,再接入页面。
|
||||||
|
|
||||||
|
Bitbybit 必须被充分用作几何、Worker、IO、三角化和缓存的首选实现层。对 Bitbybit 1.1.1 npm 包未暴露但 FreeCAD 对标必需的能力,允许锁定 Bitbybit 与 OCCT 源码提交后扩展绑定并用 Emscripten 自构建 WASM;不得以现成包 API 不足作为降低功能范围的理由。自构建 provider 与现有 provider 必须共享 Facade/Worker 合同、能力探测、句柄生命周期、取消/诊断和黄金测试。
|
||||||
|
|
||||||
### 1.3 “完整对标”的可验收定义
|
### 1.3 “完整对标”的可验收定义
|
||||||
|
|
||||||
| 等级 | 含义 | 允许的产品表述 |
|
| 等级 | 含义 | 允许的产品表述 |
|
||||||
@@ -24,6 +28,16 @@
|
|||||||
|
|
||||||
没有黄金测试、差异报告和迁移策略的功能,不得标记为 `exact` 或 `compatible`。浏览器限制必须体现为能力矩阵和用户可见诊断,不能静默删除 FreeCAD 行为。
|
没有黄金测试、差异报告和迁移策略的功能,不得标记为 `exact` 或 `compatible`。浏览器限制必须体现为能力矩阵和用户可见诊断,不能静默删除 FreeCAD 行为。
|
||||||
|
|
||||||
|
### 1.4 全量范围和清册门禁
|
||||||
|
|
||||||
|
锁定源码 `src/Mod` 的 34 个目录全部纳入范围:`AddonManager`、`Assembly`、`BIM`、`CAM`、`Cloud`、`Draft`、`Fem`、`Help`、`Idf`、`Import`、`Inspection`、`JtReader`、`Material`、`Measure`、`Mesh`、`MeshPart`、`OpenSCAD`、`Part`、`PartDesign`、`Plot`、`Points`、`ReverseEngineering`、`Robot`、`Sandbox`、`Show`、`Sketcher`、`Spreadsheet`、`Start`、`Surface`、`TechDraw`、`TemplatePyMod`、`Test`、`Tux`、`Web`。同时覆盖 `App`、`Base`、`Gui`、`Main` 暴露的文档、Property、事务、选择、视图、参数、命令、Python 控制台、宏、偏好、快捷键、多文档、剪贴板和打印行为。
|
||||||
|
|
||||||
|
`P0-02/INV-01..06` 未完成以前,“全部功能”范围仍视为未冻结。源码模块中的测试、模板或开发设施可以被分为开发等价能力或明确 `unsupported`,但不得遗漏;Python、宏、插件、外部求解器和后处理器必须有沙箱与权限决策。
|
||||||
|
|
||||||
|
项目 Node/npm 命令统一经 `./npmw` 执行;系统 `npm` 的结果不得用作阶段门证据。
|
||||||
|
|
||||||
|
本机 `FreeCADCmd` 可作为第一层行为 oracle:`./npmw run probe:freecad-reference` 采集版本、提交、已加载模块和核心 Property 元数据;当前构建是无界面 `Part/Material/Measure` 子集。完整桌面/工作台回放必须另行启用 FreeCAD 原生模块,不能将 headless probe 的可用性扩展到未构建的工作台。
|
||||||
|
|
||||||
## 2. 当前差距与解决策略
|
## 2. 当前差距与解决策略
|
||||||
|
|
||||||
| 领域 | 当前切片 | 达到对标还缺少的核心能力 | 主任务包 |
|
| 领域 | 当前切片 | 达到对标还缺少的核心能力 | 主任务包 |
|
||||||
@@ -56,6 +70,7 @@
|
|||||||
- 依赖循环、求解冲突、拓扑歧义和未知文件对象必须显式失败并保留诊断。
|
- 依赖循环、求解冲突、拓扑歧义和未知文件对象必须显式失败并保留诊断。
|
||||||
- Undo/Redo、自动保存和资源引用计数必须以同一事务边界提交。
|
- Undo/Redo、自动保存和资源引用计数必须以同一事务边界提交。
|
||||||
- Worker 结果不可直接改变 React 状态,必须经 Facade 校验和事件投影。
|
- Worker 结果不可直接改变 React 状态,必须经 Facade 校验和事件投影。
|
||||||
|
- 几何能力按 `reuse-bitbybit`、`extend-bitbybit-binding`、`custom-occt-binding` 三档登记;后两档必须锁定源码、补丁、ABI、Emscripten 工具链、许可证和 WASM hash,并完成 Bitbybit/FreeCAD 三方差分。
|
||||||
|
|
||||||
## 4. 专项一:稳定子形状命名(TSN)
|
## 4. 专项一:稳定子形状命名(TSN)
|
||||||
|
|
||||||
@@ -196,11 +211,15 @@
|
|||||||
| Surface | 曲面创建、偏移、放样、填充、修剪、缝合 | SURF-01..SURF-14:NURBS/BSpline 参数、边界和缝合容差 | OCCT 扩展、TSN |
|
| Surface | 曲面创建、偏移、放样、填充、修剪、缝合 | SURF-01..SURF-14:NURBS/BSpline 参数、边界和缝合容差 | OCCT 扩展、TSN |
|
||||||
| CAM | Job、刀具、路径、后处理、仿真和 G-code | CAM-01..CAM-20:刀具库、碰撞、后处理器沙箱、单位和输出黄金 | FEM/Mesh 可选,安全门 |
|
| CAM | Job、刀具、路径、后处理、仿真和 G-code | CAM-01..CAM-20:刀具库、碰撞、后处理器沙箱、单位和输出黄金 | FEM/Mesh 可选,安全门 |
|
||||||
| Inspection/Measure | 测量、剖切、偏差、标注和报告 | INSP-01..INSP-10:TopoRef 选择、精度、报告和导出 | TSN、EXU |
|
| Inspection/Measure | 测量、剖切、偏差、标注和报告 | INSP-01..INSP-10:TopoRef 选择、精度、报告和导出 | TSN、EXU |
|
||||||
| Raytracing/Robot/其他 | 基线版本清单中的其余官方模块 | WB-REST-01..WB-REST-20:逐模块决定 WASM 端口或明确不支持 | P0 manifest |
|
| 数据/交换模块 | Import、Points、ReverseEngineering、OpenSCAD、IDF、JT、Material、MeshPart | IMP/PTS/RE/OSC/IDF/JT/MAT:格式、单位、颜色、代理、几何转换和往返 | FC、QA |
|
||||||
|
| 应用/扩展模块 | Start、Help、AddonManager、Cloud、Show、Web | APPMOD/ADDON:最近项目、离线帮助、扩展治理、分享/云和安全边界 | P0 manifest、安全门 |
|
||||||
|
| Robot/Plot | 机器人、轨迹、运动学、数据绘图和结果输出 | ROBOT-01..12、PLOT-01..06 | DAG、EXU、QA |
|
||||||
|
| 脚本/宏/桌面通用 | Python 控制台、宏、插件、偏好、自定义、剪贴板、打印、多文档 | SCRIPT/GUI-REST:沙箱、权限、等价 Web 工作流或逐项不支持诊断 | P0 manifest、安全门 |
|
||||||
|
| 开发/测试模块 | Sandbox、TemplatePyMod、Test、Tux | DEV-01..06:保留开发/自动化等价入口或明确非产品能力 | P0 manifest |
|
||||||
|
|
||||||
### 8.3 工作台交付顺序
|
### 8.3 工作台交付顺序
|
||||||
|
|
||||||
先完成 `PartDesign + Part + Sketcher + TechDraw + Spreadsheet` 的参数化闭环,再做 Draft/Assembly/BIM/Mesh/Surface,最后处理 FEM/CAM/Raytracing 等需要额外数值库或后处理器的模块。每个工作台在进入下一层前必须通过对象创建、编辑、重算、撤销、保存、加载和导出七项门禁。
|
先完成 `PartDesign + Part + Sketcher` 的参数化闭环与 FCStd 双向往返,再做 TechDraw/Spreadsheet/Draft/Plot,随后做 Assembly/BIM/Mesh/Surface,最后处理 FEM/CAM/Robot/Inspection、数据交换模块、应用/扩展模块和脚本/桌面通用能力。每个工作台在进入下一层前必须通过对象创建、编辑、重算、撤销、保存、加载和导出七项门禁。
|
||||||
|
|
||||||
## 9. 专项六:文件兼容与迁移(FC)
|
## 9. 专项六:文件兼容与迁移(FC)
|
||||||
|
|
||||||
@@ -297,13 +316,17 @@
|
|||||||
3. 实现 `DAG-01..DAG-06` 与 `EXU-01..EXU-06`,让属性修改从“Touched 标记”升级为可解释的依赖重算。
|
3. 实现 `DAG-01..DAG-06` 与 `EXU-01..EXU-06`,让属性修改从“Touched 标记”升级为可解释的依赖重算。
|
||||||
4. 实现 `TSN-02..TSN-07`,再把附着、Pocket up-to-face、测量和 TechDraw 选择切换到 `TopoRef`。
|
4. 实现 `TSN-02..TSN-07`,再把附着、Pocket up-to-face、测量和 TechDraw 选择切换到 `TopoRef`。
|
||||||
5. 编译/验证 `SK-03`,然后按基础约束→高级约束→编辑器→PartDesign 轮廓的顺序推进。
|
5. 编译/验证 `SK-03`,然后按基础约束→高级约束→编辑器→PartDesign 轮廓的顺序推进。
|
||||||
6. 以 PartDesign 核心闭环为第一条完整垂直链,随后接入 FCStd 读写和 round-trip。
|
6. 以 PartDesign 核心闭环为第一条完整垂直链,随后完成 FCStd 对象/属性/Shape/视图双向读写和 round-trip。
|
||||||
7. 按工作台表逐个实现并验收,任何暂未端口的模块保持 `read-only/proxy/unsupported`,不提前宣称完整。
|
7. 按 `TD→SS→DRAFT/PLOT→ASM/BIM/MESH/SURF→FEM/CAM/ROBOT/INSP` 顺序完成工作台七项门禁。
|
||||||
|
8. 完成 Import/Points/ReverseEngineering/OpenSCAD/IDF/JT/Material、Start/Help/AddonManager/Cloud/Show/Web、Python/宏/插件和桌面通用能力的逐项兼容决策与实现。
|
||||||
|
9. 完成跨浏览器、OPFS 故障、性能/内存、安全 fuzzing、a11y/i18n、PWA、升级回滚、SBOM 和发布差异报告。
|
||||||
|
|
||||||
## 15. 完成定义
|
## 15. 完成定义
|
||||||
|
|
||||||
“完整 FreeCAD 对标”只有在 F0-F9 全部阶段门通过、核心与扩展工作台均有兼容等级、稳定子形状命名/Sketcher/Expression/单位/DAG/重算/FCStd 均有黄金报告、跨浏览器和安全门禁通过,并且发布文档列出所有差异后才能使用。当前仓库仍处于核心 Facade、OCCT 几何和属性编辑的进行中阶段,下一工作目标按第 14 节执行。
|
“完整 FreeCAD 对标”只有在 F0-F9 全部阶段门通过、核心与扩展工作台均有兼容等级、稳定子形状命名/Sketcher/Expression/单位/DAG/重算/FCStd 均有黄金报告、跨浏览器和安全门禁通过,并且发布文档列出所有差异后才能使用。当前仓库仍处于核心 Facade、OCCT 几何和属性编辑的进行中阶段,下一工作目标按第 14 节执行。
|
||||||
|
|
||||||
|
完整对标还要求 34 个源码模块和 App/Base/Gui 通用能力全部进入机器可读清册;任何尚未登记的命令、对象、属性、格式、Python/宏/插件入口都会阻断 G9。详细关闭条件见主任务账本第 3、11、13 节。
|
||||||
|
|
||||||
## 16. 已实现切片与剩余边界
|
## 16. 已实现切片与剩余边界
|
||||||
|
|
||||||
本轮已完成 `EXU-02..EXU-06` 的基础版本:Quantity 以 mm/deg 等规范单位保存,表达式支持四则运算、对象属性引用和维度错误;已完成 `DAG-01..DAG-06` 的领域版本:Link/Expression 边、下游传播、SCC 循环检测、稳定拓扑计划、generation 和 recompute 状态已进入 Facade 与 SQLite schema v3。还未把每个 DAG 节点接入真实 OCCT/Sketcher 计算 Worker,因此当前是可验证的业务调度层,不是完整几何重算。
|
本轮已完成 `EXU-02..EXU-06` 的基础版本:Quantity 以 mm/deg 等规范单位保存,表达式支持四则运算、对象属性引用和维度错误;已完成 `DAG-01..DAG-06` 的领域版本:Link/Expression 边、下游传播、SCC 循环检测、稳定拓扑计划、generation 和 recompute 状态已进入 Facade 与 SQLite schema v3。还未把每个 DAG 节点接入真实 OCCT/Sketcher 计算 Worker,因此当前是可验证的业务调度层,不是完整几何重算。
|
||||||
@@ -348,6 +371,8 @@ PartDesign Mirrored 已形成 experimental whole-shape 垂直切片:Facade 验
|
|||||||
| TSN-08/09 LinkSub 文档接线 | `PropertyValue` 支持 schema v1 TopoRef;重算 generation 自动迁移 LinkSub/草图外部几何,歧义/删除不强绑;schema v6 保存对象拓扑快照;Facade 候选替换事务验证当前同 kind persistentId,重建 DAG 并支持 Undo/Redo;属性/诊断 UI 可替换;Pocket 暴露 `UpToFace` 槽 | 非法瞬时索引/候选拒绝、DAG 边、稳定/歧义/删除迁移、跨 generation 回退解析、候选替换、Undo/Redo、保存加载和 67 项全套测试通过 | 尚未从 3D 拾取、预选或高亮候选;Pocket Up to face executor 仍保持结构化 unsupported,等待原生面历史/解析接线 |
|
| TSN-08/09 LinkSub 文档接线 | `PropertyValue` 支持 schema v1 TopoRef;重算 generation 自动迁移 LinkSub/草图外部几何,歧义/删除不强绑;schema v6 保存对象拓扑快照;Facade 候选替换事务验证当前同 kind persistentId,重建 DAG 并支持 Undo/Redo;属性/诊断 UI 可替换;Pocket 暴露 `UpToFace` 槽 | 非法瞬时索引/候选拒绝、DAG 边、稳定/歧义/删除迁移、跨 generation 回退解析、候选替换、Undo/Redo、保存加载和 67 项全套测试通过 | 尚未从 3D 拾取、预选或高亮候选;Pocket Up to face executor 仍保持结构化 unsupported,等待原生面历史/解析接线 |
|
||||||
| TSN-03/07 历史适配器回退 | Bitbybit 1.1.1 绑定仅有 `HasGenerated()`,没有逐 Shape 的 Generated/Modified/IsDeleted;`signature-fallback` 已进入真实重算 generation,对每个成功 Shape 保存快照/matches/history,采集失败释放临时 Shape并保留最近有效缓存 | 平移修改、新增/删除/重复面、跨对象竞争、generation 写回、采集失败隔离和持久化测试通过;全套 63 项测试通过 | 此 provider 不是 OCCT history;多输入 Boolean 当前仍缺逐输入原生映射,只有自构建绑定暴露逐子形状历史并通过黄金模型后,才能将 TSN-03/07 标为完成 |
|
| TSN-03/07 历史适配器回退 | Bitbybit 1.1.1 绑定仅有 `HasGenerated()`,没有逐 Shape 的 Generated/Modified/IsDeleted;`signature-fallback` 已进入真实重算 generation,对每个成功 Shape 保存快照/matches/history,采集失败释放临时 Shape并保留最近有效缓存 | 平移修改、新增/删除/重复面、跨对象竞争、generation 写回、采集失败隔离和持久化测试通过;全套 63 项测试通过 | 此 provider 不是 OCCT history;多输入 Boolean 当前仍缺逐输入原生映射,只有自构建绑定暴露逐子形状历史并通过黄金模型后,才能将 TSN-03/07 标为完成 |
|
||||||
|
|
||||||
|
原生历史 provider 适配器现已进入重算执行器:当 Geometry Worker 暴露 `topologyHistory()` 且 Base/Tool/Profile 有拓扑快照时,Facade 将真实输入/输出 Shape 和记录交给 `captureNativeTopologyHistory()`;否则保守回退。该适配层有独立单测,但当前 Bitbybit 1.1.1 provider 仍未提供真实记录,因此 TSN-03/07 仍不能标记完成。
|
||||||
|
|
||||||
后续按以下顺序连续开发:
|
后续按以下顺序连续开发:
|
||||||
|
|
||||||
1. `P4-06/DAG-08`:同步/异步重算已共享抑制传播和根因诊断语义,并支持定位、解除错误后的最小闭包重算与合法特征抑制;继续补齐多配置抑制、Body Tip 重定向、最近有效 Shape 显式回滚和性能计数器。Shape 缓存已进入 Facade,但尚未持久化。
|
1. `P4-06/DAG-08`:同步/异步重算已共享抑制传播和根因诊断语义,并支持定位、解除错误后的最小闭包重算与合法特征抑制;继续补齐多配置抑制、Body Tip 重定向、最近有效 Shape 显式回滚和性能计数器。Shape 缓存已进入 Facade,但尚未持久化。
|
||||||
|
|||||||
68
docs/freecad-reference-execution-roadmap.zh-CN.md
Normal file
68
docs/freecad-reference-execution-roadmap.zh-CN.md
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
# FreeCAD 1.1.1 全功能对标执行路线图
|
||||||
|
|
||||||
|
## 1. 基线与证据线
|
||||||
|
|
||||||
|
唯一语义基线是 FreeCAD `1.1.1`、提交 `0108fd4b4850cc46e625b60e53cea7a7bbe69f8d`。界面相似、OCCT 算法可调用或命令名称存在都不等于兼容。
|
||||||
|
|
||||||
|
本机参考分三条证据线:
|
||||||
|
|
||||||
|
| 证据线 | 当前状态 | 用途 | 不得推断 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| O1 headless oracle | `FreeCADCmd 1.1.1` 可用;`Part/Material/Measure` 可加载 | 几何、对象属性、错误和 FCStd 的确定性 CLI 回放 | Sketcher、PartDesign、TechDraw 等未构建模块的行为 |
|
||||||
|
| O2 完整桌面 oracle | 源码已锁定,完整 GUI/工作台产物尚未建立 | 命令状态、Task、选择、视图、工作台对象与 GUI 行为 | 不能用静态源码清册代替运行时行为 |
|
||||||
|
| O3 Web 对照 | Bitbybit 1.1.1、原生 OCCT 历史 provider 和 Facade 已有基础门禁 | FreeCAD↔Bitbybit↔Web 三方差分、浏览器 Worker/OPFS/交互 | Node smoke 不能代替真实浏览器证据 |
|
||||||
|
|
||||||
|
每项功能必须同时记录:FreeCAD 输入与状态、Web 输入与状态、输出、错误、持久化、资源行为、差异原因和兼容等级。
|
||||||
|
|
||||||
|
## 2. 连续执行顺序
|
||||||
|
|
||||||
|
| 批次 | 任务范围 | 主要交付物 | 退出门禁 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| R0 参考环境 | `BASE-01..04`、`INV-01..06` | 完整桌面 oracle 构建清单;34 模块、命令、TypeId、Property、格式和错误清册 | 锁定提交可重建;清册无未登记入口 |
|
||||||
|
| R1 几何内核 | `BYB-03..08`、`TSN-03..10` | 可复现 Bitbybit/OCCT 构建;浏览器原生历史;Shape 生命周期;三方黄金差分 | Boolean/PartDesign 历史无静默错绑;长会话无泄漏 |
|
||||||
|
| R2 Sketcher | `SK-03..12` | planegcs WASM;完整几何/约束/DOF/冲突;外部几何;编辑回放 | FreeCAD 草图成功/失败/冗余行为黄金通过 |
|
||||||
|
| R3 参数化核心 | `CORE-01..12`、`PART-*`、`PD-*` | Document/Property/DAG/Body/Tip/Support;Part 与 PartDesign 全特征 | 编辑→重算→Undo→保存→加载闭环通过 |
|
||||||
|
| R4 文件互操作 | `FC-04..11`、`PKG-*` | Document/GuiDocument、BRep/Shape、Expression/Link/TopoRef、未知字段保留 | FreeCAD→Web→FreeCAD 双向差异可解释且无静默丢失 |
|
||||||
|
| R5 核心工作台 | `TD-*`、`SS-*`、`DRAFT-*`、`PLOT-*` | TechDraw、Spreadsheet、Draft、Plot 的对象、命令、任务和格式 | 每命令至少成功/失败/round-trip fixture |
|
||||||
|
| R6 工程工作台 | `ASM-*`、`BIM-*`、`MESH-*`、`SURF-*` | 装配、BIM、网格、曲面全链路 | 求解/几何/IFC/网格黄金和大模型预算通过 |
|
||||||
|
| R7 分析制造 | `FEM-*`、`CAM-*`、`ROBOT-*`、`INSP-*` | 求解器/后处理/刀路/仿真/检测报告 | 求解或明确远端能力;G-code/结果文件黄金通过 |
|
||||||
|
| R8 平台长尾 | `PTS-*`、`RE-*`、`OSC-*`、`IDF-*`、`JT-*`、`MAT-*`、`APP-*`、`SCRIPT-*` | 数据模块、插件、宏/API、桌面通用能力的实现或显式代理 | 无未分类入口;安全和权限模型通过 |
|
||||||
|
| R9 发布 | `QA-*`、`REL-*`、F0-F9/G0-G9 | 跨浏览器、性能、故障注入、安全、SBOM、迁移和发布 runbook | 所有阻断项关闭后才允许声明完整对标 |
|
||||||
|
|
||||||
|
## 3. 当前立即执行队列
|
||||||
|
|
||||||
|
1. `ORA-01`:建立完整 FreeCAD 1.1.1 桌面/工作台 oracle 构建配置;探针必须区分编译缺失、导入失败和 GUI-only。
|
||||||
|
2. `INV-02`:从 O1/O2 生成模块→命令→对象→属性→格式清册,并与 Web manifest 做机器差分。
|
||||||
|
3. `BYB-06/07`:在真实浏览器实例化原生历史 Worker,执行 Bitbybit STEP→原生 OCCT→FreeCAD 三方 Boolean 黄金和内存释放测试。
|
||||||
|
4. `TSN-04/07/10`:补曲面/曲线/邻接签名,扩展 Boolean、Pattern 和 PartDesign 的逐输入历史,建立参数变异套件。
|
||||||
|
5. `SK-03/06/07`:编译 planegcs WASM,按 FreeCAD 草图约束分类逐组替换基础 TypeScript solver。
|
||||||
|
6. `CORE-09..12`、`PD-*`:完成 Support/AttachmentOffset、Body 单实体、Tip 重定向、局部特征和编辑历史。
|
||||||
|
7. `FC-04..10`:在现有安全 ZIP 和属性投影上增加 GuiDocument、BRep/Shape、Expression/Link/TopoRef 与未知 XML/资源保留。
|
||||||
|
8. 按 R5→R8 顺序逐工作台交付,禁止只启用 UI 或把未实现命令标记为成功。
|
||||||
|
9. 每个增量持续执行 R9 门禁;能力等级只能依据本机 FreeCAD 黄金证据提升。
|
||||||
|
|
||||||
|
## 4. 单项完成定义
|
||||||
|
|
||||||
|
一个命令、对象或格式只有同时满足以下条件才可从 `experimental` 提升:
|
||||||
|
|
||||||
|
1. 锁定 FreeCAD 的成功、失败和边界 fixture 已保存。
|
||||||
|
2. Facade 合同、取消、过期结果、Undo/Redo 和诊断均有测试。
|
||||||
|
3. 几何/求解结果在声明公差内,拓扑引用不使用瞬时索引。
|
||||||
|
4. 保存、加载、版本迁移和相关格式 round-trip 无静默丢失。
|
||||||
|
5. Chrome、Firefox、Safari 目标版本的 Worker/OPFS/交互证据通过。
|
||||||
|
6. 内存、性能、安全、许可证和能力矩阵已更新。
|
||||||
|
|
||||||
|
`compatible` 允许已记录且可解释的浏览器差异;`exact` 要求锁定版本的声明语义无差异。无法在浏览器安全实现的桌面能力必须标为受限代理,不能隐藏。
|
||||||
|
|
||||||
|
## 5. 每批统一命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./npmw run probe:freecad-reference
|
||||||
|
./npmw run test:golden:freecad
|
||||||
|
./npmw run test:occt-history
|
||||||
|
./npmw run verify
|
||||||
|
git diff --check
|
||||||
|
jq empty config/compatibility-matrix.json
|
||||||
|
```
|
||||||
|
|
||||||
|
O2 完整桌面 oracle 建立后,所有相关探针和黄金命令必须显式设置 `FREECAD_CMD`,防止回退到仅含 Part 的缓存构建。
|
||||||
@@ -1364,7 +1364,7 @@ SQLite 是运行时的主存储,不要求项目包直接暴露数据库内部
|
|||||||
| CAD Workspace zones | FreeCAD 风格的菜单、全局工具栏、Workbench Navigation Bar、Document Tabs、Combo View、Viewport、Task Dock、Function Rail、Report View | Dock 状态、命令 enabled/disabled、任务生命周期由 Facade 事件驱动 |
|
| CAD Workspace zones | FreeCAD 风格的菜单、全局工具栏、Workbench Navigation Bar、Document Tabs、Combo View、Viewport、Task Dock、Function Rail、Report View | Dock 状态、命令 enabled/disabled、任务生命周期由 Facade 事件驱动 |
|
||||||
| Model/Property/Task mock | 按 Document/Object/Body/Feature、Data/View、Apply/OK/Cancel 和警告状态组织 | 接入真实对象投影、Property metadata、TaskSession 和 Selection service |
|
| Model/Property/Task mock | 按 Document/Object/Body/Feature、Data/View、Apply/OK/Cancel 和警告状态组织 | 接入真实对象投影、Property metadata、TaskSession 和 Selection service |
|
||||||
| `src/styles.css` | 桌面 300px Combo View、360px Task Dock、44px 命令轨道,并有窄屏内部滚动布局 | 对照锁定 FreeCAD 版本做视觉回归,不改变业务语义 |
|
| `src/styles.css` | 桌面 300px Combo View、360px Task Dock、44px 命令轨道,并有窄屏内部滚动布局 | 对照锁定 FreeCAD 版本做视觉回归,不改变业务语义 |
|
||||||
| 构建验收 | `npm run build` 通过;核心路由返回 200;已验收 1440x900 和 390x844 截图 | 增加 Playwright/视觉回归与键盘无障碍自动化 |
|
| 构建验收 | `./npmw run build` 通过;核心路由返回 200;已验收 1440x900 和 390x844 截图 | 增加 Playwright/视觉回归与键盘无障碍自动化 |
|
||||||
|
|
||||||
页面阶段的 viewport 仍是可交互的静态几何示意,不能宣称已完成 Three.js、FreeCAD 几何或存储能力。业务接入必须从 manifest 和冻结 DOM 结构开始,并通过 BitBybit 唯一入口完成。
|
页面阶段的 viewport 仍是可交互的静态几何示意,不能宣称已完成 Three.js、FreeCAD 几何或存储能力。业务接入必须从 manifest 和冻结 DOM 结构开始,并通过 BitBybit 唯一入口完成。
|
||||||
|
|
||||||
@@ -1554,7 +1554,7 @@ P0 基线/治理
|
|||||||
6. `P4-01/P4-04`:验证最小 Document/Object 投影、特征追加、事务版本和撤销/重做边界。
|
6. `P4-01/P4-04`:验证最小 Document/Object 投影、特征追加、事务版本和撤销/重做边界。
|
||||||
7. `P8-01`:为新建文档、工作台切换、选择 Pad、打开 Task Dock、特征提交和属性编辑建立第一批自动化场景。
|
7. `P8-01`:为新建文档、工作台切换、选择 Pad、打开 Task Dock、特征提交和属性编辑建立第一批自动化场景。
|
||||||
|
|
||||||
迭代退出条件:`npm run build`、类型检查、Facade-only 架构检查和桌面/移动页面截图全部通过;提交信息必须包含任务 ID,例如 `P1-01: define facade contract`。
|
迭代退出条件:`./npmw run build`、类型检查、Facade-only 架构检查和桌面/移动页面截图全部通过;提交信息必须包含任务 ID,例如 `P1-01: define facade contract`。
|
||||||
|
|
||||||
### 16.7 任务执行和变更控制
|
### 16.7 任务执行和变更控制
|
||||||
|
|
||||||
@@ -1611,7 +1611,7 @@ P0 基线/治理
|
|||||||
| P5-02 网格增量接入 | `IN PROGRESS` | Three Adapter 可用 `BufferGeometry` 接收 Facade `MeshAsset`,替换时释放旧 GPU geometry;视口优先使用重计算缓存的对象 Shape,失败时保留最近有效结果,无缓存时才创建并释放 Pad/Pocket 临时预览链;对象级增量缓存与选择映射尚未实现 |
|
| P5-02 网格增量接入 | `IN PROGRESS` | Three Adapter 可用 `BufferGeometry` 接收 Facade `MeshAsset`,替换时释放旧 GPU geometry;视口优先使用重计算缓存的对象 Shape,失败时保留最近有效结果,无缓存时才创建并释放 Pad/Pocket 临时预览链;对象级增量缓存与选择映射尚未实现 |
|
||||||
| P8-01 第一批自动化场景 | `IN PROGRESS` | `tests/facade.test.ts` 已覆盖 67 个场景,包括 Part/PartDesign Mirrored/MultiTransform/Pattern/Hole、TopoRef generation/候选替换、Sketch provider、几何重算、诊断修复、FCStd 安全、schema migration、1000 次写队列、检查点和资源治理;E2E/黄金几何待补齐 |
|
| P8-01 第一批自动化场景 | `IN PROGRESS` | `tests/facade.test.ts` 已覆盖 67 个场景,包括 Part/PartDesign Mirrored/MultiTransform/Pattern/Hole、TopoRef generation/候选替换、Sketch provider、几何重算、诊断修复、FCStd 安全、schema migration、1000 次写队列、检查点和资源治理;E2E/黄金几何待补齐 |
|
||||||
|
|
||||||
本迭代验证命令:`npm run check:facade-boundary`、`npm run test:facade`、`npm run build`。构建产物将 Three.js 拆为独立 chunk,避免把全部渲染库重复打入应用主 chunk。当前 npm registry 的 `three` 最新版本为 `0.185.1`,已在 `package.json` 和运行时基线中锁定。下一迭代继续完成 P0-01/P0-02 的精确锁定、P2-01/P2-02 的 SQLite/OPFS schema 与 Worker 单写者实验,以及 P3-01 的 FreeCAD/OCCT WASM 构建验证。
|
本迭代验证命令:`./npmw run check:facade-boundary`、`./npmw run test:facade`、`./npmw run build`。构建产物将 Three.js 拆为独立 chunk,避免把全部渲染库重复打入应用主 chunk。当前 npm registry 的 `three` 最新版本为 `0.185.1`,已在 `package.json` 和运行时基线中锁定。下一迭代继续完成 P0-01/P0-02 的精确锁定、P2-01/P2-02 的 SQLite/OPFS schema 与 Worker 单写者实验,以及 P3-01 的 FreeCAD/OCCT WASM 构建验证。
|
||||||
|
|
||||||
### 16.10 P2 持久化/视口验证记录
|
### 16.10 P2 持久化/视口验证记录
|
||||||
|
|
||||||
@@ -1738,3 +1738,13 @@ Sketcher 当前边界:领域模型可往返保存点、线、圆、弧、椭
|
|||||||
本项目原先以系统 Node `v20.19.2` 执行 npm,而依赖要求 Node 22,因而出现 `EBADENGINE`。现已增加项目内、SHA-256 校验的 Node `22.23.2`/npm `10.9.8` 运行时;日常命令统一使用 `./npmw` 或 `./nodew`,系统解释器不被修改。详见 [项目运行时基线](project-runtime.zh-CN.md)。
|
本项目原先以系统 Node `v20.19.2` 执行 npm,而依赖要求 Node 22,因而出现 `EBADENGINE`。现已增加项目内、SHA-256 校验的 Node `22.23.2`/npm `10.9.8` 运行时;日常命令统一使用 `./npmw` 或 `./nodew`,系统解释器不被修改。详见 [项目运行时基线](project-runtime.zh-CN.md)。
|
||||||
|
|
||||||
“完整 FreeCAD 对标”不等于已有页面或少量 OCCT 特征。稳定子形状命名、Sketcher 求解器、Expression/单位、依赖 DAG/重计算、剩余工作台和 FCStd/交换格式兼容已拆成独立的可验收任务包、阶段门和黄金测试,详见 [完整 FreeCAD 对标实施方案](freecad-full-parity-plan.zh-CN.md)。FreeCAD `1.1.1` 标签已核对并记录精确提交;构建参数和 WASM 端口仍须通过该方案的 F0 基线门后才能宣称兼容。
|
“完整 FreeCAD 对标”不等于已有页面或少量 OCCT 特征。稳定子形状命名、Sketcher 求解器、Expression/单位、依赖 DAG/重计算、剩余工作台和 FCStd/交换格式兼容已拆成独立的可验收任务包、阶段门和黄金测试,详见 [完整 FreeCAD 对标实施方案](freecad-full-parity-plan.zh-CN.md)。FreeCAD `1.1.1` 标签已核对并记录精确提交;构建参数和 WASM 端口仍须通过该方案的 F0 基线门后才能宣称兼容。
|
||||||
|
|
||||||
|
### 16.18 全功能范围与主任务账本
|
||||||
|
|
||||||
|
全功能范围已经按锁定源码的 34 个 `src/Mod` 目录重新审计,并补入 App/Base/Gui 的文档、事务、Property、选择、偏好、多文档、Python 控制台、宏、插件、剪贴板和打印行为。逐模块任务、依赖波次和退出条件见 [FreeCAD 1.1.1 全功能对标主任务账本](freecad-complete-parity-backlog.zh-CN.md)。
|
||||||
|
|
||||||
|
主任务账本把原来笼统的“其他模块”拆分为数据/交换、应用/扩展、Robot/Plot、脚本/桌面通用和开发/测试模块。任何能力即使由于浏览器安全或外部二进制无法端口,也必须登记为带诊断和数据保留策略的 `unsupported`,不能从清册中消失。
|
||||||
|
|
||||||
|
执行顺序固定为:范围/oracle → 平台内核 → OCCT 原生历史/TopoRef → planegcs Sketcher → Part/PartDesign → FCStd 双向往返 → TechDraw/Spreadsheet/Draft/Plot → Assembly/BIM/Mesh/Surface → FEM/CAM/Robot/Inspection/数据与应用模块 → 发布门禁。所有项目 Node/npm 命令只允许通过 `./npmw` 执行。
|
||||||
|
|
||||||
|
几何实现遵循 Bitbybit 优先策略:先复用其 OCCT Worker、建模、IO、三角化和缓存能力,再对缺口扩展锁定的 Bitbybit/OCCT 源码绑定并用 Emscripten 构建自有 WASM provider。自构建代码不得绕过 `BitBybitWebCadFacade`,并须通过来源/补丁/SBOM/hash、句柄生命周期和 Bitbybit↔自构建 provider↔FreeCAD 三方黄金差分门禁。对应任务为主账本 `BYB-01..08`。
|
||||||
|
|||||||
28
native/occt-history/CMakeLists.txt
Normal file
28
native/occt-history/CMakeLists.txt
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.20)
|
||||||
|
project(bitbybit_occt_history LANGUAGES CXX)
|
||||||
|
|
||||||
|
set(CMAKE_CXX_STANDARD 17)
|
||||||
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
|
||||||
|
if(NOT OCCT_DIR)
|
||||||
|
message(FATAL_ERROR "OCCT_DIR must point to an Emscripten OCCT build containing OpenCASCADEConfig.cmake")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
find_package(OpenCASCADE REQUIRED CONFIG PATHS "${OCCT_DIR}" NO_DEFAULT_PATH)
|
||||||
|
|
||||||
|
add_executable(bitbybit_occt_history occt_history.cpp)
|
||||||
|
target_include_directories(bitbybit_occt_history PRIVATE "${OCCT_DIR}/include/opencascade")
|
||||||
|
target_link_libraries(bitbybit_occt_history PRIVATE TKDESTEP TKXSBase TKDE TKBO TKBRep TKTopAlgo TKPrim TKShHealing TKGeomAlgo TKGeomBase TKG3d TKG2d TKMath TKernel)
|
||||||
|
target_compile_options(bitbybit_occt_history PRIVATE -fexceptions -O2)
|
||||||
|
target_link_options(bitbybit_occt_history PRIVATE
|
||||||
|
--bind
|
||||||
|
-fexceptions
|
||||||
|
-sMODULARIZE=1
|
||||||
|
-sEXPORT_ES6=1
|
||||||
|
-sENVIRONMENT=web,worker,node
|
||||||
|
-sALLOW_MEMORY_GROWTH=1
|
||||||
|
-sINITIAL_MEMORY=134217728
|
||||||
|
-sMAXIMUM_MEMORY=2147483648
|
||||||
|
-sNO_EXIT_RUNTIME=1
|
||||||
|
)
|
||||||
|
set_target_properties(bitbybit_occt_history PROPERTIES OUTPUT_NAME "bitbybit-occt-history")
|
||||||
34
native/occt-history/README.md
Normal file
34
native/occt-history/README.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# Native OCCT history provider
|
||||||
|
|
||||||
|
This provider is compiled from the cached OCCT checkout selected by
|
||||||
|
`OCCT_SOURCE_DIR` and exposes the native Boolean history API through Emscripten
|
||||||
|
Embind. The provider keeps `TopoDS_Shape` handles and history queries in one
|
||||||
|
WASM instance. A handle from `@bitbybit-dev/occt` must never be passed directly
|
||||||
|
to this module because WebAssembly linear memories are independent.
|
||||||
|
|
||||||
|
Build and verify it with the project wrapper:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./npmw run build:occt-history
|
||||||
|
./npmw run test:occt-history
|
||||||
|
```
|
||||||
|
|
||||||
|
The build also publishes the JS/WASM pair to
|
||||||
|
`public/native/occt-history/`, which is the default URL used by
|
||||||
|
`NativeOcctHistoryWorkerProvider`. The generated artifacts remain ignored; a
|
||||||
|
clean checkout must run the build after restoring the pinned OCCT source and
|
||||||
|
Emscripten toolchain.
|
||||||
|
|
||||||
|
The exported `booleanHistory(object, tool, operation)` function supports
|
||||||
|
`fuse`, `cut`, and `common`. It returns the native result shape, per-input
|
||||||
|
`modified`/`generated`/`deleted` records, and capability flags. The provider also
|
||||||
|
exports `shapeToStep(shape)` and `booleanHistoryFromStep(objectStep, toolStep,
|
||||||
|
operation)`. The latter is the cross-WASM transport: Bitbybit exports STEP text,
|
||||||
|
the native provider reads it with `STEPControl_Reader`, returns the Boolean
|
||||||
|
result again as `resultStep`, and no linear-memory pointer crosses the boundary.
|
||||||
|
|
||||||
|
The TypeScript boundary mapper is `mapNativeOcctHistoryRecords`; the reusable
|
||||||
|
`createNativeOcctStepHistoryBridge` exports Bitbybit ShapeHandles to STEP and
|
||||||
|
converts provider source names into document object IDs before calling
|
||||||
|
`captureNativeTopologyHistory`. The direct handle API remains for calls that
|
||||||
|
stay inside this single native WASM instance.
|
||||||
79
native/occt-history/build.sh
Executable file
79
native/occt-history/build.sh
Executable file
@@ -0,0 +1,79 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
OCCT_SOURCE_DIR="${OCCT_SOURCE_DIR:-/home/mes123456/working/WebCam/occt}"
|
||||||
|
OCCT_BUILD_DIR="${OCCT_BUILD_DIR:-${ROOT_DIR}/.cache/bitbybit/occt-history-build}"
|
||||||
|
DIST_DIR="${OCCT_HISTORY_DIST_DIR:-${ROOT_DIR}/native/occt-history/dist}"
|
||||||
|
PUBLIC_DIR="${OCCT_HISTORY_PUBLIC_DIR:-${ROOT_DIR}/public/native/occt-history}"
|
||||||
|
JOBS="${JOBS:-$(nproc)}"
|
||||||
|
|
||||||
|
if [[ ! -f "${OCCT_SOURCE_DIR}/CMakeLists.txt" ]]; then
|
||||||
|
echo "OCCT source checkout not found: ${OCCT_SOURCE_DIR}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if ! command -v emcmake >/dev/null || ! command -v em++ >/dev/null; then
|
||||||
|
echo "Emscripten emcmake/em++ are required." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
emcmake cmake -S "${OCCT_SOURCE_DIR}" -B "${OCCT_BUILD_DIR}" -G Ninja \
|
||||||
|
-DCMAKE_BUILD_TYPE=Release \
|
||||||
|
-DCMAKE_MAKE_PROGRAM="$(command -v ninja)" \
|
||||||
|
-DCMAKE_CXX_FLAGS="-fexceptions" \
|
||||||
|
-DCMAKE_CXX_FLAGS_RELEASE="-O0 -DNDEBUG -fexceptions" \
|
||||||
|
-DBUILD_RELEASE_DISABLE_EXCEPTIONS=OFF \
|
||||||
|
-DBUILD_LIBRARY_TYPE=Static \
|
||||||
|
-DBUILD_MODULE_FoundationClasses=ON \
|
||||||
|
-DBUILD_MODULE_ModelingData=ON \
|
||||||
|
-DBUILD_MODULE_ModelingAlgorithms=OFF \
|
||||||
|
-DBUILD_MODULE_Visualization=OFF \
|
||||||
|
-DBUILD_MODULE_ApplicationFramework=OFF \
|
||||||
|
-DBUILD_MODULE_DataExchange=ON \
|
||||||
|
-DBUILD_MODULE_Draw=OFF \
|
||||||
|
-DBUILD_ADDITIONAL_TOOLKITS="TKBO;TKDESTEP" \
|
||||||
|
-DBUILD_DOC_Overview=OFF \
|
||||||
|
-DBUILD_Inspector=OFF \
|
||||||
|
-DBUILD_SAMPLES_QT=OFF \
|
||||||
|
-DBUILD_USE_PCH=OFF \
|
||||||
|
-DUSE_FREETYPE=OFF \
|
||||||
|
-DUSE_TK=OFF \
|
||||||
|
-DUSE_TBB=OFF \
|
||||||
|
-DUSE_RAPIDJSON=OFF
|
||||||
|
|
||||||
|
cmake --build "${OCCT_BUILD_DIR}" --target \
|
||||||
|
TKernel TKMath TKG2d TKG3d TKGeomBase TKGeomAlgo TKShHealing TKPrim TKTopAlgo TKBRep TKBO \
|
||||||
|
TKDE TKXSBase TKDESTEP \
|
||||||
|
--parallel "${JOBS}"
|
||||||
|
mkdir -p "${DIST_DIR}"
|
||||||
|
|
||||||
|
em++ "${ROOT_DIR}/native/occt-history/occt_history.cpp" \
|
||||||
|
-I"${OCCT_BUILD_DIR}/include/opencascade" \
|
||||||
|
-I"${OCCT_SOURCE_DIR}/src" \
|
||||||
|
-std=c++17 -O2 -fexceptions --bind \
|
||||||
|
-Wl,--start-group \
|
||||||
|
"${OCCT_BUILD_DIR}/lin32/clang/lib/libTKBO.a" \
|
||||||
|
"${OCCT_BUILD_DIR}/lin32/clang/lib/libTKDESTEP.a" \
|
||||||
|
"${OCCT_BUILD_DIR}/lin32/clang/lib/libTKXSBase.a" \
|
||||||
|
"${OCCT_BUILD_DIR}/lin32/clang/lib/libTKDE.a" \
|
||||||
|
"${OCCT_BUILD_DIR}/lin32/clang/lib/libTKBRep.a" \
|
||||||
|
"${OCCT_BUILD_DIR}/lin32/clang/lib/libTKTopAlgo.a" \
|
||||||
|
"${OCCT_BUILD_DIR}/lin32/clang/lib/libTKPrim.a" \
|
||||||
|
"${OCCT_BUILD_DIR}/lin32/clang/lib/libTKShHealing.a" \
|
||||||
|
"${OCCT_BUILD_DIR}/lin32/clang/lib/libTKGeomAlgo.a" \
|
||||||
|
"${OCCT_BUILD_DIR}/lin32/clang/lib/libTKGeomBase.a" \
|
||||||
|
"${OCCT_BUILD_DIR}/lin32/clang/lib/libTKG3d.a" \
|
||||||
|
"${OCCT_BUILD_DIR}/lin32/clang/lib/libTKG2d.a" \
|
||||||
|
"${OCCT_BUILD_DIR}/lin32/clang/lib/libTKMath.a" \
|
||||||
|
"${OCCT_BUILD_DIR}/lin32/clang/lib/libTKernel.a" \
|
||||||
|
-Wl,--end-group \
|
||||||
|
-sMODULARIZE=1 -sEXPORT_ES6=1 -sENVIRONMENT=web,worker,node \
|
||||||
|
-sALLOW_MEMORY_GROWTH=1 -sINITIAL_MEMORY=134217728 -sMAXIMUM_MEMORY=2147483648 \
|
||||||
|
-sNO_EXIT_RUNTIME=1 \
|
||||||
|
-o "${DIST_DIR}/bitbybit-occt-history.js"
|
||||||
|
|
||||||
|
cp "${ROOT_DIR}/native/occt-history/package.json" "${DIST_DIR}/package.json"
|
||||||
|
mkdir -p "${PUBLIC_DIR}"
|
||||||
|
cp "${DIST_DIR}/bitbybit-occt-history.js" "${PUBLIC_DIR}/bitbybit-occt-history.js"
|
||||||
|
cp "${DIST_DIR}/bitbybit-occt-history.wasm" "${PUBLIC_DIR}/bitbybit-occt-history.wasm"
|
||||||
|
sha256sum "${DIST_DIR}/bitbybit-occt-history.js" "${DIST_DIR}/bitbybit-occt-history.wasm"
|
||||||
247
native/occt-history/occt_history.cpp
Normal file
247
native/occt-history/occt_history.cpp
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
#include <BRepAlgoAPI_Common.hxx>
|
||||||
|
#include <BRepAlgoAPI_Cut.hxx>
|
||||||
|
#include <BRepAlgoAPI_Fuse.hxx>
|
||||||
|
#include <BRepPrimAPI_MakeBox.hxx>
|
||||||
|
#include <Standard_Version.hxx>
|
||||||
|
#include <STEPControl_Reader.hxx>
|
||||||
|
#include <STEPControl_Writer.hxx>
|
||||||
|
#include <STEPControl_StepModelType.hxx>
|
||||||
|
#include <TopAbs_ShapeEnum.hxx>
|
||||||
|
#include <TopExp_Explorer.hxx>
|
||||||
|
#include <TopoDS_Shape.hxx>
|
||||||
|
|
||||||
|
#include <emscripten/bind.h>
|
||||||
|
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <sstream>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
struct HistoryRecord
|
||||||
|
{
|
||||||
|
std::string relation;
|
||||||
|
std::string kind;
|
||||||
|
int sourceIndex = -1;
|
||||||
|
int resultIndex = -1;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::string kindName(const TopAbs_ShapeEnum theKind)
|
||||||
|
{
|
||||||
|
switch (theKind)
|
||||||
|
{
|
||||||
|
case TopAbs_VERTEX: return "vertex";
|
||||||
|
case TopAbs_EDGE: return "edge";
|
||||||
|
case TopAbs_WIRE: return "wire";
|
||||||
|
case TopAbs_FACE: return "face";
|
||||||
|
case TopAbs_SHELL: return "shell";
|
||||||
|
case TopAbs_SOLID: return "solid";
|
||||||
|
case TopAbs_COMPSOLID: return "compsolid";
|
||||||
|
case TopAbs_COMPOUND: return "compound";
|
||||||
|
default: return "shape";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<TopoDS_Shape> subShapes(const TopoDS_Shape& theShape, const TopAbs_ShapeEnum theKind)
|
||||||
|
{
|
||||||
|
std::vector<TopoDS_Shape> aShapes;
|
||||||
|
for (TopExp_Explorer anExplorer(theShape, theKind); anExplorer.More(); anExplorer.Next())
|
||||||
|
{
|
||||||
|
aShapes.push_back(anExplorer.Current());
|
||||||
|
}
|
||||||
|
return aShapes;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string writeStep(const TopoDS_Shape& theShape);
|
||||||
|
|
||||||
|
int resultIndex(const TopoDS_Shape& theShape,
|
||||||
|
const TopoDS_Shape& theResult,
|
||||||
|
const TopAbs_ShapeEnum theKind)
|
||||||
|
{
|
||||||
|
int anIndex = 0;
|
||||||
|
for (TopExp_Explorer anExplorer(theResult, theKind); anExplorer.More(); anExplorer.Next(), ++anIndex)
|
||||||
|
{
|
||||||
|
if (anExplorer.Current().IsSame(theShape))
|
||||||
|
{
|
||||||
|
return anIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename TAlgorithm>
|
||||||
|
emscripten::val runBoolean(TAlgorithm& theAlgorithm,
|
||||||
|
const TopoDS_Shape& theObject,
|
||||||
|
const TopoDS_Shape& theTool)
|
||||||
|
{
|
||||||
|
theAlgorithm.Build();
|
||||||
|
if (!theAlgorithm.IsDone() || theAlgorithm.HasErrors())
|
||||||
|
{
|
||||||
|
throw std::runtime_error("OCCT boolean operation failed while collecting native history.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const TopoDS_Shape aResult = theAlgorithm.Shape();
|
||||||
|
emscripten::val aRecords = emscripten::val::array();
|
||||||
|
const TopAbs_ShapeEnum aKinds[] = {TopAbs_VERTEX, TopAbs_EDGE, TopAbs_FACE};
|
||||||
|
const TopoDS_Shape aSources[] = {theObject, theTool};
|
||||||
|
const char* aSourceNames[] = {"object", "tool"};
|
||||||
|
unsigned int aRecordIndex = 0;
|
||||||
|
|
||||||
|
for (int aSource = 0; aSource < 2; ++aSource)
|
||||||
|
{
|
||||||
|
for (const TopAbs_ShapeEnum aKind : aKinds)
|
||||||
|
{
|
||||||
|
const auto aSubShapes = subShapes(aSources[aSource], aKind);
|
||||||
|
for (std::size_t aSourceIndex = 0; aSourceIndex < aSubShapes.size(); ++aSourceIndex)
|
||||||
|
{
|
||||||
|
const TopoDS_Shape& anInput = aSubShapes[aSourceIndex];
|
||||||
|
const auto& aModified = theAlgorithm.Modified(anInput);
|
||||||
|
for (NCollection_List<TopoDS_Shape>::Iterator anIterator(aModified); anIterator.More(); anIterator.Next())
|
||||||
|
{
|
||||||
|
const int anOutputIndex = resultIndex(anIterator.Value(), aResult, aKind);
|
||||||
|
if (anOutputIndex < 0) continue;
|
||||||
|
emscripten::val aRecord = emscripten::val::object();
|
||||||
|
aRecord.set("relation", "modified");
|
||||||
|
aRecord.set("source", aSourceNames[aSource]);
|
||||||
|
aRecord.set("kind", kindName(aKind));
|
||||||
|
aRecord.set("sourceIndex", static_cast<int>(aSourceIndex));
|
||||||
|
aRecord.set("resultIndex", anOutputIndex);
|
||||||
|
aRecords.set(aRecordIndex++, aRecord);
|
||||||
|
}
|
||||||
|
const auto& aGenerated = theAlgorithm.Generated(anInput);
|
||||||
|
for (NCollection_List<TopoDS_Shape>::Iterator anIterator(aGenerated); anIterator.More(); anIterator.Next())
|
||||||
|
{
|
||||||
|
const int anOutputIndex = resultIndex(anIterator.Value(), aResult, aKind);
|
||||||
|
if (anOutputIndex < 0) continue;
|
||||||
|
emscripten::val aRecord = emscripten::val::object();
|
||||||
|
aRecord.set("relation", "generated");
|
||||||
|
aRecord.set("source", aSourceNames[aSource]);
|
||||||
|
aRecord.set("kind", kindName(aKind));
|
||||||
|
aRecord.set("sourceIndex", static_cast<int>(aSourceIndex));
|
||||||
|
aRecord.set("resultIndex", anOutputIndex);
|
||||||
|
aRecords.set(aRecordIndex++, aRecord);
|
||||||
|
}
|
||||||
|
if (theAlgorithm.IsDeleted(anInput))
|
||||||
|
{
|
||||||
|
emscripten::val aRecord = emscripten::val::object();
|
||||||
|
aRecord.set("relation", "deleted");
|
||||||
|
aRecord.set("source", aSourceNames[aSource]);
|
||||||
|
aRecord.set("kind", kindName(aKind));
|
||||||
|
aRecord.set("sourceIndex", static_cast<int>(aSourceIndex));
|
||||||
|
aRecord.set("resultIndex", -1);
|
||||||
|
aRecords.set(aRecordIndex++, aRecord);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
emscripten::val aResponse = emscripten::val::object();
|
||||||
|
aResponse.set("provider", "occt-native");
|
||||||
|
aResponse.set("occtVersion", OCC_VERSION_COMPLETE);
|
||||||
|
aResponse.set("result", aResult);
|
||||||
|
aResponse.set("resultStep", writeStep(aResult));
|
||||||
|
aResponse.set("records", aRecords);
|
||||||
|
aResponse.set("hasModified", theAlgorithm.HasModified());
|
||||||
|
aResponse.set("hasGenerated", theAlgorithm.HasGenerated());
|
||||||
|
aResponse.set("hasDeleted", theAlgorithm.HasDeleted());
|
||||||
|
return aResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
TopoDS_Shape makeBox(const double theX, const double theY, const double theZ)
|
||||||
|
{
|
||||||
|
if (theX <= 0.0 || theY <= 0.0 || theZ <= 0.0)
|
||||||
|
{
|
||||||
|
throw std::invalid_argument("Box dimensions must be positive.");
|
||||||
|
}
|
||||||
|
return BRepPrimAPI_MakeBox(theX, theY, theZ).Shape();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string occtVersion()
|
||||||
|
{
|
||||||
|
return OCC_VERSION_COMPLETE;
|
||||||
|
}
|
||||||
|
|
||||||
|
emscripten::val booleanHistory(const TopoDS_Shape& theObject,
|
||||||
|
const TopoDS_Shape& theTool,
|
||||||
|
const std::string& theOperation)
|
||||||
|
{
|
||||||
|
if (theOperation == "fuse")
|
||||||
|
{
|
||||||
|
BRepAlgoAPI_Fuse anAlgorithm(theObject, theTool);
|
||||||
|
return runBoolean(anAlgorithm, theObject, theTool);
|
||||||
|
}
|
||||||
|
if (theOperation == "cut")
|
||||||
|
{
|
||||||
|
BRepAlgoAPI_Cut anAlgorithm(theObject, theTool);
|
||||||
|
return runBoolean(anAlgorithm, theObject, theTool);
|
||||||
|
}
|
||||||
|
if (theOperation == "common")
|
||||||
|
{
|
||||||
|
BRepAlgoAPI_Common anAlgorithm(theObject, theTool);
|
||||||
|
return runBoolean(anAlgorithm, theObject, theTool);
|
||||||
|
}
|
||||||
|
throw std::invalid_argument("Unsupported Boolean operation. Expected fuse, cut or common.");
|
||||||
|
}
|
||||||
|
|
||||||
|
TopoDS_Shape readStep(const std::string& theText)
|
||||||
|
{
|
||||||
|
if (theText.empty())
|
||||||
|
{
|
||||||
|
throw std::invalid_argument("STEP text must not be empty.");
|
||||||
|
}
|
||||||
|
std::istringstream aStream(theText);
|
||||||
|
STEPControl_Reader aReader;
|
||||||
|
const IFSelect_ReturnStatus aStatus = aReader.ReadStream("bitbybit.step", aStream);
|
||||||
|
if (aStatus != IFSelect_RetDone || aReader.NbRootsForTransfer() <= 0 || !aReader.TransferRoots())
|
||||||
|
{
|
||||||
|
throw std::runtime_error("OCCT could not read and transfer the supplied STEP text.");
|
||||||
|
}
|
||||||
|
const TopoDS_Shape aShape = aReader.OneShape();
|
||||||
|
if (aShape.IsNull())
|
||||||
|
{
|
||||||
|
throw std::runtime_error("OCCT STEP transfer produced a null shape.");
|
||||||
|
}
|
||||||
|
return aShape;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string writeStep(const TopoDS_Shape& theShape)
|
||||||
|
{
|
||||||
|
if (theShape.IsNull())
|
||||||
|
{
|
||||||
|
throw std::invalid_argument("Cannot serialize a null OCCT shape.");
|
||||||
|
}
|
||||||
|
STEPControl_Writer aWriter;
|
||||||
|
if (aWriter.Transfer(theShape, STEPControl_AsIs) != IFSelect_RetDone)
|
||||||
|
{
|
||||||
|
throw std::runtime_error("OCCT could not transfer the shape to STEP.");
|
||||||
|
}
|
||||||
|
std::ostringstream aStream;
|
||||||
|
if (aWriter.WriteStream(aStream) != IFSelect_RetDone)
|
||||||
|
{
|
||||||
|
throw std::runtime_error("OCCT could not write the shape to STEP.");
|
||||||
|
}
|
||||||
|
return aStream.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
emscripten::val booleanHistoryFromStep(const std::string& theObjectStep,
|
||||||
|
const std::string& theToolStep,
|
||||||
|
const std::string& theOperation)
|
||||||
|
{
|
||||||
|
return booleanHistory(readStep(theObjectStep), readStep(theToolStep), theOperation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EMSCRIPTEN_BINDINGS(bitbybit_occt_history)
|
||||||
|
{
|
||||||
|
emscripten::class_<TopoDS_Shape>("TopoDS_Shape")
|
||||||
|
.constructor<>()
|
||||||
|
.function("isNull", &TopoDS_Shape::IsNull)
|
||||||
|
.function("shapeType", &TopoDS_Shape::ShapeType)
|
||||||
|
.function("isSame", &TopoDS_Shape::IsSame);
|
||||||
|
emscripten::function("occtVersion", &occtVersion);
|
||||||
|
emscripten::function("makeBox", &makeBox);
|
||||||
|
emscripten::function("booleanHistory", &booleanHistory);
|
||||||
|
emscripten::function("shapeToStep", &writeStep);
|
||||||
|
emscripten::function("booleanHistoryFromStep", &booleanHistoryFromStep);
|
||||||
|
}
|
||||||
3
native/occt-history/package.json
Normal file
3
native/occt-history/package.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"type": "module"
|
||||||
|
}
|
||||||
22
native/occt-history/smoke-test.mjs
Normal file
22
native/occt-history/smoke-test.mjs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import createOcctHistory from './dist/bitbybit-occt-history.js'
|
||||||
|
|
||||||
|
const occt = await createOcctHistory()
|
||||||
|
assert.match(occt.occtVersion(), /^8\./)
|
||||||
|
const object = occt.makeBox(10, 10, 10)
|
||||||
|
const tool = occt.makeBox(5, 5, 5)
|
||||||
|
const response = occt.booleanHistory(object, tool, 'cut')
|
||||||
|
const objectStep = occt.shapeToStep(object)
|
||||||
|
const toolStep = occt.shapeToStep(tool)
|
||||||
|
assert.match(objectStep, /^ISO-10303-21;/)
|
||||||
|
assert.match(toolStep, /^ISO-10303-21;/)
|
||||||
|
const serializedResponse = occt.booleanHistoryFromStep(objectStep, toolStep, 'cut')
|
||||||
|
assert.equal(response.provider, 'occt-native')
|
||||||
|
assert.equal(response.records.every((record) => ['generated', 'modified', 'deleted'].includes(record.relation)), true)
|
||||||
|
assert.equal(typeof response.hasModified, 'boolean')
|
||||||
|
assert.equal(typeof response.hasGenerated, 'boolean')
|
||||||
|
assert.equal(typeof response.hasDeleted, 'boolean')
|
||||||
|
assert.equal(serializedResponse.provider, 'occt-native')
|
||||||
|
assert.equal(serializedResponse.records.length > 0, true)
|
||||||
|
assert.match(serializedResponse.resultStep, /^ISO-10303-21;/)
|
||||||
|
console.log(JSON.stringify({ occtVersion: response.occtVersion, recordCount: response.records.length, hasModified: response.hasModified, hasGenerated: response.hasGenerated, hasDeleted: response.hasDeleted }, null, 2))
|
||||||
@@ -13,15 +13,19 @@
|
|||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"check:baseline": "node scripts/check-baseline.mjs",
|
"check:baseline": "node scripts/check-baseline.mjs",
|
||||||
"check:freecad-source": "node scripts/verify-freecad-source.mjs",
|
"check:freecad-source": "node scripts/verify-freecad-source.mjs",
|
||||||
|
"check:bitbybit-history": "node scripts/check-bitbybit-history-surface.mjs",
|
||||||
"check:facade-boundary": "node scripts/check-facade-boundary.mjs",
|
"check:facade-boundary": "node scripts/check-facade-boundary.mjs",
|
||||||
"fetch:freecad-source": "node scripts/fetch-freecad-source.mjs",
|
"fetch:freecad-source": "node scripts/fetch-freecad-source.mjs",
|
||||||
"configure:freecad-native": "node scripts/configure-freecad-native.mjs",
|
"configure:freecad-native": "node scripts/configure-freecad-native.mjs",
|
||||||
"build:freecad-native": "node scripts/build-freecad-native.mjs",
|
"build:freecad-native": "node scripts/build-freecad-native.mjs",
|
||||||
|
"build:occt-history": "bash native/occt-history/build.sh",
|
||||||
|
"test:occt-history": "node native/occt-history/smoke-test.mjs",
|
||||||
"test:facade": "tsx --test tests/facade.test.ts",
|
"test:facade": "tsx --test tests/facade.test.ts",
|
||||||
"test:golden": "node --test tests/goldenReplay.test.mjs",
|
"test:golden": "node --test tests/goldenReplay.test.mjs",
|
||||||
"test:golden:freecad": "node scripts/run-freecad-golden.mjs",
|
"test:golden:freecad": "node scripts/run-freecad-golden.mjs",
|
||||||
|
"probe:freecad-reference": "node scripts/run-freecad-reference-probe.mjs",
|
||||||
"check:runtime": "node scripts/check-runtime.mjs",
|
"check:runtime": "node scripts/check-runtime.mjs",
|
||||||
"verify": "npm run check:runtime && npm run check:baseline && npm run check:facade-boundary && npm run test:golden && npm run test:facade && npm run build"
|
"verify": "npm run check:runtime && npm run check:baseline && npm run check:bitbybit-history && npm run check:facade-boundary && npm run test:golden && npm run test:facade && npm run build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bitbybit-dev/occt": "1.1.1",
|
"@bitbybit-dev/occt": "1.1.1",
|
||||||
|
|||||||
83
scripts/check-bitbybit-history-surface.mjs
Normal file
83
scripts/check-bitbybit-history-surface.mjs
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import { readFile } from 'node:fs/promises'
|
||||||
|
import { createHash } from 'node:crypto'
|
||||||
|
import { execFile } from 'node:child_process'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
import { promisify } from 'node:util'
|
||||||
|
import { resolve } from 'node:path'
|
||||||
|
|
||||||
|
const root = resolve(fileURLToPath(new URL('..', import.meta.url)))
|
||||||
|
const run = promisify(execFile)
|
||||||
|
const packageJson = JSON.parse(await readFile(resolve(root, 'node_modules/@bitbybit-dev/occt/package.json'), 'utf8'))
|
||||||
|
const matrix = JSON.parse(await readFile(resolve(root, 'config/compatibility-matrix.json'), 'utf8'))
|
||||||
|
const sourcePath = resolve(root, matrix.bitbybitSource.localSnapshot)
|
||||||
|
const { stdout: sourceCommitOutput } = await run('git', ['-C', sourcePath, 'rev-parse', 'HEAD'])
|
||||||
|
const sourceCommit = sourceCommitOutput.trim()
|
||||||
|
if (sourceCommit !== matrix.bitbybitSource.commit) throw new Error(`Bitbybit source commit mismatch: expected ${matrix.bitbybitSource.commit}, got ${sourceCommit}.`)
|
||||||
|
if (packageJson.version !== matrix.bitbybitSource.version) throw new Error(`Bitbybit package version mismatch: expected ${matrix.bitbybitSource.version}, got ${packageJson.version}.`)
|
||||||
|
const sourceCppFiles = (await run('git', ['-C', sourcePath, 'ls-tree', '-r', '--name-only', 'HEAD'])).stdout
|
||||||
|
.split('\n').filter((file) => /\.(c|cc|cpp|cxx|h|hh|hpp)$/.test(file))
|
||||||
|
if (sourceCppFiles.length !== 0) throw new Error(`Bitbybit source snapshot unexpectedly contains native source files (${sourceCppFiles.length}); review the custom binding build inputs.`)
|
||||||
|
const declarationPath = resolve(root, 'node_modules/@bitbybit-dev/occt/bitbybit-dev-occt/bitbybit-dev-occt.d.ts')
|
||||||
|
const declarations = await readFile(declarationPath, 'utf8')
|
||||||
|
|
||||||
|
const interfaceBody = (name) => {
|
||||||
|
const match = declarations.match(new RegExp(`export interface ${name} extends ClassHandle \\{([\\s\\S]*?)\\n\\}`))
|
||||||
|
if (!match) throw new Error(`Bitbybit declaration is missing interface ${name}.`)
|
||||||
|
return match[1]
|
||||||
|
}
|
||||||
|
const hasMethod = (body, name) => new RegExp(`\\n\\s*${name}\\(`).test(body)
|
||||||
|
const assert = (condition, message) => {
|
||||||
|
if (!condition) throw new Error(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
const booleanInterfaces = ['BRepAlgoAPI_Fuse', 'BRepAlgoAPI_Cut', 'BRepAlgoAPI_Common']
|
||||||
|
const booleanHistorySurface = Object.fromEntries(booleanInterfaces.map((name) => {
|
||||||
|
const body = interfaceBody(name)
|
||||||
|
return [name, {
|
||||||
|
generated: hasMethod(body, 'Generated'),
|
||||||
|
modified: hasMethod(body, 'Modified'),
|
||||||
|
deleted: hasMethod(body, 'IsDeleted'),
|
||||||
|
hasGenerated: hasMethod(body, 'HasGenerated')
|
||||||
|
}]
|
||||||
|
}))
|
||||||
|
const builderBody = interfaceBody('BOPAlgo_Builder')
|
||||||
|
const listBody = interfaceBody('TopTools_ListOfShape')
|
||||||
|
|
||||||
|
for (const [name, surface] of Object.entries(booleanHistorySurface)) {
|
||||||
|
assert(!surface.generated && !surface.modified && !surface.deleted, `${name} unexpectedly exposes native Generated/Modified/IsDeleted; review the adapter contract.`)
|
||||||
|
assert(surface.hasGenerated, `${name} must retain HasGenerated for capability reporting.`)
|
||||||
|
}
|
||||||
|
assert(hasMethod(builderBody, 'Modified'), 'BOPAlgo_Builder.Modified(shape) is required for the partial history surface.')
|
||||||
|
for (const method of ['Size', 'IsEmpty', 'Append', 'First']) {
|
||||||
|
assert(hasMethod(listBody, method), `TopTools_ListOfShape.${method}() is required for the partial history surface.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify({
|
||||||
|
provider: 'bitbybit-occt',
|
||||||
|
version: packageJson.version,
|
||||||
|
sourceCommit,
|
||||||
|
sourceForm: matrix.bitbybitSource.sourceForm,
|
||||||
|
missingBuildInputs: matrix.bitbybitSource.missingBuildInputs,
|
||||||
|
declarationPath: 'node_modules/@bitbybit-dev/occt/bitbybit-dev-occt/bitbybit-dev-occt.d.ts',
|
||||||
|
nativeBooleanHistory: { generated: false, modified: false, deleted: false },
|
||||||
|
partialBuilderHistory: { modified: true, listIndexAccess: false },
|
||||||
|
nativeExtension: await (async () => {
|
||||||
|
const result = { status: 'not-built' }
|
||||||
|
for (const [key, relativePath, expectedHash] of [
|
||||||
|
['javascript', matrix.nativeOcctHistory.artifact.javascript, matrix.nativeOcctHistory.artifact.javascriptSha256],
|
||||||
|
['wasm', matrix.nativeOcctHistory.artifact.wasm, matrix.nativeOcctHistory.artifact.wasmSha256]
|
||||||
|
]) {
|
||||||
|
try {
|
||||||
|
const bytes = await readFile(resolve(root, relativePath))
|
||||||
|
const hash = createHash('sha256').update(bytes).digest('hex')
|
||||||
|
if (hash !== expectedHash) throw new Error(`${key} hash mismatch: expected ${expectedHash}, got ${hash}.`)
|
||||||
|
result[key] = 'hash-ok'
|
||||||
|
} catch (error) {
|
||||||
|
if (error?.code !== 'ENOENT') throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (result.javascript === 'hash-ok' && result.wasm === 'hash-ok') result.status = 'built-hash-ok'
|
||||||
|
return result
|
||||||
|
})(),
|
||||||
|
status: 'audit-pass'
|
||||||
|
}, null, 2))
|
||||||
@@ -5,7 +5,7 @@ const sourceRoot = new URL('../src/', import.meta.url)
|
|||||||
const forbidden = [
|
const forbidden = [
|
||||||
/from\s+["']three(?:\/|["'])/, /from\s+["']@types\/three/, /from\s+["']sqlite3?/, /from\s+["']@sqlite/, /from\s+["']opfs/, /SharedArrayBuffer/, /\bpostMessage\s*\(/,
|
/from\s+["']three(?:\/|["'])/, /from\s+["']@types\/three/, /from\s+["']sqlite3?/, /from\s+["']@sqlite/, /from\s+["']opfs/, /SharedArrayBuffer/, /\bpostMessage\s*\(/,
|
||||||
]
|
]
|
||||||
const allowDirectRuntime = new Set(['facade/threeViewport.ts', 'facade/threeViewport.tsx', 'facade/persistenceWorker.ts', 'facade/projectStore.ts', 'facade/geometryWorker.ts'])
|
const allowDirectRuntime = new Set(['facade/threeViewport.ts', 'facade/threeViewport.tsx', 'facade/persistenceWorker.ts', 'facade/projectStore.ts', 'facade/geometryWorker.ts', 'facade/nativeHistoryWorkerClient.ts', 'facade/nativeHistoryWorkerEntry.ts'])
|
||||||
|
|
||||||
async function walk(relative = '') {
|
async function walk(relative = '') {
|
||||||
const directory = new URL(relative, sourceRoot)
|
const directory = new URL(relative, sourceRoot)
|
||||||
|
|||||||
68
scripts/freecad-reference-probe.py
Normal file
68
scripts/freecad-reference-probe.py
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import importlib
|
||||||
|
import json
|
||||||
|
|
||||||
|
import FreeCAD as App
|
||||||
|
|
||||||
|
|
||||||
|
def property_metadata(obj):
|
||||||
|
properties = []
|
||||||
|
for name in obj.PropertiesList:
|
||||||
|
try:
|
||||||
|
type_id = obj.getTypeIdOfProperty(name)
|
||||||
|
except Exception:
|
||||||
|
type_id = "unknown"
|
||||||
|
try:
|
||||||
|
group = obj.getGroupOfProperty(name)
|
||||||
|
except Exception:
|
||||||
|
group = ""
|
||||||
|
try:
|
||||||
|
status = list(obj.getPropertyStatus(name))
|
||||||
|
except Exception:
|
||||||
|
status = []
|
||||||
|
properties.append({"name": name, "typeId": type_id, "group": group, "status": status})
|
||||||
|
return properties
|
||||||
|
|
||||||
|
|
||||||
|
def probe_object(document, type_id):
|
||||||
|
try:
|
||||||
|
obj = document.addObject(type_id, "Reference" + type_id.replace(":", "_"))
|
||||||
|
except Exception as error:
|
||||||
|
return {"typeId": type_id, "available": False, "error": str(error)}
|
||||||
|
return {
|
||||||
|
"typeId": type_id,
|
||||||
|
"available": True,
|
||||||
|
"runtimeTypeId": obj.TypeId,
|
||||||
|
"properties": property_metadata(obj),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def probe_module(name):
|
||||||
|
try:
|
||||||
|
module = importlib.import_module(name)
|
||||||
|
return {"name": name, "available": True, "file": getattr(module, "__file__", None)}
|
||||||
|
except Exception as error:
|
||||||
|
return {"name": name, "available": False, "error": str(error)}
|
||||||
|
|
||||||
|
|
||||||
|
document = App.newDocument("ReferenceProbe")
|
||||||
|
version = App.Version()
|
||||||
|
result = {
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"baselineId": "freecad-1.1.1",
|
||||||
|
"freecadVersion": ".".join(str(value) for value in version[:3]),
|
||||||
|
"revision": str(version[3]),
|
||||||
|
"gitBranch": str(version[6]) if len(version) > 6 else "",
|
||||||
|
"gitCommit": str(version[7]) if len(version) > 7 else "",
|
||||||
|
"guiUp": bool(getattr(App, "GuiUp", False)),
|
||||||
|
"buildPurpose": "headless-reference-oracle",
|
||||||
|
"modules": [probe_module(name) for name in [
|
||||||
|
"Part", "Material", "Measure", "Sketcher", "PartDesign", "TechDraw",
|
||||||
|
"Spreadsheet", "Draft", "Mesh", "Fem", "CAM", "Assembly",
|
||||||
|
]],
|
||||||
|
"objects": [probe_object(document, type_id) for type_id in [
|
||||||
|
"Part::Box", "Part::Cylinder", "Part::Sphere", "Part::Cone",
|
||||||
|
"Part::Feature", "PartDesign::Feature", "Sketcher::SketchObject",
|
||||||
|
]],
|
||||||
|
}
|
||||||
|
App.closeDocument(document.Name)
|
||||||
|
print("FREECAD_REFERENCE_RESULT=" + json.dumps(result, sort_keys=True, separators=(",", ":")))
|
||||||
29
scripts/run-freecad-reference-probe.mjs
Normal file
29
scripts/run-freecad-reference-probe.mjs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { spawnSync } from 'node:child_process'
|
||||||
|
import { dirname, resolve } from 'node:path'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
|
||||||
|
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||||
|
const localOracle = resolve(root, '.cache/freecad/install-native/bin/FreeCADCmd')
|
||||||
|
const candidates = process.env.FREECAD_CMD ? [process.env.FREECAD_CMD] : [localOracle, 'FreeCADCmd', 'freecadcmd']
|
||||||
|
let command = null
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
const probe = spawnSync(candidate, ['--version'], { encoding: 'utf8', timeout: 15000 })
|
||||||
|
if (!probe.error || probe.error.code !== 'ENOENT') { command = candidate; break }
|
||||||
|
}
|
||||||
|
if (!command) throw new Error('FreeCADCmd 1.1.1 is unavailable. Set FREECAD_CMD to the locked oracle executable.')
|
||||||
|
|
||||||
|
const execution = spawnSync(command, [resolve(root, 'scripts/freecad-reference-probe.py')], {
|
||||||
|
cwd: root,
|
||||||
|
encoding: 'utf8',
|
||||||
|
timeout: 120000,
|
||||||
|
maxBuffer: 20 * 1024 * 1024,
|
||||||
|
env: { ...process.env },
|
||||||
|
})
|
||||||
|
const output = `${execution.stdout || ''}\n${execution.stderr || ''}`
|
||||||
|
const resultLine = output.split(/\r?\n/).find((line) => line.startsWith('FREECAD_REFERENCE_RESULT='))
|
||||||
|
if (execution.error || execution.status !== 0 || !resultLine) {
|
||||||
|
throw new Error(`FreeCAD reference probe failed with status ${execution.status}: ${execution.error?.message || output.trim()}`)
|
||||||
|
}
|
||||||
|
const result = JSON.parse(resultLine.slice('FREECAD_REFERENCE_RESULT='.length))
|
||||||
|
if (result.freecadVersion !== '1.1.1') throw new Error(`Expected FreeCAD 1.1.1, received ${result.freecadVersion}`)
|
||||||
|
console.log(JSON.stringify({ command, result }, null, 2))
|
||||||
@@ -22,11 +22,19 @@ export type FcstdEntryMetadata = {
|
|||||||
|
|
||||||
export type FcstdObjectSupport = 'recognized' | 'proxy' | 'blocked'
|
export type FcstdObjectSupport = 'recognized' | 'proxy' | 'blocked'
|
||||||
|
|
||||||
|
export type FcstdPropertySummary = {
|
||||||
|
name: string
|
||||||
|
typeId: string
|
||||||
|
element: string
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
|
||||||
export type FcstdObjectSummary = {
|
export type FcstdObjectSummary = {
|
||||||
name: string
|
name: string
|
||||||
label: string
|
label: string
|
||||||
typeId: string
|
typeId: string
|
||||||
propertyCount: number
|
propertyCount: number
|
||||||
|
properties: FcstdPropertySummary[]
|
||||||
support: FcstdObjectSupport
|
support: FcstdObjectSupport
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,6 +187,18 @@ const propertyValue = (property: Record<string, unknown>): string => {
|
|||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const propertyElement = (property: Record<string, unknown>): string => {
|
||||||
|
const entry = Object.entries(property).find(([name, value]) => !name.startsWith('@_') && Boolean(value && typeof value === 'object'))
|
||||||
|
return entry?.[0] || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const propertySummaries = (properties: Record<string, unknown>[]): FcstdPropertySummary[] => properties.map((property) => ({
|
||||||
|
name: attribute(property, 'name') || '<unnamed>',
|
||||||
|
typeId: attribute(property, 'type') || propertyElement(property) || 'unknown',
|
||||||
|
element: propertyElement(property),
|
||||||
|
value: propertyValue(property),
|
||||||
|
}))
|
||||||
|
|
||||||
const parseDocumentXml = (bytes: Uint8Array) => {
|
const parseDocumentXml = (bytes: Uint8Array) => {
|
||||||
const xml = new TextDecoder('utf-8', { fatal: true }).decode(bytes)
|
const xml = new TextDecoder('utf-8', { fatal: true }).decode(bytes)
|
||||||
if (/<!DOCTYPE|<!ENTITY/i.test(xml)) throw new Error('FCStd Document.xml declarations and entities are not allowed.')
|
if (/<!DOCTYPE|<!ENTITY/i.test(xml)) throw new Error('FCStd Document.xml declarations and entities are not allowed.')
|
||||||
@@ -202,7 +222,7 @@ const parseDocumentXml = (bytes: Uint8Array) => {
|
|||||||
const properties = asArray((((data?.Properties as Record<string, unknown> | undefined)?.Property) as Record<string, unknown> | Record<string, unknown>[] | undefined))
|
const properties = asArray((((data?.Properties as Record<string, unknown> | undefined)?.Property) as Record<string, unknown> | Record<string, unknown>[] | undefined))
|
||||||
const objectLabelProperty = properties.find((property) => attribute(property, 'name') === 'Label')
|
const objectLabelProperty = properties.find((property) => attribute(property, 'name') === 'Label')
|
||||||
const support: FcstdObjectSupport = blockedTypeId(typeId) ? 'blocked' : recognizedTypeIds.has(typeId) ? 'recognized' : 'proxy'
|
const support: FcstdObjectSupport = blockedTypeId(typeId) ? 'blocked' : recognizedTypeIds.has(typeId) ? 'recognized' : 'proxy'
|
||||||
return { name, label: objectLabelProperty ? propertyValue(objectLabelProperty) || name : name, typeId, propertyCount: properties.length, support }
|
return { name, label: objectLabelProperty ? propertyValue(objectLabelProperty) || name : name, typeId, propertyCount: properties.length, properties: propertySummaries(properties), support }
|
||||||
})
|
})
|
||||||
return {
|
return {
|
||||||
schemaVersion: attribute(root, 'SchemaVersion') || attribute(root, 'schemaVersion') || 'unknown',
|
schemaVersion: attribute(root, 'SchemaVersion') || attribute(root, 'schemaVersion') || 'unknown',
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { BitByBitOCCT, OccStateEnum } from '@bitbybit-dev/occt-worker'
|
import { BitByBitOCCT, OccStateEnum } from '@bitbybit-dev/occt-worker'
|
||||||
import type { Inputs } from '@bitbybit-dev/occt'
|
import type { Inputs } from '@bitbybit-dev/occt'
|
||||||
import type { ApplyPlacementInput, BooleanCutInput, BooleanIntersectionInput, BooleanUnionInput, ChamferInput, CreateBoxInput, CreateConeInput, CreateCylinderInput, CreateSphereInput, FilletInput, GeometryCapabilities, GeometryDocumentContext, GeometryFileExport, LinearFeatureParameters, MeshAsset, MirrorInput, PadInput, PlanarProfile, PocketInput, Point3, RevolutionInput, ShapeHandle, SubshapeRef, SubshapeTopology } from './types'
|
import type { ApplyPlacementInput, BooleanCutInput, BooleanIntersectionInput, BooleanUnionInput, ChamferInput, CreateBoxInput, CreateConeInput, CreateCylinderInput, CreateSphereInput, FilletInput, GeometryCapabilities, GeometryDocumentContext, GeometryFileExport, LinearFeatureParameters, MeshAsset, MirrorInput, NativeTopologyHistoryInput, NativeTopologyHistoryRecord, PadInput, PlanarProfile, PocketInput, Point3, RevolutionInput, ShapeHandle, SubshapeRef, SubshapeTopology } from './types'
|
||||||
|
import { mapNativeOcctHistoryRecords } from './nativeHistoryProvider'
|
||||||
|
import { NativeOcctHistoryCoordinator, type NativeOcctHistoryProvider } from './nativeHistoryProtocol'
|
||||||
import { createEdgeSubshapeRefs, createSubshapeRefs, createVertexSubshapeRefs } from './topologyNaming'
|
import { createEdgeSubshapeRefs, createSubshapeRefs, createVertexSubshapeRefs } from './topologyNaming'
|
||||||
|
|
||||||
type KernelShapeReference = Inputs.OCCT.TopoDSShapePointer
|
type KernelShapeReference = Inputs.OCCT.TopoDSShapePointer
|
||||||
@@ -230,6 +232,34 @@ export class BitbybitGeometryRuntime {
|
|||||||
private sequence = 0
|
private sequence = 0
|
||||||
private readonly shapes = new Map<string, ShapeEntry>()
|
private readonly shapes = new Map<string, ShapeEntry>()
|
||||||
private readonly kernelReferences = new Map<number, KernelReferenceEntry>()
|
private readonly kernelReferences = new Map<number, KernelReferenceEntry>()
|
||||||
|
private nativeHistory: { provider: NativeOcctHistoryProvider; coordinator: NativeOcctHistoryCoordinator } | null = null
|
||||||
|
private readonly nativeHistoryDocumentVersions = new Map<string, number>()
|
||||||
|
|
||||||
|
configureNativeHistory(provider: NativeOcctHistoryProvider | null, timeoutMs = 120_000) {
|
||||||
|
this.nativeHistory?.coordinator.cancel()
|
||||||
|
if (provider === null) {
|
||||||
|
this.nativeHistory = null
|
||||||
|
this.nativeHistoryDocumentVersions.clear()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) throw new RangeError('Native history timeoutMs must be a positive safe integer.')
|
||||||
|
this.nativeHistory = {
|
||||||
|
provider,
|
||||||
|
coordinator: new NativeOcctHistoryCoordinator((documentId) => this.nativeHistoryDocumentVersions.get(documentId) ?? null, timeoutMs),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nativeHistoryCapabilities() {
|
||||||
|
return this.nativeHistory?.provider.capabilities() ?? {
|
||||||
|
providerId: 'occt-native.history-step',
|
||||||
|
providerVersion: 'unconfigured',
|
||||||
|
occtVersion: 'unknown',
|
||||||
|
availability: 'unavailable' as const,
|
||||||
|
operations: [],
|
||||||
|
transport: 'step-text' as const,
|
||||||
|
reason: 'Native OCCT history provider is not configured.',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
capabilities(): GeometryCapabilities { return { ...this.capabilitiesState } }
|
capabilities(): GeometryCapabilities { return { ...this.capabilitiesState } }
|
||||||
|
|
||||||
@@ -471,6 +501,30 @@ export class BitbybitGeometryRuntime {
|
|||||||
return { faces: faceTopology.refs, edges: edgeTopology.refs, vertices: vertexTopology.refs, entries }
|
return { faces: faceTopology.refs, edges: edgeTopology.refs, vertices: vertexTopology.refs, entries }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async topologyHistory(input: NativeTopologyHistoryInput): Promise<NativeTopologyHistoryRecord[]> {
|
||||||
|
const nativeHistory = this.nativeHistory
|
||||||
|
if (!nativeHistory) throw new Error('Native OCCT history provider is not configured.')
|
||||||
|
if (!input.operation) throw new Error('Native OCCT history requires a supported Boolean operation.')
|
||||||
|
if (input.inputs.length !== 2) throw new Error('Native OCCT history requires exactly an object and a tool input.')
|
||||||
|
validateDocumentContext(input)
|
||||||
|
this.nativeHistoryDocumentVersions.set(input.documentId, input.documentVersion)
|
||||||
|
const [objectInput, toolInput] = input.inputs
|
||||||
|
const [objectStep, toolStep] = await Promise.all([
|
||||||
|
this.exportStep(objectInput.shape, `${objectInput.objectId}.step`),
|
||||||
|
this.exportStep(toolInput.shape, `${toolInput.objectId}.step`),
|
||||||
|
])
|
||||||
|
const execution = await nativeHistory.coordinator.capture(nativeHistory.provider, {
|
||||||
|
documentId: input.documentId,
|
||||||
|
documentVersion: input.documentVersion,
|
||||||
|
operationId: input.operationId,
|
||||||
|
operation: input.operation,
|
||||||
|
objectStep: objectStep.text,
|
||||||
|
toolStep: toolStep.text,
|
||||||
|
})
|
||||||
|
if (execution.status !== 'completed' || !execution.response) throw new Error(`Native OCCT history ${execution.status}.`)
|
||||||
|
return mapNativeOcctHistoryRecords(execution.response.history, { object: objectInput.objectId, tool: toolInput.objectId })
|
||||||
|
}
|
||||||
|
|
||||||
async release(shape: ShapeHandle): Promise<void> {
|
async release(shape: ShapeHandle): Promise<void> {
|
||||||
const entry = this.shapes.get(shape.id)
|
const entry = this.shapes.get(shape.id)
|
||||||
if (!entry) return
|
if (!entry) return
|
||||||
@@ -487,6 +541,7 @@ export class BitbybitGeometryRuntime {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dispose() {
|
dispose() {
|
||||||
|
this.nativeHistory?.coordinator.cancel()
|
||||||
this.cancelInitialization?.()
|
this.cancelInitialization?.()
|
||||||
this.shapes.clear()
|
this.shapes.clear()
|
||||||
this.kernelReferences.clear()
|
this.kernelReferences.clear()
|
||||||
@@ -496,6 +551,8 @@ export class BitbybitGeometryRuntime {
|
|||||||
this.worker = null
|
this.worker = null
|
||||||
this.initialization = null
|
this.initialization = null
|
||||||
this.cancelInitialization = null
|
this.cancelInitialization = null
|
||||||
|
this.nativeHistoryDocumentVersions.clear()
|
||||||
|
this.nativeHistory = null
|
||||||
this.capabilitiesState = unavailableCapabilities()
|
this.capabilitiesState = unavailableCapabilities()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,8 +11,15 @@ export type { ApplyPlacementInput, BitBybitViewportAdapter, BitBybitWebCadFacade
|
|||||||
export { createEdgeSubshapeRefs, createSubshapeRefs, createVertexSubshapeRefs, matchSubshapes, signatureForEdge, signatureForFace, signatureForVertex } from './topologyNaming'
|
export { createEdgeSubshapeRefs, createSubshapeRefs, createVertexSubshapeRefs, matchSubshapes, signatureForEdge, signatureForFace, signatureForVertex } from './topologyNaming'
|
||||||
export { cloneObjectTopologySnapshot, createPersistedTopoRef, migrateDocumentTopologyReferences, migrateTopoRefs, parseTopoRef, resolveDocumentTopologyReference, resolveTopoRef, serializeTopoRef } from './topologyReferences'
|
export { cloneObjectTopologySnapshot, createPersistedTopoRef, migrateDocumentTopologyReferences, migrateTopoRefs, parseTopoRef, resolveDocumentTopologyReference, resolveTopoRef, serializeTopoRef } from './topologyReferences'
|
||||||
export type { DocumentTopologyReferenceMigration, PersistedTopoRef, TopologyMigration, TopologyReferenceMigrationIssue, TopoRefResolution } from './topologyReferences'
|
export type { DocumentTopologyReferenceMigration, PersistedTopoRef, TopologyMigration, TopologyReferenceMigrationIssue, TopoRefResolution } from './topologyReferences'
|
||||||
export { captureSignatureTopologyHistory } from './topologyHistory'
|
export { captureNativeTopologyHistory, captureSignatureTopologyHistory } from './topologyHistory'
|
||||||
export type { TopologyHistoryEntry, TopologyHistoryRelation, TopologyHistoryResult } from './topologyHistory'
|
export type { TopologyHistoryEntry, TopologyHistoryRelation, TopologyHistoryResult } from './topologyHistory'
|
||||||
|
export { createNativeOcctStepHistoryBridge, mapNativeOcctHistoryRecords } from './nativeHistoryProvider'
|
||||||
|
export type { NativeOcctHistoryOperation, NativeOcctHistoryRecord, NativeOcctHistoryResponse, NativeOcctHistoryStepGeometry, NativeOcctHistoryStepProvider } from './nativeHistoryProvider'
|
||||||
|
export { DirectNativeOcctHistoryProvider, NativeOcctHistoryCoordinator, NativeOcctHistoryUnavailableError, UnavailableNativeOcctHistoryProvider } from './nativeHistoryProtocol'
|
||||||
|
export { NATIVE_OCCT_HISTORY_PROTOCOL_VERSION } from './nativeHistoryProtocol'
|
||||||
|
export type { NativeOcctHistoryCapabilities, NativeOcctHistoryExecution, NativeOcctHistoryProvider, NativeOcctHistoryProtocolResponse, NativeOcctHistoryRequest } from './nativeHistoryProtocol'
|
||||||
|
export { NativeOcctHistoryWorkerProvider } from './nativeHistoryWorkerClient'
|
||||||
|
export type { NativeOcctHistoryWorkerOptions } from './nativeHistoryWorkerClient'
|
||||||
export { BasicSketchSolverAdapter, cloneSketch, cloneSketchConstraint, cloneSketchGeometry, createSketch, solveSketch } from './sketcher'
|
export { BasicSketchSolverAdapter, cloneSketch, cloneSketchConstraint, cloneSketchGeometry, createSketch, solveSketch } from './sketcher'
|
||||||
export type { SketchConstraint, SketchDiagnostic, SketchExternalGeometry, SketchGeometry, SketchPoint, SketchPointRef, SketchSnapshot, SketchSolveOptions, SketchSolveResult, SketchSolverAdapter, SketchSolverStatus } from './sketcher'
|
export type { SketchConstraint, SketchDiagnostic, SketchExternalGeometry, SketchGeometry, SketchPoint, SketchPointRef, SketchSnapshot, SketchSolveOptions, SketchSolveResult, SketchSolverAdapter, SketchSolverStatus } from './sketcher'
|
||||||
export { BasicSketchSolverProvider, SKETCH_SOLVER_PROTOCOL_VERSION, SketchSolverCoordinator, SketchSolverUnavailableError, UnavailablePlanegcsProvider, runSketchSolverReplay } from './sketchSolverProtocol'
|
export { BasicSketchSolverProvider, SKETCH_SOLVER_PROTOCOL_VERSION, SketchSolverCoordinator, SketchSolverUnavailableError, UnavailablePlanegcsProvider, runSketchSolverReplay } from './sketchSolverProtocol'
|
||||||
@@ -20,4 +27,4 @@ export type { SketchSolverCapabilities, SketchSolverCompatibility, SketchSolverE
|
|||||||
export { createFacadeGeometryRecomputeExecutor, executeFacadeRecomputeNode, RecomputeCoordinator } from './recomputeEngine'
|
export { createFacadeGeometryRecomputeExecutor, executeFacadeRecomputeNode, RecomputeCoordinator } from './recomputeEngine'
|
||||||
export type { RecomputeExecutionError, RecomputeExecutionOptions, RecomputeExecutionResult, RecomputeExecutionStatus, RecomputeGeometryRuntime, RecomputeNodeContext, RecomputeNodeExecutor, RecomputeNodeResult, RecomputeProgress } from './recomputeEngine'
|
export type { RecomputeExecutionError, RecomputeExecutionOptions, RecomputeExecutionResult, RecomputeExecutionStatus, RecomputeGeometryRuntime, RecomputeNodeContext, RecomputeNodeExecutor, RecomputeNodeResult, RecomputeProgress } from './recomputeEngine'
|
||||||
export { DEFAULT_FCSTD_LIMITS, inspectFcstdArchive } from './fcstd'
|
export { DEFAULT_FCSTD_LIMITS, inspectFcstdArchive } from './fcstd'
|
||||||
export type { FcstdArchiveLimits, FcstdCompatibilityReport, FcstdEntryMetadata, FcstdEntryRole, FcstdInspection, FcstdObjectSummary, FcstdObjectSupport } from './fcstd'
|
export type { FcstdArchiveLimits, FcstdCompatibilityReport, FcstdEntryMetadata, FcstdEntryRole, FcstdInspection, FcstdObjectSummary, FcstdObjectSupport, FcstdPropertySummary } from './fcstd'
|
||||||
|
|||||||
@@ -809,7 +809,7 @@ export function createMockFacade(): BitBybitWebCadFacade {
|
|||||||
task: { getActive: () => getState().task, begin: beginTask, update: (draft) => { if (state.task) state = { ...state, task: { ...state.task, draft: { ...state.task.draft, ...draft } } }; emitState() }, apply: applyTask, cancel: () => { if (state.task) state = { ...state, task: { ...state.task, status: 'cancelled' } }; emitState() } },
|
task: { getActive: () => getState().task, begin: beginTask, update: (draft) => { if (state.task) state = { ...state, task: { ...state.task, draft: { ...state.task.draft, ...draft } } }; emitState() }, apply: applyTask, cancel: () => { if (state.task) state = { ...state, task: { ...state.task, status: 'cancelled' } }; emitState() } },
|
||||||
diagnostics: { list: () => state.diagnostics.map(cloneDiagnostic), tree: () => buildDiagnosticTree(state.diagnostics), repair: repairDiagnostic },
|
diagnostics: { list: () => state.diagnostics.map(cloneDiagnostic), tree: () => buildDiagnosticTree(state.diagnostics), repair: repairDiagnostic },
|
||||||
project: { capabilities: () => projectPersistence.capabilities(), subscribeExternalChanges: (listener) => projectPersistence.subscribeExternalChanges(listener), list: () => projectPersistence.list(), save: (document = getState().document) => projectPersistence.save(document), load: (documentId) => projectPersistence.load(documentId), loadCheckpoint: (documentId, version) => projectPersistence.loadCheckpoint(documentId, version), recovery: (documentId) => projectPersistence.recovery(documentId), fcstd: { inspect: (bytes, limits) => inspectFcstdArchive(bytes, limits) }, resource: projectPersistence.resource },
|
project: { capabilities: () => projectPersistence.capabilities(), subscribeExternalChanges: (listener) => projectPersistence.subscribeExternalChanges(listener), list: () => projectPersistence.list(), save: (document = getState().document) => projectPersistence.save(document), load: (documentId) => projectPersistence.load(documentId), loadCheckpoint: (documentId, version) => projectPersistence.loadCheckpoint(documentId, version), recovery: (documentId) => projectPersistence.recovery(documentId), fcstd: { inspect: (bytes, limits) => inspectFcstdArchive(bytes, limits) }, resource: projectPersistence.resource },
|
||||||
geometry: { capabilities: () => geometryRuntime.capabilities(), initialize: () => geometryRuntime.initialize(), createBox: (input) => geometryRuntime.createBox(input), createCylinder: (input) => geometryRuntime.createCylinder(input), createSphere: (input) => geometryRuntime.createSphere(input), createCone: (input) => geometryRuntime.createCone(input), applyPlacement: (input) => geometryRuntime.applyPlacement(input), mirror: (input) => geometryRuntime.mirror(input), union: (input) => geometryRuntime.union(input), cut: (input) => geometryRuntime.cut(input), intersection: (input) => geometryRuntime.intersection(input), fillet: (input) => geometryRuntime.fillet(input), chamfer: (input) => geometryRuntime.chamfer(input), exportStep: (shape, fileName) => geometryRuntime.exportStep(shape, fileName), exportStl: (shape, fileName, precision) => geometryRuntime.exportStl(shape, fileName, precision), pad: (input) => geometryRuntime.pad(input), pocket: (input) => geometryRuntime.pocket(input), revolution: (input) => geometryRuntime.revolution(input), mesh: (shape, precision) => geometryRuntime.mesh(shape, precision), subshapes: (shape, precision) => geometryRuntime.subshapes(shape, precision), topology: (shape, precision) => geometryRuntime.topology(shape, precision), getObjectShape: (objectId) => { const shape = featureShapes.get(objectId); return shape ? { ...shape } : null }, release: (shape) => geometryRuntime.release(shape), dispose: () => { clearFeatureShapes(); geometryRuntime.dispose() } },
|
geometry: { capabilities: () => geometryRuntime.capabilities(), configureNativeHistory: (provider, timeoutMs) => geometryRuntime.configureNativeHistory(provider, timeoutMs), nativeHistoryCapabilities: () => geometryRuntime.nativeHistoryCapabilities(), initialize: () => geometryRuntime.initialize(), createBox: (input) => geometryRuntime.createBox(input), createCylinder: (input) => geometryRuntime.createCylinder(input), createSphere: (input) => geometryRuntime.createSphere(input), createCone: (input) => geometryRuntime.createCone(input), applyPlacement: (input) => geometryRuntime.applyPlacement(input), mirror: (input) => geometryRuntime.mirror(input), union: (input) => geometryRuntime.union(input), cut: (input) => geometryRuntime.cut(input), intersection: (input) => geometryRuntime.intersection(input), fillet: (input) => geometryRuntime.fillet(input), chamfer: (input) => geometryRuntime.chamfer(input), exportStep: (shape, fileName) => geometryRuntime.exportStep(shape, fileName), exportStl: (shape, fileName, precision) => geometryRuntime.exportStl(shape, fileName, precision), pad: (input) => geometryRuntime.pad(input), pocket: (input) => geometryRuntime.pocket(input), revolution: (input) => geometryRuntime.revolution(input), mesh: (shape, precision) => geometryRuntime.mesh(shape, precision), subshapes: (shape, precision) => geometryRuntime.subshapes(shape, precision), topology: (shape, precision) => geometryRuntime.topology(shape, precision), topologyHistory: (input) => geometryRuntime.topologyHistory(input), getObjectShape: (objectId) => { const shape = featureShapes.get(objectId); return shape ? { ...shape } : null }, release: (shape) => geometryRuntime.release(shape), dispose: () => { clearFeatureShapes(); geometryRuntime.dispose() } },
|
||||||
viewport: { createAdapter: () => new ThreeViewportAdapter() },
|
viewport: { createAdapter: () => new ThreeViewportAdapter() },
|
||||||
getState, subscribe: (listener) => { listeners.add(listener); return () => { listeners.delete(listener) } }, notify,
|
getState, subscribe: (listener) => { listeners.add(listener); return () => { listeners.delete(listener) } }, notify,
|
||||||
}
|
}
|
||||||
|
|||||||
138
src/facade/nativeHistoryProtocol.ts
Normal file
138
src/facade/nativeHistoryProtocol.ts
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
import type { NativeOcctHistoryOperation, NativeOcctHistoryResponse, NativeOcctHistoryStepProvider } from './nativeHistoryProvider'
|
||||||
|
|
||||||
|
export const NATIVE_OCCT_HISTORY_PROTOCOL_VERSION = 1 as const
|
||||||
|
|
||||||
|
export type NativeOcctHistoryCapabilities = {
|
||||||
|
providerId: string
|
||||||
|
providerVersion: string
|
||||||
|
occtVersion: string
|
||||||
|
availability: 'available' | 'unavailable'
|
||||||
|
operations: NativeOcctHistoryOperation[]
|
||||||
|
transport: 'step-text'
|
||||||
|
reason?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NativeOcctHistoryRequest = {
|
||||||
|
protocolVersion: typeof NATIVE_OCCT_HISTORY_PROTOCOL_VERSION
|
||||||
|
requestId: string
|
||||||
|
documentId: string
|
||||||
|
documentVersion: number
|
||||||
|
operationId: string
|
||||||
|
operation: NativeOcctHistoryOperation
|
||||||
|
objectStep: string
|
||||||
|
toolStep: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NativeOcctHistoryProtocolResponse = {
|
||||||
|
protocolVersion: typeof NATIVE_OCCT_HISTORY_PROTOCOL_VERSION
|
||||||
|
requestId: string
|
||||||
|
documentId: string
|
||||||
|
documentVersion: number
|
||||||
|
operationId: string
|
||||||
|
provider: NativeOcctHistoryCapabilities
|
||||||
|
history: NativeOcctHistoryResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NativeOcctHistoryProvider {
|
||||||
|
capabilities(): NativeOcctHistoryCapabilities
|
||||||
|
capture(request: NativeOcctHistoryRequest, signal: AbortSignal): Promise<NativeOcctHistoryProtocolResponse>
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NativeOcctHistoryUnavailableError extends Error {
|
||||||
|
readonly code = 'NATIVE_OCCT_HISTORY_UNAVAILABLE'
|
||||||
|
|
||||||
|
constructor(readonly provider: NativeOcctHistoryCapabilities) {
|
||||||
|
super(provider.reason || `Native OCCT history provider '${provider.providerId}' is unavailable.`)
|
||||||
|
this.name = 'NativeOcctHistoryUnavailableError'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const abortError = () => new DOMException('Native OCCT history request cancelled.', 'AbortError')
|
||||||
|
|
||||||
|
const assertRequest = (request: NativeOcctHistoryRequest) => {
|
||||||
|
if (request.protocolVersion !== NATIVE_OCCT_HISTORY_PROTOCOL_VERSION) throw new RangeError(`Unsupported native OCCT history protocol version: ${request.protocolVersion}.`)
|
||||||
|
if (!request.requestId.trim() || !request.documentId.trim() || !request.operationId.trim()) throw new TypeError('Native OCCT history request IDs must be non-empty strings.')
|
||||||
|
if (!Number.isSafeInteger(request.documentVersion) || request.documentVersion < 0) throw new RangeError('Native OCCT history documentVersion must be a non-negative integer.')
|
||||||
|
if (!request.objectStep.startsWith('ISO-10303-21;') || !request.toolStep.startsWith('ISO-10303-21;')) throw new TypeError('Native OCCT history transport requires ISO-10303-21 STEP text.')
|
||||||
|
}
|
||||||
|
|
||||||
|
const assertResponse = (request: NativeOcctHistoryRequest, response: NativeOcctHistoryProtocolResponse) => {
|
||||||
|
if (response.protocolVersion !== request.protocolVersion || response.requestId !== request.requestId || response.documentId !== request.documentId || response.documentVersion !== request.documentVersion || response.operationId !== request.operationId) throw new Error('Native OCCT history response does not match its request context.')
|
||||||
|
if (response.history.provider !== 'occt-native' || response.history.occtVersion !== response.provider.occtVersion) throw new Error('Native OCCT history response provider metadata is inconsistent.')
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DirectNativeOcctHistoryProvider implements NativeOcctHistoryProvider {
|
||||||
|
constructor(private readonly module: NativeOcctHistoryStepProvider, private readonly providerVersion = '8.0.0-embind') {}
|
||||||
|
|
||||||
|
capabilities(): NativeOcctHistoryCapabilities {
|
||||||
|
const occtVersion = this.module.occtVersion()
|
||||||
|
return {
|
||||||
|
providerId: 'occt-native.history-step',
|
||||||
|
providerVersion: this.providerVersion,
|
||||||
|
occtVersion,
|
||||||
|
availability: 'available',
|
||||||
|
operations: ['fuse', 'cut', 'common'],
|
||||||
|
transport: 'step-text',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async capture(request: NativeOcctHistoryRequest, signal: AbortSignal): Promise<NativeOcctHistoryProtocolResponse> {
|
||||||
|
assertRequest(request)
|
||||||
|
if (signal.aborted) throw abortError()
|
||||||
|
const provider = this.capabilities()
|
||||||
|
const history = await Promise.resolve().then(() => this.module.booleanHistoryFromStep(request.objectStep, request.toolStep, request.operation))
|
||||||
|
if (signal.aborted) throw abortError()
|
||||||
|
return assertResponse(request, { protocolVersion: request.protocolVersion, requestId: request.requestId, documentId: request.documentId, documentVersion: request.documentVersion, operationId: request.operationId, provider, history })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UnavailableNativeOcctHistoryProvider implements NativeOcctHistoryProvider {
|
||||||
|
constructor(private readonly message = 'No native OCCT history WASM artifact is installed.') {}
|
||||||
|
|
||||||
|
capabilities(): NativeOcctHistoryCapabilities {
|
||||||
|
return { providerId: 'occt-native.history-step', providerVersion: 'unavailable', occtVersion: 'unknown', availability: 'unavailable', operations: [], transport: 'step-text', reason: this.message }
|
||||||
|
}
|
||||||
|
|
||||||
|
capture(): Promise<NativeOcctHistoryProtocolResponse> {
|
||||||
|
return Promise.reject(new NativeOcctHistoryUnavailableError(this.capabilities()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NativeOcctHistoryExecution = {
|
||||||
|
status: 'completed' | 'cancelled' | 'stale' | 'timed-out'
|
||||||
|
requestId: string
|
||||||
|
response?: NativeOcctHistoryProtocolResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NativeOcctHistoryCoordinator {
|
||||||
|
private active: { requestId: string; controller: AbortController } | null = null
|
||||||
|
private sequence = 0
|
||||||
|
|
||||||
|
constructor(private readonly currentDocumentVersion: (documentId: string) => number | null, private readonly timeoutMs = 120_000) {}
|
||||||
|
|
||||||
|
cancel() { this.active?.controller.abort() }
|
||||||
|
|
||||||
|
async capture(provider: NativeOcctHistoryProvider, input: Omit<NativeOcctHistoryRequest, 'protocolVersion' | 'requestId'>): Promise<NativeOcctHistoryExecution> {
|
||||||
|
this.active?.controller.abort()
|
||||||
|
const requestId = `native-history-${++this.sequence}`
|
||||||
|
const controller = new AbortController()
|
||||||
|
this.active = { requestId, controller }
|
||||||
|
const request: NativeOcctHistoryRequest = { ...input, protocolVersion: NATIVE_OCCT_HISTORY_PROTOCOL_VERSION, requestId }
|
||||||
|
let timeout: ReturnType<typeof setTimeout> | undefined
|
||||||
|
try {
|
||||||
|
const timeoutPromise = new Promise<never>((_, reject) => { timeout = setTimeout(() => { controller.abort(); reject(new DOMException('Native OCCT history request timed out.', 'TimeoutError')) }, this.timeoutMs) })
|
||||||
|
const response = await Promise.race([provider.capture(request, controller.signal), timeoutPromise])
|
||||||
|
if (controller.signal.aborted || this.active?.requestId !== requestId) return { status: 'cancelled', requestId }
|
||||||
|
if (this.currentDocumentVersion(input.documentId) !== input.documentVersion) return { status: 'stale', requestId }
|
||||||
|
return { status: 'completed', requestId, response }
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error && error.name === 'TimeoutError') return { status: 'timed-out', requestId }
|
||||||
|
if (controller.signal.aborted || (error instanceof Error && error.name === 'AbortError')) return { status: 'cancelled', requestId }
|
||||||
|
throw error
|
||||||
|
} finally {
|
||||||
|
if (timeout) clearTimeout(timeout)
|
||||||
|
if (this.active?.requestId === requestId) this.active = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
80
src/facade/nativeHistoryProvider.ts
Normal file
80
src/facade/nativeHistoryProvider.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import type { NativeTopologyHistoryInput, NativeTopologyHistoryRecord, ShapeHandle, SubshapeRef } from './types'
|
||||||
|
|
||||||
|
export type NativeOcctHistoryRecord = {
|
||||||
|
relation: 'modified' | 'generated' | 'deleted'
|
||||||
|
source: 'object' | 'tool'
|
||||||
|
kind: string
|
||||||
|
sourceIndex: number
|
||||||
|
resultIndex: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NativeOcctHistoryResponse = {
|
||||||
|
provider: 'occt-native'
|
||||||
|
occtVersion: string
|
||||||
|
resultStep?: string
|
||||||
|
records: NativeOcctHistoryRecord[]
|
||||||
|
hasModified: boolean
|
||||||
|
hasGenerated: boolean
|
||||||
|
hasDeleted: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NativeOcctHistoryOperation = 'fuse' | 'cut' | 'common'
|
||||||
|
|
||||||
|
export type NativeOcctHistoryStepProvider = {
|
||||||
|
occtVersion(): string
|
||||||
|
booleanHistoryFromStep(objectStep: string, toolStep: string, operation: NativeOcctHistoryOperation): NativeOcctHistoryResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NativeOcctHistoryStepGeometry = {
|
||||||
|
exportStep(shape: ShapeHandle, fileName?: string): Promise<{ format: 'step'; fileName: string; mediaType: string; text: string }>
|
||||||
|
}
|
||||||
|
|
||||||
|
const kinds: SubshapeRef['kind'][] = ['vertex', 'edge', 'face']
|
||||||
|
const isKind = (value: string): value is SubshapeRef['kind'] => kinds.includes(value as SubshapeRef['kind'])
|
||||||
|
|
||||||
|
export const mapNativeOcctHistoryRecords = (
|
||||||
|
response: NativeOcctHistoryResponse,
|
||||||
|
sourceObjectIds: { object: string; tool: string },
|
||||||
|
): NativeTopologyHistoryRecord[] => {
|
||||||
|
if (response.provider !== 'occt-native' || !/^8\./.test(response.occtVersion)) throw new Error('Unsupported native OCCT history provider response.')
|
||||||
|
return response.records.map((record) => {
|
||||||
|
if (!isKind(record.kind)) throw new Error(`Unsupported native OCCT subshape kind: ${record.kind}`)
|
||||||
|
if (!Number.isSafeInteger(record.sourceIndex) || record.sourceIndex < 0) throw new RangeError('Native OCCT sourceIndex must be a non-negative safe integer.')
|
||||||
|
const sourceObjectId = sourceObjectIds[record.source]
|
||||||
|
if (!sourceObjectId) throw new Error(`Missing native OCCT source object ID for ${record.source}.`)
|
||||||
|
if (record.relation === 'deleted') {
|
||||||
|
return { sourceObjectId, sourceKind: record.kind, sourceIndex: record.sourceIndex, relation: 'deleted' }
|
||||||
|
}
|
||||||
|
if (!Number.isSafeInteger(record.resultIndex) || record.resultIndex < 0) throw new RangeError('Native OCCT resultIndex must be a non-negative safe integer.')
|
||||||
|
return {
|
||||||
|
sourceObjectId,
|
||||||
|
sourceKind: record.kind,
|
||||||
|
sourceIndex: record.sourceIndex,
|
||||||
|
relation: record.relation,
|
||||||
|
resultKind: record.kind,
|
||||||
|
resultIndexes: [record.resultIndex],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adapts Bitbybit STEP exports to the standalone OCCT history WASM module.
|
||||||
|
* ShapeHandle values never cross this boundary; only versioned STEP text does.
|
||||||
|
*/
|
||||||
|
export const createNativeOcctStepHistoryBridge = (
|
||||||
|
geometry: NativeOcctHistoryStepGeometry,
|
||||||
|
provider: NativeOcctHistoryStepProvider,
|
||||||
|
) => async (input: NativeTopologyHistoryInput): Promise<NativeTopologyHistoryRecord[]> => {
|
||||||
|
if (!input.operation) throw new Error('Native OCCT STEP history requires a supported Boolean operation.')
|
||||||
|
if (input.inputs.length !== 2) throw new Error('Native OCCT STEP history requires exactly an object and a tool input.')
|
||||||
|
const [objectInput, toolInput] = input.inputs
|
||||||
|
const [objectStep, toolStep] = await Promise.all([
|
||||||
|
geometry.exportStep(objectInput.shape, `${objectInput.objectId}.step`),
|
||||||
|
geometry.exportStep(toolInput.shape, `${toolInput.objectId}.step`),
|
||||||
|
])
|
||||||
|
if (objectStep.format !== 'step' || toolStep.format !== 'step' || typeof objectStep.text !== 'string' || typeof toolStep.text !== 'string') {
|
||||||
|
throw new Error('Bitbybit STEP export returned an invalid transport payload.')
|
||||||
|
}
|
||||||
|
const response = provider.booleanHistoryFromStep(objectStep.text, toolStep.text, input.operation)
|
||||||
|
return mapNativeOcctHistoryRecords(response, { object: objectInput.objectId, tool: toolInput.objectId })
|
||||||
|
}
|
||||||
111
src/facade/nativeHistoryWorkerClient.ts
Normal file
111
src/facade/nativeHistoryWorkerClient.ts
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import type { NativeOcctHistoryResponse, NativeOcctHistoryStepProvider } from './nativeHistoryProvider'
|
||||||
|
import { NATIVE_OCCT_HISTORY_PROTOCOL_VERSION, type NativeOcctHistoryCapabilities, type NativeOcctHistoryProvider, type NativeOcctHistoryProtocolResponse, type NativeOcctHistoryRequest } from './nativeHistoryProtocol'
|
||||||
|
|
||||||
|
type WorkerLike = Pick<Worker, 'postMessage' | 'terminate'> & {
|
||||||
|
addEventListener(type: 'message', listener: (event: MessageEvent) => void): void
|
||||||
|
addEventListener(type: 'error', listener: (event: ErrorEvent) => void): void
|
||||||
|
removeEventListener(type: 'message', listener: (event: MessageEvent) => void): void
|
||||||
|
removeEventListener(type: 'error', listener: (event: ErrorEvent) => void): void
|
||||||
|
}
|
||||||
|
|
||||||
|
type WorkerResponse = { type: 'ready'; capabilities: NativeOcctHistoryCapabilities } | { type: 'response'; response: NativeOcctHistoryProtocolResponse } | { type: 'error'; requestId?: string; error: string }
|
||||||
|
|
||||||
|
const abortError = () => new DOMException('Native OCCT history Worker request cancelled.', 'AbortError')
|
||||||
|
|
||||||
|
export type NativeOcctHistoryWorkerOptions = {
|
||||||
|
moduleUrl?: string
|
||||||
|
initializationTimeoutMs?: number
|
||||||
|
workerFactory?: () => WorkerLike
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultWorkerFactory = () => new Worker(new URL('./nativeHistoryWorkerEntry.ts', import.meta.url), { type: 'module', name: 'occt-native-history' })
|
||||||
|
|
||||||
|
export class NativeOcctHistoryWorkerProvider implements NativeOcctHistoryProvider {
|
||||||
|
private readonly worker: WorkerLike
|
||||||
|
private readonly options: Required<Pick<NativeOcctHistoryWorkerOptions, 'moduleUrl' | 'initializationTimeoutMs'>>
|
||||||
|
private readonly pending = new Map<string, { resolve: (response: NativeOcctHistoryProtocolResponse) => void; reject: (error: Error) => void }>()
|
||||||
|
private initialization: Promise<NativeOcctHistoryCapabilities> | null = null
|
||||||
|
private initializationReject: ((error: Error) => void) | null = null
|
||||||
|
private current: NativeOcctHistoryCapabilities = { providerId: 'occt-native.history-step', providerVersion: 'unavailable', occtVersion: 'unknown', availability: 'unavailable', operations: [], transport: 'step-text', reason: 'Native OCCT history Worker has not been initialized.' }
|
||||||
|
private readonly onMessage = (event: MessageEvent<WorkerResponse>) => {
|
||||||
|
if (event.data.type === 'ready') {
|
||||||
|
this.current = event.data.capabilities
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (event.data.type === 'error') {
|
||||||
|
const requestId = event.data.requestId
|
||||||
|
const pending = requestId ? this.pending.get(requestId) : null
|
||||||
|
if (pending && requestId) { this.pending.delete(requestId); pending.reject(new Error(event.data.error)) }
|
||||||
|
if (!requestId) this.initializationReject?.(new Error(event.data.error))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const pending = this.pending.get(event.data.response.requestId)
|
||||||
|
if (!pending) return
|
||||||
|
this.pending.delete(event.data.response.requestId)
|
||||||
|
pending.resolve(event.data.response)
|
||||||
|
}
|
||||||
|
private readonly onError = (event: ErrorEvent) => {
|
||||||
|
const error = new Error(event.message || 'Native OCCT history Worker failed.')
|
||||||
|
for (const pending of this.pending.values()) pending.reject(error)
|
||||||
|
this.pending.clear()
|
||||||
|
this.initialization = null
|
||||||
|
this.initializationReject?.(error)
|
||||||
|
this.initializationReject = null
|
||||||
|
this.current = { ...this.current, availability: 'unavailable', reason: error.message }
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(options: NativeOcctHistoryWorkerOptions = {}) {
|
||||||
|
this.worker = (options.workerFactory || defaultWorkerFactory)()
|
||||||
|
this.options = {
|
||||||
|
moduleUrl: options.moduleUrl || '/native/occt-history/bitbybit-occt-history.js',
|
||||||
|
initializationTimeoutMs: options.initializationTimeoutMs ?? 120_000,
|
||||||
|
}
|
||||||
|
this.worker.addEventListener('message', this.onMessage)
|
||||||
|
this.worker.addEventListener('error', this.onError)
|
||||||
|
}
|
||||||
|
|
||||||
|
capabilities(): NativeOcctHistoryCapabilities { return { ...this.current, operations: [...this.current.operations] } }
|
||||||
|
|
||||||
|
initialize(): Promise<NativeOcctHistoryCapabilities> {
|
||||||
|
if (this.current.availability === 'available') return Promise.resolve(this.capabilities())
|
||||||
|
if (this.initialization) return this.initialization
|
||||||
|
this.initialization = new Promise<NativeOcctHistoryCapabilities>((resolve, reject) => {
|
||||||
|
let settled = false
|
||||||
|
const timer = setTimeout(() => fail(new Error('Native OCCT history Worker initialization timed out.')), this.options.initializationTimeoutMs)
|
||||||
|
const fail = (error: Error) => { if (settled) return; settled = true; clearTimeout(timer); this.worker.removeEventListener('message', waitForReady); this.initialization = null; this.initializationReject = null; this.current = { ...this.current, reason: error.message }; reject(error) }
|
||||||
|
this.initializationReject = fail
|
||||||
|
const waitForReady = (event: MessageEvent<WorkerResponse>) => { if (event.data.type === 'error') return fail(new Error(event.data.error)); if (event.data.type !== 'ready') return; settled = true; clearTimeout(timer); this.worker.removeEventListener('message', waitForReady); this.initializationReject = null; resolve(this.capabilities()) }
|
||||||
|
this.worker.addEventListener('message', waitForReady)
|
||||||
|
this.worker.postMessage({ type: 'initialize', moduleUrl: this.options.moduleUrl })
|
||||||
|
})
|
||||||
|
return this.initialization
|
||||||
|
}
|
||||||
|
|
||||||
|
async capture(request: NativeOcctHistoryRequest, signal: AbortSignal): Promise<NativeOcctHistoryProtocolResponse> {
|
||||||
|
if (request.protocolVersion !== NATIVE_OCCT_HISTORY_PROTOCOL_VERSION) throw new RangeError(`Unsupported native OCCT history protocol version: ${request.protocolVersion}.`)
|
||||||
|
await this.initialize()
|
||||||
|
if (signal.aborted) throw abortError()
|
||||||
|
const response = new Promise<NativeOcctHistoryProtocolResponse>((resolve, reject) => {
|
||||||
|
this.pending.set(request.requestId, { resolve, reject })
|
||||||
|
this.worker.postMessage({ type: 'capture', request })
|
||||||
|
})
|
||||||
|
const cancel = () => { this.worker.postMessage({ type: 'cancel', requestId: request.requestId }); this.pending.get(request.requestId)?.reject(abortError()); this.pending.delete(request.requestId) }
|
||||||
|
signal.addEventListener('abort', cancel, { once: true })
|
||||||
|
try { return await response } finally { signal.removeEventListener('abort', cancel) }
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose() {
|
||||||
|
this.worker.postMessage({ type: 'dispose' })
|
||||||
|
this.worker.removeEventListener('message', this.onMessage)
|
||||||
|
this.worker.removeEventListener('error', this.onError)
|
||||||
|
this.worker.terminate()
|
||||||
|
this.initializationReject?.(new Error('Native OCCT history Worker disposed.'))
|
||||||
|
this.initializationReject = null
|
||||||
|
for (const pending of this.pending.values()) pending.reject(new Error('Native OCCT history Worker disposed.'))
|
||||||
|
this.pending.clear()
|
||||||
|
this.initialization = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NativeOcctHistoryWorkerModule = NativeOcctHistoryStepProvider & { default?: never }
|
||||||
|
export type NativeOcctHistoryWorkerResponse = NativeOcctHistoryResponse
|
||||||
49
src/facade/nativeHistoryWorkerEntry.ts
Normal file
49
src/facade/nativeHistoryWorkerEntry.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
/// <reference lib="webworker" />
|
||||||
|
|
||||||
|
import type { NativeOcctHistoryStepProvider } from './nativeHistoryProvider'
|
||||||
|
import type { NativeOcctHistoryCapabilities, NativeOcctHistoryRequest, NativeOcctHistoryProtocolResponse } from './nativeHistoryProtocol'
|
||||||
|
|
||||||
|
type WorkerRequest = { type: 'initialize'; moduleUrl: string } | { type: 'capture'; request: NativeOcctHistoryRequest } | { type: 'cancel'; requestId: string } | { type: 'dispose' }
|
||||||
|
type WorkerResponse = { type: 'ready'; capabilities: NativeOcctHistoryCapabilities } | { type: 'response'; response: NativeOcctHistoryProtocolResponse } | { type: 'error'; requestId?: string; error: string }
|
||||||
|
|
||||||
|
const scope = self as DedicatedWorkerGlobalScope
|
||||||
|
let provider: NativeOcctHistoryStepProvider | null = null
|
||||||
|
const cancelled = new Set<string>()
|
||||||
|
|
||||||
|
const send = (message: WorkerResponse) => scope.postMessage(message)
|
||||||
|
|
||||||
|
const initialize = async (moduleUrl: string) => {
|
||||||
|
const imported = await import(/* @vite-ignore */ moduleUrl) as { default?: () => Promise<NativeOcctHistoryStepProvider> }
|
||||||
|
if (typeof imported.default !== 'function') throw new Error('Native OCCT history module has no default Emscripten factory export.')
|
||||||
|
provider = await imported.default()
|
||||||
|
send({
|
||||||
|
type: 'ready',
|
||||||
|
capabilities: {
|
||||||
|
providerId: 'occt-native.history-step',
|
||||||
|
providerVersion: '8.0.0-embind',
|
||||||
|
occtVersion: provider.occtVersion(),
|
||||||
|
availability: 'available',
|
||||||
|
operations: ['fuse', 'cut', 'common'],
|
||||||
|
transport: 'step-text',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
scope.onmessage = ({ data }: MessageEvent<WorkerRequest>) => {
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
|
if (data.type === 'initialize') return await initialize(data.moduleUrl)
|
||||||
|
if (data.type === 'dispose') { provider = null; scope.close(); return }
|
||||||
|
if (data.type === 'cancel') { cancelled.add(data.requestId); return }
|
||||||
|
if (!provider) throw new Error('Native OCCT history Worker is not initialized.')
|
||||||
|
if (cancelled.delete(data.request.requestId)) return
|
||||||
|
const history = provider.booleanHistoryFromStep(data.request.objectStep, data.request.toolStep, data.request.operation)
|
||||||
|
if (cancelled.delete(data.request.requestId)) return
|
||||||
|
send({ type: 'response', response: { protocolVersion: data.request.protocolVersion, requestId: data.request.requestId, documentId: data.request.documentId, documentVersion: data.request.documentVersion, operationId: data.request.operationId, provider: { providerId: 'occt-native.history-step', providerVersion: '8.0.0-embind', occtVersion: history.occtVersion, availability: 'available', operations: ['fuse', 'cut', 'common'], transport: 'step-text' }, history } })
|
||||||
|
} catch (error) {
|
||||||
|
send({ type: 'error', requestId: data.type === 'capture' ? data.request.requestId : undefined, error: error instanceof Error ? error.message : String(error) })
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
}
|
||||||
|
|
||||||
|
export {}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { DependencyGraph, type RecomputeState } from './dependencyGraph'
|
import { DependencyGraph, type RecomputeState } from './dependencyGraph'
|
||||||
import { cloneSketch, solveSketch } from './sketcher'
|
import { cloneSketch, solveSketch } from './sketcher'
|
||||||
import type { ApplyPlacementInput, BooleanCutInput, BooleanIntersectionInput, BooleanUnionInput, ChamferInput, CreateBoxInput, CreateConeInput, CreateCylinderInput, CreateSphereInput, DocumentObjectSnapshot, DocumentSnapshot, FilletInput, MirrorInput, MultiTransformStep, ObjectTopologySnapshot, PadInput, PlanarProfile, PocketInput, RevolutionInput, ShapeHandle, SubshapeTopology } from './types'
|
import type { ApplyPlacementInput, BooleanCutInput, BooleanIntersectionInput, BooleanUnionInput, ChamferInput, CreateBoxInput, CreateConeInput, CreateCylinderInput, CreateSphereInput, DocumentObjectSnapshot, DocumentSnapshot, FilletInput, MirrorInput, MultiTransformStep, ObjectTopologySnapshot, PadInput, PlanarProfile, PocketInput, RevolutionInput, ShapeHandle, SubshapeTopology } from './types'
|
||||||
import { captureSignatureTopologyHistory } from './topologyHistory'
|
import { captureNativeTopologyHistory, captureSignatureTopologyHistory } from './topologyHistory'
|
||||||
import { migrateTopoRefs } from './topologyReferences'
|
import { migrateTopoRefs } from './topologyReferences'
|
||||||
|
|
||||||
export type RecomputeExecutionStatus = 'completed' | 'failed' | 'cancelled' | 'stale'
|
export type RecomputeExecutionStatus = 'completed' | 'failed' | 'cancelled' | 'stale'
|
||||||
@@ -49,6 +49,7 @@ export type RecomputeGeometryRuntime = {
|
|||||||
fillet(input: FilletInput): Promise<ShapeHandle>
|
fillet(input: FilletInput): Promise<ShapeHandle>
|
||||||
chamfer(input: ChamferInput): Promise<ShapeHandle>
|
chamfer(input: ChamferInput): Promise<ShapeHandle>
|
||||||
topology?(shape: ShapeHandle, precision?: number): Promise<SubshapeTopology>
|
topology?(shape: ShapeHandle, precision?: number): Promise<SubshapeTopology>
|
||||||
|
topologyHistory?(input: { documentId: string; documentVersion: number; operationId: string; operation?: 'fuse' | 'cut' | 'common'; inputs: Array<{ objectId: string; shape: ShapeHandle }>; result: ShapeHandle }): Promise<import('./types').NativeTopologyHistoryRecord[]>
|
||||||
release(shape: ShapeHandle): Promise<void>
|
release(shape: ShapeHandle): Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,6 +304,8 @@ const placementForObject = (object: DocumentObjectSnapshot) => {
|
|||||||
const topologyForObject = async (
|
const topologyForObject = async (
|
||||||
geometry: RecomputeGeometryRuntime,
|
geometry: RecomputeGeometryRuntime,
|
||||||
object: DocumentObjectSnapshot,
|
object: DocumentObjectSnapshot,
|
||||||
|
document: DocumentSnapshot,
|
||||||
|
shapes: Map<string, ShapeHandle>,
|
||||||
shape: ShapeHandle,
|
shape: ShapeHandle,
|
||||||
context: RecomputeNodeContext,
|
context: RecomputeNodeContext,
|
||||||
): Promise<ObjectTopologySnapshot | undefined> => {
|
): Promise<ObjectTopologySnapshot | undefined> => {
|
||||||
@@ -319,11 +322,28 @@ const topologyForObject = async (
|
|||||||
},
|
},
|
||||||
signature: { ...entry.signature, centroid: [...entry.signature.centroid], bounds: { min: [...entry.signature.bounds.min], max: [...entry.signature.bounds.max] }, normal: [...entry.signature.normal] },
|
signature: { ...entry.signature, centroid: [...entry.signature.centroid], bounds: { min: [...entry.signature.bounds.min], max: [...entry.signature.bounds.max] }, normal: [...entry.signature.normal] },
|
||||||
})) as ObjectTopologySnapshot['entries']
|
})) as ObjectTopologySnapshot['entries']
|
||||||
const history = captureSignatureTopologyHistory(
|
const operationId = `${object.id}:generation:${context.generation}`
|
||||||
`${object.id}:generation:${context.generation}`,
|
const linkedSources = ['Base', 'Tool', 'Profile']
|
||||||
previous.length > 0 ? [{ objectId: object.id, entries: previous }] : [],
|
.map((propertyName) => linkedObject(object, propertyName, document))
|
||||||
entries,
|
.filter((candidate): candidate is DocumentObjectSnapshot => Boolean(candidate))
|
||||||
)
|
const sourceObjects = [...new Map(linkedSources.map((source) => [source.id, source])).values()]
|
||||||
|
const nativeInputs = sourceObjects
|
||||||
|
.map((source) => ({ source, shape: shapes.get(source.id) }))
|
||||||
|
.filter((entry): entry is { source: DocumentObjectSnapshot; shape: ShapeHandle } => Boolean(entry.shape && entry.source.topology?.entries.length))
|
||||||
|
const nativeOperation = object.typeId === 'Part::Fuse' ? 'fuse' : object.typeId === 'Part::Cut' ? 'cut' : object.typeId === 'Part::Common' ? 'common' : undefined
|
||||||
|
const nativeRecords = geometry.topologyHistory && nativeOperation && nativeInputs.length > 0
|
||||||
|
? await geometry.topologyHistory({
|
||||||
|
documentId: context.documentId,
|
||||||
|
documentVersion: context.documentVersion,
|
||||||
|
operationId,
|
||||||
|
operation: nativeOperation,
|
||||||
|
inputs: nativeInputs.map(({ source, shape: inputShape }) => ({ objectId: source.id, shape: inputShape })),
|
||||||
|
result: shape,
|
||||||
|
})
|
||||||
|
: undefined
|
||||||
|
const history = nativeRecords
|
||||||
|
? captureNativeTopologyHistory(operationId, nativeInputs.map(({ source }) => ({ objectId: source.id, entries: source.topology!.entries })), entries, nativeRecords)
|
||||||
|
: captureSignatureTopologyHistory(operationId, previous.length > 0 ? [{ objectId: object.id, entries: previous }] : [], entries)
|
||||||
return {
|
return {
|
||||||
shapeId: shape.id,
|
shapeId: shape.id,
|
||||||
documentVersion: context.documentVersion,
|
documentVersion: context.documentVersion,
|
||||||
@@ -597,7 +617,7 @@ export const createFacadeGeometryRecomputeExecutor = (
|
|||||||
}
|
}
|
||||||
let topology: ObjectTopologySnapshot | undefined
|
let topology: ObjectTopologySnapshot | undefined
|
||||||
try {
|
try {
|
||||||
topology = await topologyForObject(geometry, object, result, context)
|
topology = await topologyForObject(geometry, object, document, shapes, result, context)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await Promise.allSettled([geometry.release(result)])
|
await Promise.allSettled([geometry.release(result)])
|
||||||
throw error
|
throw error
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { SubshapeRef, TopologyHistoryRelation as StoredTopologyHistoryRelation, TopologyHistoryResult as StoredTopologyHistoryResult } from './types'
|
import type { NativeTopologyHistoryRecord, SubshapeRef, TopologyHistoryRelation as StoredTopologyHistoryRelation, TopologyHistoryResult as StoredTopologyHistoryResult } from './types'
|
||||||
import { matchSubshapes, type SubshapeSignature } from './topologyNaming'
|
import { matchSubshapes, type SubshapeSignature } from './topologyNaming'
|
||||||
|
|
||||||
export type TopologyHistoryEntry = { ref: SubshapeRef; signature: SubshapeSignature }
|
export type TopologyHistoryEntry = { ref: SubshapeRef; signature: SubshapeSignature }
|
||||||
@@ -7,6 +7,53 @@ export type TopologyHistoryRelation = StoredTopologyHistoryRelation
|
|||||||
export type TopologyHistoryResult = StoredTopologyHistoryResult
|
export type TopologyHistoryResult = StoredTopologyHistoryResult
|
||||||
|
|
||||||
const sourceKey = (objectId: string, persistentId: string) => `${encodeURIComponent(objectId)}::${persistentId}`
|
const sourceKey = (objectId: string, persistentId: string) => `${encodeURIComponent(objectId)}::${persistentId}`
|
||||||
|
const emptyCounts = (): TopologyHistoryResult['counts'] => ({ preserved: 0, modified: 0, generated: 0, deleted: 0, ambiguous: 0 })
|
||||||
|
const entriesOfKind = (entries: TopologyHistoryEntry[], kind: SubshapeRef['kind']) => entries.filter((entry) => entry.ref.kind === kind)
|
||||||
|
|
||||||
|
export const captureNativeTopologyHistory = (
|
||||||
|
operationId: string,
|
||||||
|
inputs: Array<{ objectId: string; entries: TopologyHistoryEntry[] }>,
|
||||||
|
output: TopologyHistoryEntry[],
|
||||||
|
records: NativeTopologyHistoryRecord[],
|
||||||
|
): TopologyHistoryResult => {
|
||||||
|
if (!operationId.trim()) throw new RangeError('Topology history operationId is required.')
|
||||||
|
const inputById = new Map(inputs.map((input) => [input.objectId, input]))
|
||||||
|
if (inputById.size !== inputs.length) throw new RangeError('Native topology history input object IDs must be unique.')
|
||||||
|
const counts = emptyCounts()
|
||||||
|
const relations: TopologyHistoryRelation[] = []
|
||||||
|
const seen = new Set<string>()
|
||||||
|
|
||||||
|
for (const record of records) {
|
||||||
|
const input = inputById.get(record.sourceObjectId)
|
||||||
|
if (!input) throw new RangeError(`Native topology history references unknown input object ${record.sourceObjectId}.`)
|
||||||
|
const sources = entriesOfKind(input.entries, record.sourceKind)
|
||||||
|
if (!Number.isSafeInteger(record.sourceIndex) || record.sourceIndex < 0 || record.sourceIndex >= sources.length) throw new RangeError(`Native topology history source index is out of range for ${record.sourceObjectId} ${record.sourceKind}.`)
|
||||||
|
const source = sources[record.sourceIndex]
|
||||||
|
if (record.relation === 'deleted') {
|
||||||
|
if (record.resultIndexes?.length) throw new RangeError('Deleted native topology history records cannot reference result indexes.')
|
||||||
|
const key = `${record.sourceObjectId}:${source.ref.persistentId}:deleted`
|
||||||
|
if (!seen.has(key)) {
|
||||||
|
seen.add(key)
|
||||||
|
relations.push({ relation: 'deleted', sourceObjectId: record.sourceObjectId, sourcePersistentId: source.ref.persistentId, score: 1 })
|
||||||
|
counts.deleted += 1
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const resultKind = record.resultKind ?? record.sourceKind
|
||||||
|
const results = entriesOfKind(output, resultKind)
|
||||||
|
if (!record.resultIndexes?.length) throw new RangeError(`${record.relation} native topology history records require result indexes.`)
|
||||||
|
for (const resultIndex of record.resultIndexes) {
|
||||||
|
if (!Number.isSafeInteger(resultIndex) || resultIndex < 0 || resultIndex >= results.length) throw new RangeError(`Native topology history result index is out of range for ${resultKind}.`)
|
||||||
|
const result = results[resultIndex]
|
||||||
|
const key = `${record.sourceObjectId}:${source.ref.persistentId}:${record.relation}:${result.ref.persistentId}`
|
||||||
|
if (seen.has(key)) continue
|
||||||
|
seen.add(key)
|
||||||
|
relations.push({ relation: record.relation, sourceObjectId: record.sourceObjectId, sourcePersistentId: source.ref.persistentId, resultPersistentId: result.ref.persistentId, score: 1 })
|
||||||
|
counts[record.relation] += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { operationId, provider: 'occt-native', relations, counts }
|
||||||
|
}
|
||||||
|
|
||||||
export const captureSignatureTopologyHistory = (
|
export const captureSignatureTopologyHistory = (
|
||||||
operationId: string,
|
operationId: string,
|
||||||
@@ -21,7 +68,7 @@ export const captureSignatureTopologyHistory = (
|
|||||||
return { ref: { ...entry.ref, persistentId: key }, signature: entry.signature }
|
return { ref: { ...entry.ref, persistentId: key }, signature: entry.signature }
|
||||||
}))
|
}))
|
||||||
const matches = matchSubshapes(previous, output)
|
const matches = matchSubshapes(previous, output)
|
||||||
const counts: TopologyHistoryResult['counts'] = { preserved: 0, modified: 0, generated: 0, deleted: 0, ambiguous: 0 }
|
const counts = emptyCounts()
|
||||||
const relations = matches.map((match): TopologyHistoryRelation => {
|
const relations = matches.map((match): TopologyHistoryRelation => {
|
||||||
if (match.status === 'new') return { relation: 'generated', resultPersistentId: match.current.persistentId, score: match.score }
|
if (match.status === 'new') return { relation: 'generated', resultPersistentId: match.current.persistentId, score: match.score }
|
||||||
if (match.status === 'deleted') {
|
if (match.status === 'deleted') {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type { Quantity, QuantityDimension } from './units'
|
|||||||
import type { SketchConstraint, SketchExternalGeometry, SketchGeometry, SketchSnapshot, SketchSolveResult } from './sketcher'
|
import type { SketchConstraint, SketchExternalGeometry, SketchGeometry, SketchSnapshot, SketchSolveResult } from './sketcher'
|
||||||
import type { RecomputeExecutionOptions, RecomputeExecutionResult } from './recomputeEngine'
|
import type { RecomputeExecutionOptions, RecomputeExecutionResult } from './recomputeEngine'
|
||||||
import type { FcstdArchiveLimits, FcstdInspection } from './fcstd'
|
import type { FcstdArchiveLimits, FcstdInspection } from './fcstd'
|
||||||
|
import type { NativeOcctHistoryCapabilities, NativeOcctHistoryProvider } from './nativeHistoryProtocol'
|
||||||
|
|
||||||
export type ModelTreeItem = {
|
export type ModelTreeItem = {
|
||||||
id: string
|
id: string
|
||||||
@@ -221,11 +222,27 @@ export type TopologyHistoryRelation = {
|
|||||||
|
|
||||||
export type TopologyHistoryResult = {
|
export type TopologyHistoryResult = {
|
||||||
operationId: string
|
operationId: string
|
||||||
provider: 'signature-fallback'
|
provider: 'occt-native' | 'signature-fallback'
|
||||||
relations: TopologyHistoryRelation[]
|
relations: TopologyHistoryRelation[]
|
||||||
counts: Record<TopologyHistoryRelation['relation'], number>
|
counts: Record<TopologyHistoryRelation['relation'], number>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type NativeTopologyHistoryRecord = {
|
||||||
|
sourceObjectId: string
|
||||||
|
sourceKind: SubshapeRef['kind']
|
||||||
|
sourceIndex: number
|
||||||
|
relation: 'preserved' | 'modified' | 'generated' | 'deleted'
|
||||||
|
resultKind?: SubshapeRef['kind']
|
||||||
|
resultIndexes?: number[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NativeTopologyHistoryInput = GeometryDocumentContext & {
|
||||||
|
operationId: string
|
||||||
|
operation?: 'fuse' | 'cut' | 'common'
|
||||||
|
inputs: Array<{ objectId: string; shape: ShapeHandle }>
|
||||||
|
result: ShapeHandle
|
||||||
|
}
|
||||||
|
|
||||||
export type ObjectTopologySnapshot = {
|
export type ObjectTopologySnapshot = {
|
||||||
shapeId: string
|
shapeId: string
|
||||||
documentVersion: number
|
documentVersion: number
|
||||||
@@ -555,6 +572,8 @@ export interface BitBybitWebCadFacade {
|
|||||||
}
|
}
|
||||||
readonly geometry: {
|
readonly geometry: {
|
||||||
capabilities(): GeometryCapabilities
|
capabilities(): GeometryCapabilities
|
||||||
|
configureNativeHistory?(provider: NativeOcctHistoryProvider | null, timeoutMs?: number): void
|
||||||
|
nativeHistoryCapabilities?(): NativeOcctHistoryCapabilities
|
||||||
initialize(): Promise<GeometryCapabilities>
|
initialize(): Promise<GeometryCapabilities>
|
||||||
createBox(input: CreateBoxInput): Promise<ShapeHandle>
|
createBox(input: CreateBoxInput): Promise<ShapeHandle>
|
||||||
createCylinder(input: CreateCylinderInput): Promise<ShapeHandle>
|
createCylinder(input: CreateCylinderInput): Promise<ShapeHandle>
|
||||||
@@ -575,6 +594,7 @@ export interface BitBybitWebCadFacade {
|
|||||||
mesh(shape: ShapeHandle, precision?: number): Promise<MeshAsset>
|
mesh(shape: ShapeHandle, precision?: number): Promise<MeshAsset>
|
||||||
subshapes(shape: ShapeHandle, precision?: number): Promise<SubshapeRef[]>
|
subshapes(shape: ShapeHandle, precision?: number): Promise<SubshapeRef[]>
|
||||||
topology(shape: ShapeHandle, precision?: number): Promise<SubshapeTopology>
|
topology(shape: ShapeHandle, precision?: number): Promise<SubshapeTopology>
|
||||||
|
topologyHistory?(input: NativeTopologyHistoryInput): Promise<NativeTopologyHistoryRecord[]>
|
||||||
getObjectShape(objectId: string): ShapeHandle | null
|
getObjectShape(objectId: string): ShapeHandle | null
|
||||||
release(shape: ShapeHandle): Promise<void>
|
release(shape: ShapeHandle): Promise<void>
|
||||||
dispose(): void
|
dispose(): void
|
||||||
|
|||||||
@@ -8,13 +8,16 @@ import { DependencyGraph } from '../src/facade/dependencyGraph'
|
|||||||
import { evaluateQuantityExpression, quantityFromNumber } from '../src/facade/units'
|
import { evaluateQuantityExpression, quantityFromNumber } from '../src/facade/units'
|
||||||
import { createEdgeSubshapeRefs, createSubshapeRefs, createVertexSubshapeRefs, matchSubshapes, signatureForEdge, signatureForFace, signatureForVertex } from '../src/facade/topologyNaming'
|
import { createEdgeSubshapeRefs, createSubshapeRefs, createVertexSubshapeRefs, matchSubshapes, signatureForEdge, signatureForFace, signatureForVertex } from '../src/facade/topologyNaming'
|
||||||
import { createPersistedTopoRef, migrateDocumentTopologyReferences, migrateTopoRefs, parseTopoRef, resolveDocumentTopologyReference, resolveTopoRef, serializeTopoRef } from '../src/facade/topologyReferences'
|
import { createPersistedTopoRef, migrateDocumentTopologyReferences, migrateTopoRefs, parseTopoRef, resolveDocumentTopologyReference, resolveTopoRef, serializeTopoRef } from '../src/facade/topologyReferences'
|
||||||
import { captureSignatureTopologyHistory } from '../src/facade/topologyHistory'
|
import { captureNativeTopologyHistory, captureSignatureTopologyHistory } from '../src/facade/topologyHistory'
|
||||||
|
import { createNativeOcctStepHistoryBridge, mapNativeOcctHistoryRecords } from '../src/facade/nativeHistoryProvider'
|
||||||
|
import { DirectNativeOcctHistoryProvider, NativeOcctHistoryCoordinator, NATIVE_OCCT_HISTORY_PROTOCOL_VERSION, type NativeOcctHistoryProvider } from '../src/facade/nativeHistoryProtocol'
|
||||||
|
import { NativeOcctHistoryWorkerProvider } from '../src/facade/nativeHistoryWorkerClient'
|
||||||
import { assessResourceQuota, planResourceSweep } from '../src/facade/resourcePolicy'
|
import { assessResourceQuota, planResourceSweep } from '../src/facade/resourcePolicy'
|
||||||
import { cloneSketch, createSketch, solveSketch } from '../src/facade/sketcher'
|
import { cloneSketch, createSketch, solveSketch } from '../src/facade/sketcher'
|
||||||
import { BasicSketchSolverProvider, SKETCH_SOLVER_PROTOCOL_VERSION, SketchSolverCoordinator, SketchSolverUnavailableError, UnavailablePlanegcsProvider, runSketchSolverReplay, type SketchSolverProvider, type SketchSolverRequest } from '../src/facade/sketchSolverProtocol'
|
import { BasicSketchSolverProvider, SKETCH_SOLVER_PROTOCOL_VERSION, SketchSolverCoordinator, SketchSolverUnavailableError, UnavailablePlanegcsProvider, runSketchSolverReplay, type SketchSolverProvider, type SketchSolverRequest } from '../src/facade/sketchSolverProtocol'
|
||||||
import { createFacadeGeometryRecomputeExecutor, executeFacadeRecomputeNode, RecomputeCoordinator, type RecomputeGeometryRuntime } from '../src/facade/recomputeEngine'
|
import { createFacadeGeometryRecomputeExecutor, executeFacadeRecomputeNode, RecomputeCoordinator, type RecomputeGeometryRuntime } from '../src/facade/recomputeEngine'
|
||||||
import { inspectFcstdArchive } from '../src/facade/fcstd'
|
import { inspectFcstdArchive } from '../src/facade/fcstd'
|
||||||
import { assertShapeHandleIntegrity, normalizeBitbybitMesh, validateBooleanUnionInput, validateBoxInput, validateChamferInput, validateConeInput, validateCylinderInput, validateFilletInput, validateMirrorInput, validatePadInput, validatePlacementInput, validatePlanarProfile, validateRevolutionInput, validateSphereInput } from '../src/facade/geometryRuntime'
|
import { assertShapeHandleIntegrity, BitbybitGeometryRuntime, normalizeBitbybitMesh, validateBooleanUnionInput, validateBoxInput, validateChamferInput, validateConeInput, validateCylinderInput, validateFilletInput, validateMirrorInput, validatePadInput, validatePlacementInput, validatePlanarProfile, validateRevolutionInput, validateSphereInput } from '../src/facade/geometryRuntime'
|
||||||
import type { DocumentObjectSnapshot, DocumentSnapshot, MultiTransformValue, ObjectTopologySnapshot, ShapeHandle, SubshapeTopology } from '../src/facade/types'
|
import type { DocumentObjectSnapshot, DocumentSnapshot, MultiTransformValue, ObjectTopologySnapshot, ShapeHandle, SubshapeTopology } from '../src/facade/types'
|
||||||
|
|
||||||
const recomputeDocumentFixture = (edges: DocumentSnapshot['dependencies'] = []): DocumentSnapshot => ({
|
const recomputeDocumentFixture = (edges: DocumentSnapshot['dependencies'] = []): DocumentSnapshot => ({
|
||||||
@@ -226,6 +229,194 @@ test('signature topology history conservatively classifies boolean fallback rela
|
|||||||
assert.deepEqual(new Set(ambiguous.relations[0].candidates?.map((candidate) => candidate.sourceObjectId)), new Set(['base', 'tool']))
|
assert.deepEqual(new Set(ambiguous.relations[0].candidates?.map((candidate) => candidate.sourceObjectId)), new Set(['base', 'tool']))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('native topology history maps OCCT relation indexes without transient references', () => {
|
||||||
|
const face = { vertexCoord: [0, 0, 0, 2, 0, 0, 2, 2, 0, 0, 2, 0], normalCoord: [], triIndexes: [0, 1, 2, 0, 2, 3] }
|
||||||
|
const secondFace = { vertexCoord: [0, 0, 1, 2, 0, 1, 2, 2, 1, 0, 2, 1], normalCoord: [], triIndexes: [0, 1, 2, 0, 2, 3] }
|
||||||
|
const input = createSubshapeRefs('native-input', 1, [face, secondFace])
|
||||||
|
const output = createSubshapeRefs('native-output', 2, [face])
|
||||||
|
const result = captureNativeTopologyHistory(
|
||||||
|
'boolean-native',
|
||||||
|
[{ objectId: 'base', entries: input.refs.map((ref, index) => ({ ref, signature: input.signatures[index] })) }],
|
||||||
|
output.refs.map((ref, index) => ({ ref, signature: output.signatures[index] })),
|
||||||
|
[
|
||||||
|
{ sourceObjectId: 'base', sourceKind: 'face', sourceIndex: 0, relation: 'modified', resultIndexes: [0] },
|
||||||
|
{ sourceObjectId: 'base', sourceKind: 'face', sourceIndex: 1, relation: 'deleted' },
|
||||||
|
],
|
||||||
|
)
|
||||||
|
assert.equal(result.provider, 'occt-native')
|
||||||
|
assert.equal(result.counts.modified, 1)
|
||||||
|
assert.equal(result.counts.deleted, 1)
|
||||||
|
assert.equal(result.relations[0].sourcePersistentId, input.refs[0].persistentId)
|
||||||
|
assert.equal(result.relations[0].resultPersistentId, output.refs[0].persistentId)
|
||||||
|
assert.equal(result.relations[1].sourcePersistentId, input.refs[1].persistentId)
|
||||||
|
assert.throws(() => captureNativeTopologyHistory('boolean-native', [{ objectId: 'base', entries: [] }], [], [{ sourceObjectId: 'base', sourceKind: 'face', sourceIndex: 0, relation: 'deleted' }]), /out of range/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('native OCCT provider protocol maps worker records to facade source IDs', () => {
|
||||||
|
const records = mapNativeOcctHistoryRecords({
|
||||||
|
provider: 'occt-native',
|
||||||
|
occtVersion: '8.0.0',
|
||||||
|
hasModified: true,
|
||||||
|
hasGenerated: false,
|
||||||
|
hasDeleted: true,
|
||||||
|
records: [
|
||||||
|
{ relation: 'modified', source: 'object', kind: 'face', sourceIndex: 0, resultIndex: 1 },
|
||||||
|
{ relation: 'deleted', source: 'tool', kind: 'face', sourceIndex: 2, resultIndex: -1 },
|
||||||
|
],
|
||||||
|
}, { object: 'base', tool: 'tool' })
|
||||||
|
assert.deepEqual(records, [
|
||||||
|
{ sourceObjectId: 'base', sourceKind: 'face', sourceIndex: 0, relation: 'modified', resultKind: 'face', resultIndexes: [1] },
|
||||||
|
{ sourceObjectId: 'tool', sourceKind: 'face', sourceIndex: 2, relation: 'deleted' },
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('native OCCT STEP bridge serializes handles before invoking the provider', async () => {
|
||||||
|
const calls: string[] = []
|
||||||
|
const bridge = createNativeOcctStepHistoryBridge(
|
||||||
|
{ exportStep: async (shape) => { calls.push(shape.id); return { format: 'step', fileName: `${shape.id}.step`, mediaType: 'application/step', text: `ISO-10303-21; ${shape.id}` } } },
|
||||||
|
{
|
||||||
|
occtVersion: () => '8.0.0',
|
||||||
|
booleanHistoryFromStep: (objectStep, toolStep, operation) => {
|
||||||
|
assert.match(objectStep, /base-shape/)
|
||||||
|
assert.match(toolStep, /tool-shape/)
|
||||||
|
assert.equal(operation, 'cut')
|
||||||
|
return { provider: 'occt-native', occtVersion: '8.0.0', hasModified: true, hasGenerated: false, hasDeleted: false, records: [{ relation: 'modified', source: 'object', kind: 'face', sourceIndex: 0, resultIndex: 0 }] }
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
const records = await bridge({ documentId: 'doc', documentVersion: 1, operationId: 'cut-1', operation: 'cut', inputs: [
|
||||||
|
{ objectId: 'base', shape: { id: 'base-shape', kernel: 'bitbybit-occt', kind: 'solid', documentId: 'doc', documentVersion: 1 } },
|
||||||
|
{ objectId: 'tool', shape: { id: 'tool-shape', kernel: 'bitbybit-occt', kind: 'solid', documentId: 'doc', documentVersion: 1 } },
|
||||||
|
], result: { id: 'result-shape', kernel: 'bitbybit-occt', kind: 'solid', documentId: 'doc', documentVersion: 1 } })
|
||||||
|
assert.deepEqual(calls, ['base-shape', 'tool-shape'])
|
||||||
|
assert.deepEqual(records, [{ sourceObjectId: 'base', sourceKind: 'face', sourceIndex: 0, relation: 'modified', resultKind: 'face', resultIndexes: [0] }])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('native OCCT history protocol validates STEP context and isolates stale generations', async () => {
|
||||||
|
const provider = new DirectNativeOcctHistoryProvider({
|
||||||
|
occtVersion: () => '8.0.0',
|
||||||
|
booleanHistoryFromStep: (objectStep, toolStep, operation) => ({
|
||||||
|
provider: 'occt-native', occtVersion: '8.0.0', hasModified: true, hasGenerated: false, hasDeleted: false,
|
||||||
|
resultStep: objectStep, records: [{ relation: operation === 'cut' ? 'modified' : 'generated', source: 'object', kind: 'face', sourceIndex: 0, resultIndex: 0 }],
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
assert.deepEqual(provider.capabilities(), { providerId: 'occt-native.history-step', providerVersion: '8.0.0-embind', occtVersion: '8.0.0', availability: 'available', operations: ['fuse', 'cut', 'common'], transport: 'step-text' })
|
||||||
|
const coordinator = new NativeOcctHistoryCoordinator(() => 2)
|
||||||
|
const execution = await coordinator.capture(provider, { documentId: 'doc', documentVersion: 2, operationId: 'cut-1', operation: 'cut', objectStep: 'ISO-10303-21; object', toolStep: 'ISO-10303-21; tool' })
|
||||||
|
assert.equal(execution.status, 'completed')
|
||||||
|
assert.equal(execution.response?.protocolVersion, NATIVE_OCCT_HISTORY_PROTOCOL_VERSION)
|
||||||
|
assert.equal(execution.response?.history.records[0].relation, 'modified')
|
||||||
|
await assert.rejects(() => provider.capture({ protocolVersion: NATIVE_OCCT_HISTORY_PROTOCOL_VERSION, requestId: 'bad', documentId: 'doc', documentVersion: 2, operationId: 'bad', operation: 'cut', objectStep: 'not-step', toolStep: 'ISO-10303-21; tool' }, new AbortController().signal), /STEP text/)
|
||||||
|
const stale = new NativeOcctHistoryCoordinator(() => 3)
|
||||||
|
const staleExecution = await stale.capture(provider, { documentId: 'doc', documentVersion: 2, operationId: 'cut-2', operation: 'cut', objectStep: 'ISO-10303-21; object', toolStep: 'ISO-10303-21; tool' })
|
||||||
|
assert.equal(staleExecution.status, 'stale')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('native OCCT history coordinator reports timeout and cancellation', async () => {
|
||||||
|
const hanging: NativeOcctHistoryProvider = { capabilities: () => ({ providerId: 'test', providerVersion: '1', occtVersion: '8.0.0', availability: 'available', operations: ['cut'], transport: 'step-text' }), capture: async (_request, signal) => await new Promise<never>((_, reject) => { signal.addEventListener('abort', () => reject(new DOMException('cancelled', 'AbortError')), { once: true }) }) }
|
||||||
|
const timedOut = await new NativeOcctHistoryCoordinator(() => 1, 5).capture(hanging, { documentId: 'doc', documentVersion: 1, operationId: 'cut-timeout', operation: 'cut', objectStep: 'ISO-10303-21; object', toolStep: 'ISO-10303-21; tool' })
|
||||||
|
assert.equal(timedOut.status, 'timed-out')
|
||||||
|
const coordinator = new NativeOcctHistoryCoordinator(() => 1, 120_000)
|
||||||
|
const pending = coordinator.capture(hanging, { documentId: 'doc', documentVersion: 1, operationId: 'cut-cancel', operation: 'cut', objectStep: 'ISO-10303-21; object', toolStep: 'ISO-10303-21; tool' })
|
||||||
|
coordinator.cancel()
|
||||||
|
assert.equal((await pending).status, 'cancelled')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('native OCCT Worker provider preserves versioned request and response context', async () => {
|
||||||
|
const messageListeners = new Set<(event: MessageEvent) => void>()
|
||||||
|
const errorListeners = new Set<(event: ErrorEvent) => void>()
|
||||||
|
const fakeWorker = {
|
||||||
|
postMessage(message: { type: string; request?: { requestId: string; protocolVersion: number; documentId: string; documentVersion: number; operationId: string; objectStep: string; toolStep: string; operation: 'cut' } }) {
|
||||||
|
if (message.type === 'initialize') {
|
||||||
|
queueMicrotask(() => messageListeners.forEach((listener) => listener({ data: { type: 'ready', capabilities: { providerId: 'occt-native.history-step', providerVersion: '8.0.0-embind', occtVersion: '8.0.0', availability: 'available', operations: ['fuse', 'cut', 'common'], transport: 'step-text' } } } as MessageEvent)))
|
||||||
|
} else if (message.type === 'capture' && message.request) {
|
||||||
|
const request = message.request
|
||||||
|
const response = {
|
||||||
|
protocolVersion: request.protocolVersion,
|
||||||
|
requestId: request.requestId,
|
||||||
|
documentId: request.documentId,
|
||||||
|
documentVersion: request.documentVersion,
|
||||||
|
operationId: request.operationId,
|
||||||
|
provider: { providerId: 'occt-native.history-step', providerVersion: '8.0.0-embind', occtVersion: '8.0.0', availability: 'available', operations: ['fuse', 'cut', 'common'], transport: 'step-text' },
|
||||||
|
history: { provider: 'occt-native', occtVersion: '8.0.0', hasModified: true, hasGenerated: false, hasDeleted: false, records: [] },
|
||||||
|
}
|
||||||
|
queueMicrotask(() => messageListeners.forEach((listener) => listener({ data: { type: 'response', response } } as MessageEvent)))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
terminate() {},
|
||||||
|
addEventListener(type: 'message' | 'error', listener: (event: MessageEvent | ErrorEvent) => void) { (type === 'message' ? messageListeners : errorListeners).add(listener as never) },
|
||||||
|
removeEventListener(type: 'message' | 'error', listener: (event: MessageEvent | ErrorEvent) => void) { (type === 'message' ? messageListeners : errorListeners).delete(listener as never) },
|
||||||
|
}
|
||||||
|
const provider = new NativeOcctHistoryWorkerProvider({ workerFactory: () => fakeWorker })
|
||||||
|
const request = { protocolVersion: NATIVE_OCCT_HISTORY_PROTOCOL_VERSION, requestId: 'worker-1', documentId: 'doc', documentVersion: 4, operationId: 'cut-worker', operation: 'cut' as const, objectStep: 'ISO-10303-21; object', toolStep: 'ISO-10303-21; tool' }
|
||||||
|
const response = await provider.capture(request, new AbortController().signal)
|
||||||
|
assert.equal(response.requestId, request.requestId)
|
||||||
|
assert.equal(response.documentVersion, request.documentVersion)
|
||||||
|
assert.equal(provider.capabilities().availability, 'available')
|
||||||
|
provider.dispose()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('native OCCT Worker provider propagates AbortSignal cancellation to the worker', async () => {
|
||||||
|
const messageListeners = new Set<(event: MessageEvent) => void>()
|
||||||
|
const cancelRequests: string[] = []
|
||||||
|
const fakeWorker = {
|
||||||
|
postMessage(message: { type: string; requestId?: string }) {
|
||||||
|
if (message.type === 'initialize') {
|
||||||
|
queueMicrotask(() => messageListeners.forEach((listener) => listener({ data: { type: 'ready', capabilities: { providerId: 'occt-native.history-step', providerVersion: '8.0.0-embind', occtVersion: '8.0.0', availability: 'available', operations: ['cut'], transport: 'step-text' } } } as MessageEvent)))
|
||||||
|
} else if (message.type === 'cancel' && message.requestId) cancelRequests.push(message.requestId)
|
||||||
|
},
|
||||||
|
terminate() {},
|
||||||
|
addEventListener(type: 'message' | 'error', listener: (event: MessageEvent | ErrorEvent) => void) { if (type === 'message') messageListeners.add(listener as never) },
|
||||||
|
removeEventListener(type: 'message' | 'error', listener: (event: MessageEvent | ErrorEvent) => void) { if (type === 'message') messageListeners.delete(listener as never) },
|
||||||
|
}
|
||||||
|
const provider = new NativeOcctHistoryWorkerProvider({ workerFactory: () => fakeWorker })
|
||||||
|
const request = { protocolVersion: NATIVE_OCCT_HISTORY_PROTOCOL_VERSION, requestId: 'worker-cancel-1', documentId: 'doc', documentVersion: 4, operationId: 'cut-worker-cancel', operation: 'cut' as const, objectStep: 'ISO-10303-21; object', toolStep: 'ISO-10303-21; tool' }
|
||||||
|
const controller = new AbortController()
|
||||||
|
const pending = provider.capture(request, controller.signal)
|
||||||
|
await new Promise<void>((resolve) => queueMicrotask(resolve))
|
||||||
|
controller.abort()
|
||||||
|
await assert.rejects(pending, (error: unknown) => error instanceof DOMException && error.name === 'AbortError')
|
||||||
|
assert.deepEqual(cancelRequests, [request.requestId])
|
||||||
|
provider.dispose()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Bitbybit geometry runtime routes configured native history through STEP and the facade mapper', async () => {
|
||||||
|
const runtime = new BitbybitGeometryRuntime()
|
||||||
|
runtime.exportStep = async (shape, fileName = `${shape.id}.step`) => ({ format: 'step', fileName, mediaType: 'application/step', text: `ISO-10303-21; ${shape.id}` })
|
||||||
|
let capturedRequestId = ''
|
||||||
|
const provider: NativeOcctHistoryProvider = {
|
||||||
|
capabilities: () => ({ providerId: 'occt-native.history-step', providerVersion: '8.0.0-embind', occtVersion: '8.0.0', availability: 'available', operations: ['cut'], transport: 'step-text' }),
|
||||||
|
capture: async (request) => {
|
||||||
|
capturedRequestId = request.requestId
|
||||||
|
assert.match(request.objectStep, /base/)
|
||||||
|
assert.match(request.toolStep, /tool/)
|
||||||
|
return {
|
||||||
|
protocolVersion: NATIVE_OCCT_HISTORY_PROTOCOL_VERSION,
|
||||||
|
requestId: request.requestId,
|
||||||
|
documentId: request.documentId,
|
||||||
|
documentVersion: request.documentVersion,
|
||||||
|
operationId: request.operationId,
|
||||||
|
provider: provider.capabilities(),
|
||||||
|
history: { provider: 'occt-native', occtVersion: '8.0.0', hasModified: true, hasGenerated: false, hasDeleted: true, records: [
|
||||||
|
{ relation: 'modified', source: 'object', kind: 'face', sourceIndex: 0, resultIndex: 0 },
|
||||||
|
{ relation: 'deleted', source: 'tool', kind: 'edge', sourceIndex: 1 },
|
||||||
|
] },
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
runtime.configureNativeHistory(provider, 1000)
|
||||||
|
const handle = (id: string): ShapeHandle => ({ id, kernel: 'bitbybit-occt', kind: 'solid', documentId: 'doc', documentVersion: 3 })
|
||||||
|
const records = await runtime.topologyHistory({ documentId: 'doc', documentVersion: 3, operationId: 'cut-runtime', operation: 'cut', inputs: [{ objectId: 'base', shape: handle('base') }, { objectId: 'tool', shape: handle('tool') }], result: handle('result') })
|
||||||
|
assert.match(capturedRequestId, /^native-history-/)
|
||||||
|
assert.deepEqual(records, [
|
||||||
|
{ sourceObjectId: 'base', sourceKind: 'face', sourceIndex: 0, relation: 'modified', resultKind: 'face', resultIndexes: [0] },
|
||||||
|
{ sourceObjectId: 'tool', sourceKind: 'edge', sourceIndex: 1, relation: 'deleted' },
|
||||||
|
])
|
||||||
|
runtime.configureNativeHistory(null)
|
||||||
|
assert.equal(runtime.nativeHistoryCapabilities().availability, 'unavailable')
|
||||||
|
runtime.dispose()
|
||||||
|
})
|
||||||
|
|
||||||
test('edge and vertex topology signatures are orientation/index independent', () => {
|
test('edge and vertex topology signatures are orientation/index independent', () => {
|
||||||
const edge = signatureForEdge([0, 0, 0], [2, 0, 0])
|
const edge = signatureForEdge([0, 0, 0], [2, 0, 0])
|
||||||
assert.equal(edge.hash, signatureForEdge([2, 0, 0], [0, 0, 0]).hash)
|
assert.equal(edge.hash, signatureForEdge([2, 0, 0], [0, 0, 0]).hash)
|
||||||
@@ -1489,6 +1680,7 @@ test('FCStd inspection reports recognized, proxy, and Python-backed objects with
|
|||||||
assert.equal(inspection.label, 'Imported assembly')
|
assert.equal(inspection.label, 'Imported assembly')
|
||||||
assert.equal(inspection.schemaVersion, '4')
|
assert.equal(inspection.schemaVersion, '4')
|
||||||
assert.equal(inspection.objects.find((object) => object.name === 'Body')?.label, 'Main body')
|
assert.equal(inspection.objects.find((object) => object.name === 'Body')?.label, 'Main body')
|
||||||
|
assert.deepEqual(inspection.objects.find((object) => object.name === 'Body')?.properties, [{ name: 'Label', typeId: 'App::PropertyString', element: 'String', value: 'Main body' }])
|
||||||
assert.equal(inspection.compatibility.recognizedObjects, 1)
|
assert.equal(inspection.compatibility.recognizedObjects, 1)
|
||||||
assert.equal(inspection.compatibility.proxyObjects, 1)
|
assert.equal(inspection.compatibility.proxyObjects, 1)
|
||||||
assert.equal(inspection.compatibility.blockedObjects, 1)
|
assert.equal(inspection.compatibility.blockedObjects, 1)
|
||||||
|
|||||||
Reference in New Issue
Block a user