feat: advance FreeCAD parity verification and runtime boundaries
Some checks failed
real-verification / chrome (push) Has been cancelled
real-verification / freecad-oracle (push) Has been cancelled
real-verification / wasm (push) Has been cancelled

This commit is contained in:
2026-08-11 02:14:45 -04:00
parent 26ea9b57d7
commit 2be18a511e
31 changed files with 451 additions and 57 deletions

View File

@@ -23,9 +23,32 @@ jobs:
node-version: 22.23.2 node-version: 22.23.2
cache: npm cache: npm
- run: ./npmw ci --ignore-scripts - run: ./npmw ci --ignore-scripts
- run: ./npmw run ci:real -- --lane=chrome - name: Run real Chrome harnesses
run: ./npmw run ci:real:chrome
env: env:
CHROME_BIN: ${{ vars.CHROME_BIN }} CHROME_BIN: ${{ vars.CHROME_BIN }}
- name: Check generated Chrome reports
run: ./npmw run ci:check:chrome
env:
CHROME_BIN: ${{ vars.CHROME_BIN }}
- name: Run Firefox and WebKit matrix
run: ./npmw run test:browser-matrix
env:
FIREFOX_BIN: ${{ vars.FIREFOX_BIN }}
WEBKIT_BIN: ${{ vars.WEBKIT_BIN }}
- name: Check Firefox and WebKit reports
run: ./npmw run check:browser-matrix
- name: Upload Chrome evidence
if: always()
uses: actions/upload-artifact@v4
with:
name: chrome-real-verification-${{ github.run_id }}
if-no-files-found: warn
path: |
.cache/ci/real-verification*.json
config/chrome-*-verification.json
config/browser-matrix-verification.json
fixtures/chrome-app-e2e/*.png
freecad-oracle: freecad-oracle:
runs-on: [self-hosted, linux, cad] runs-on: [self-hosted, linux, cad]

View File

@@ -0,0 +1,45 @@
{
"schemaVersion": 1,
"generatedAt": "2026-08-11T04:50:53.969Z",
"status": "pass",
"browsers": [
{
"engine": "firefox",
"status": "pass",
"userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:153.0) Gecko/20100101 Firefox/153.0",
"crossOriginIsolated": true,
"sharedArrayBuffer": true,
"opfs": true,
"persistence": "opfs",
"shell": true,
"workspace": true,
"workbench": "Part Design",
"canvas": {
"width": 732,
"height": 579,
"geometrySource": "bitbybit-occt"
},
"screenshotBytes": 131545,
"pageErrors": []
},
{
"engine": "webkit",
"status": "pass",
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.5 Safari/605.1.15",
"crossOriginIsolated": true,
"sharedArrayBuffer": true,
"opfs": false,
"persistence": "memory-fallback",
"shell": true,
"workspace": true,
"workbench": "Part Design",
"canvas": {
"width": 732,
"height": 579,
"geometrySource": "bitbybit-occt"
},
"screenshotBytes": 128023,
"pageErrors": []
}
]
}

View File

@@ -1,6 +1,6 @@
{ {
"schemaVersion": 1, "schemaVersion": 1,
"generatedAt": "2026-08-08T17:54:57.664Z", "generatedAt": "2026-08-11T02:37:40.468Z",
"browserId": "chrome", "browserId": "chrome",
"status": "pass", "status": "pass",
"browser": { "browser": {
@@ -423,18 +423,18 @@
"screenshots": { "screenshots": {
"desktop": { "desktop": {
"path": "fixtures/chrome-app-e2e/desktop-workspace.png", "path": "fixtures/chrome-app-e2e/desktop-workspace.png",
"byteLength": 108999, "byteLength": 108743,
"sha256": "33456e593549e88e561d1168eb80644b362fd941725bf4fff76500a14a6d7949" "sha256": "dd67ab23283ffd55dc0509174f8636eac5344de581d4cacfff13cc1828617399"
}, },
"cam": { "cam": {
"path": "fixtures/chrome-app-e2e/cam-workbench.png", "path": "fixtures/chrome-app-e2e/cam-workbench.png",
"byteLength": 118390, "byteLength": 118389,
"sha256": "5349262af666380bbc96f2932eed2613547ae60870a1d8b9fe44d8ed4831b4ab" "sha256": "c339e4b998b8ce7b3290c32efeffebed85dbd1d56d8ae9f764196c7b52e80a8f"
}, },
"mobile": { "mobile": {
"path": "fixtures/chrome-app-e2e/mobile-start.png", "path": "fixtures/chrome-app-e2e/mobile-start.png",
"byteLength": 130367, "byteLength": 130371,
"sha256": "c3873002bf12dde3a7731a511eb651880a6733ce68b149167d1ff84d6e436b69" "sha256": "9d69c9e9ced4b45bbd083a37a94b5df074e8d60540c0857e79b9df08a8648d81"
} }
}, },
"pageErrors": [] "pageErrors": []

View File

@@ -2,7 +2,7 @@
"schemaVersion": 1, "schemaVersion": 1,
"freecadBaseline": "freecad-1.1.1", "freecadBaseline": "freecad-1.1.1",
"workbenches": { "workbenches": {
"Part Design": { "status": "facade-partial", "level": "experimental", "commands": ["create-body", "create-sketch", "pad", "pocket", "revolution", "fillet", "chamfer", "mirrored", "multi-transform", "linear-pattern", "polar-pattern"] }, "Part Design": { "status": "facade-partial", "level": "experimental", "commands": ["create-body", "create-sketch", "shape-binder", "datum", "pad", "pocket", "revolution", "fillet", "chamfer", "mirrored", "multi-transform", "linear-pattern", "polar-pattern"] },
"Part": { "status": "facade-partial", "level": "experimental", "commands": ["primitive", "union", "cut", "intersection", "check-shape"] }, "Part": { "status": "facade-partial", "level": "experimental", "commands": ["primitive", "union", "cut", "intersection", "check-shape"] },
"Sketcher": { "status": "facade-model", "level": "experimental", "commands": ["new-sketch", "line", "arc", "circle", "rectangle", "constrain-horizontal", "constrain-vertical", "solve-sketch"] }, "Sketcher": { "status": "facade-model", "level": "experimental", "commands": ["new-sketch", "line", "arc", "circle", "rectangle", "constrain-horizontal", "constrain-vertical", "solve-sketch"] },
"Draft": { "status": "domain-contract", "level": "experimental", "commands": ["working-plane", "snap-settings", "draft-line", "draft-wire", "draft-circle", "move", "rotate", "scale", "offset", "array", "clone", "layer"] }, "Draft": { "status": "domain-contract", "level": "experimental", "commands": ["working-plane", "snap-settings", "draft-line", "draft-wire", "draft-circle", "move", "rotate", "scale", "offset", "array", "clone", "layer"] },

View File

@@ -24,7 +24,7 @@
"wrong-document-root": 77 "wrong-document-root": 77
}, },
"timing": { "timing": {
"p95Ms": 0.121, "p95Ms": 0.137,
"maxMs": 2.672 "maxMs": 2.691
} }
} }

View File

@@ -41,7 +41,7 @@
"zero-revolution-axis": 67 "zero-revolution-axis": 67
}, },
"timing": { "timing": {
"p95Ms": 0.021, "p95Ms": 0.02,
"maxMs": 1.033 "maxMs": 1.133
} }
} }

View File

@@ -22,7 +22,7 @@
"invalid": 800 "invalid": 800
}, },
"timing": { "timing": {
"p95Ms": 0.03, "p95Ms": 0.032,
"maxMs": 1.012 "maxMs": 0.733
} }
} }

View File

@@ -22,20 +22,20 @@
"bytes": 556597, "bytes": 556597,
"sha256": "1679307fb0f01e9d7b9f37a081458bf44583e4d1899edf9224b49a1435ce7105" "sha256": "1679307fb0f01e9d7b9f37a081458bf44583e4d1899edf9224b49a1435ce7105"
}, },
{
"path": "assets/index-BcH6J3tB.js",
"bytes": 1103011,
"sha256": "870d33fae0ceabaf31a8100e65ad7f1afb3f4f869bfc225bb8c722d7771ca917"
},
{ {
"path": "assets/index-BnRqhr3m.css", "path": "assets/index-BnRqhr3m.css",
"bytes": 65976, "bytes": 65976,
"sha256": "b8eacf86224d2705e3c27367b3a0b166f4d662a5b8142d34a8abcdeef58753ec" "sha256": "b8eacf86224d2705e3c27367b3a0b166f4d662a5b8142d34a8abcdeef58753ec"
}, },
{ {
"path": "assets/index-CBxL0K57.js", "path": "assets/nativeHistoryWorkerEntry-D9i46nqJ.js",
"bytes": 1099755, "bytes": 4664,
"sha256": "02858b3fb3a849f18e45dc5ccd906d54db30abe46352ab6d2483a405a9185a56" "sha256": "488e45728dd4ccfd65acb82c61266332171129a52e916ecbb7cc2a8ab61d4ed6"
},
{
"path": "assets/nativeHistoryWorkerEntry-HtQ0QZhi.js",
"bytes": 4544,
"sha256": "31eec7608dd0a500340319a7f14f3f9ec83ecd9cb4104a00eb6c77329c406e8c"
}, },
{ {
"path": "assets/persistenceWorker-CyD5EUVX.js", "path": "assets/persistenceWorker-CyD5EUVX.js",
@@ -330,7 +330,7 @@
{ {
"path": "index.html", "path": "index.html",
"bytes": 678, "bytes": 678,
"sha256": "56c68b39c24935e08673f86ae825539f4f1f58e1fcc4232f64472c8466b444c9" "sha256": "07fab5a0e83597c5ca0a0cd2d57625f0134b56c02f2349a35be4e6de23540d33"
}, },
{ {
"path": "manifest.webmanifest", "path": "manifest.webmanifest",
@@ -380,7 +380,7 @@
{ {
"path": "sw.js", "path": "sw.js",
"bytes": 1390, "bytes": 1390,
"sha256": "52a07bb516143ea6578fbf346881b2e7140fbc8c85d4492bf72fb88b8a66c95b" "sha256": "01593ec533053a9ccdf341ef276ee5c0e1752e6dc30951acdc042c33d60f206c"
}, },
{ {
"path": "vendor/camotics/camotics-sweep.wasm", "path": "vendor/camotics/camotics-sweep.wasm",
@@ -402,7 +402,7 @@
"status": "signed", "status": "signed",
"algorithm": "Ed25519", "algorithm": "Ed25519",
"keyId": "bitbybit-local-release-2026", "keyId": "bitbybit-local-release-2026",
"payloadSha256": "87baf721899f6acb4c5ea8436f8d6e53de3dc5937431ea572c3df33f09c3576e", "payloadSha256": "f1fe903dc9879b0f02d1a4becbd5daf7fb9d7b96ef304f2cc04fc5058d80256e",
"signature": "9/KPdUGbb0FmGTMftU1k2rbwJ5rjbq3nDHZLLjY0tFfreKuPFfy9mByuHHsUSQIz5Fx0CVJO+Xai/qXjrCNBBw==" "signature": "RY1mpA6T/zWYx1/Z/9Z7A2kpkBbbCbP8TQIIykaFbY2E8qzmlAw0qotNYq3yOP5OfbJqas+BjAXWJeEY7WA4DQ=="
} }
} }

View File

@@ -8,7 +8,9 @@
./npmw run ci:real -- --lane=wasm ./npmw run ci:real -- --lane=wasm
``` ```
`--lane=all` 会按 oracle、WASM、Chrome 顺序执行全部 lane。任一命令退出非零都会使整个 lane 失败;运行摘要写入 `.cache/ci/real-verification.json`,该文件不作为仓库基线证据提交 Chrome 还提供显式的两阶段入口:`./npmw run ci:real:chrome` 只启动真实 Chrome harness 并生成报告,`./npmw run ci:check:chrome` 只读取并校验这些报告。CI 会按这两个步骤执行并上传 JSON/截图证据;因此历史报告检查不会被误认为重新执行了浏览器。随后 `test:browser-matrix` / `check:browser-matrix` 会用 Playwright 的 Firefox 和 WebKit 对生产构建执行 shell、OCCT canvas、COOP/COEP、SharedArrayBuffer 和持久化能力 smoke gateFirefox 强制 OPFSWebKit 缺少 OPFS 时必须明确报告 `memory-fallback`
`--lane=all` 会按 oracle、WASM、Chrome 顺序执行全部 lane。任一命令退出非零都会使整个 lane 失败;完整运行摘要写入 `.cache/ci/real-verification.json`,分阶段运行分别写入 `real-verification-execute.json``real-verification-check.json`,这些文件不作为仓库基线证据提交。
oracle lane 默认会重新抓取锁定的 FreeCAD commit 并初始化子模块。仅在本机已经具备该 commit、且需要离线复现时才可显式使用 `FREECAD_SOURCE_OFFLINE=1`;该模式会拒绝未锁定的 checkout并跳过网络相关子模块更新不会替代 CI 的真实 fetch。 oracle lane 默认会重新抓取锁定的 FreeCAD commit 并初始化子模块。仅在本机已经具备该 commit、且需要离线复现时才可显式使用 `FREECAD_SOURCE_OFFLINE=1`;该模式会拒绝未锁定的 checkout并跳过网络相关子模块更新不会替代 CI 的真实 fetch。
@@ -17,7 +19,7 @@ oracle lane 默认会重新抓取锁定的 FreeCAD commit 并初始化子模块
工作流使用标签为 `self-hosted, linux, cad` 的 runner因为 FreeCAD 原生编译和外围 CAM harness 需要较大的本地工具链。runner 必须提供: 工作流使用标签为 `self-hosted, linux, cad` 的 runner因为 FreeCAD 原生编译和外围 CAM harness 需要较大的本地工具链。runner 必须提供:
- Node `22.23.2`、CMake、Ninja、C/C++ 编译器和 Python - Node `22.23.2`、CMake、Ninja、C/C++ 编译器和 Python
- Chrome/Chromium通过 `CHROME_BIN` 指定)以及可用的无头模式; - Chrome/Chromium通过 `CHROME_BIN` 指定、Firefox 和 WebKit缓存版本不匹配时分别通过 `FIREFOX_BIN``WEBKIT_BIN` 指定)以及可用的无头模式;
- Emscripten通过 `EMSDK` 指定)和 OCCT 源码目录(通过 `OCCT_SOURCE_DIR` 指定); - Emscripten通过 `EMSDK` 指定)和 OCCT 源码目录(通过 `OCCT_SOURCE_DIR` 指定);
- `CAMotics/``OpenCAMLib/``cnc_wams_gpt6/``working_ocl/` 外部工作树。它们不进入 Git版本由 `config/` 中的锁定清单和 drift check 校验; - `CAMotics/``OpenCAMLib/``cnc_wams_gpt6/``working_ocl/` 外部工作树。它们不进入 Git版本由 `config/` 中的锁定清单和 drift check 校验;
- FreeCAD 构建所需的系统开发包。`ci:real -- --lane=oracle` 会重新抓取锁定的 FreeCAD 源码、配置、编译、安装并执行 oracle不接受旧报告代替。 - FreeCAD 构建所需的系统开发包。`ci:real -- --lane=oracle` 会重新抓取锁定的 FreeCAD 源码、配置、编译、安装并执行 oracle不接受旧报告代替。

View File

@@ -182,7 +182,7 @@ OwnerIO。GateG5。
| FC-10 | IO+ORACLEFreeCAD→Web→FreeCAD 核心/代理报告 | 无静默丢失;差异全部分类 | | FC-10 | IO+ORACLEFreeCAD→Web→FreeCAD 核心/代理报告 | 无静默丢失;差异全部分类 |
| FC-11 | IO+QUALITY大文件、损坏、取消、中断和恢复 | 能力内不崩溃;超限明确拒绝 | | FC-11 | IO+QUALITY大文件、损坏、取消、中断和恢复 | 能力内不崩溃;超限明确拒绝 |
P06 已由 `check:fcstd-closure` 聚合关闭FreeCAD 1.1.1 原生双向 22 场景没有未知差异,未知 proxy 归档逐字节保持Chrome 锁定 Expression/Link/LinkSubList/TopoRef/Sketcher/GuiDocument/BRep/ElementMap 语义。100 模型 FCStd+BRep 以及 STEP/IGES/BREP 三格式质量、毫米单位和容差矩阵全部通过DXF/SVG/PDF、OBJ/PLY/STL、IFC、CSV 八种次级格式分别具有成功、错误签名拒绝和字节精确回环。FCStd ZIP/XML/stream 限额、损坏、取消与 checkpoint 恢复由 `check:fcstd-hardening` 锁定。 P06 已由 `check:fcstd-closure` 聚合关闭FreeCAD 1.1.1 原生双向 23 场景没有未知差异,未知 proxy 归档逐字节保持Chrome 锁定 Expression/Link/LinkSubList/TopoRef/Sketcher/GuiDocument/BRep/ElementMap 语义。100 模型 FCStd+BRep 以及 STEP/IGES/BREP 三格式质量、毫米单位和容差矩阵全部通过DXF/SVG/PDF、OBJ/PLY/STL、IFC、CSV 八种次级格式分别具有成功、错误签名拒绝和字节精确回环。FCStd ZIP/XML/stream 限额、损坏、取消与 checkpoint 恢复由 `check:fcstd-hardening` 锁定。
### P07 TechDraw、Spreadsheet、Draft、Plot ### P07 TechDraw、Spreadsheet、Draft、Plot
@@ -327,7 +327,7 @@ Points、ReverseEngineering、OpenSCAD、IDF、JT、Material、Import 七个专
## 16. P10 QA 与发布闭包证据 ## 16. P10 QA 与发布闭包证据
QA-01/02 由 24 个测试套件、217 个 Node 用例、100 个成功/50 个失败 FreeCAD golden、19 类 Sketcher 成功/失败 oracle、17 个 PartDesign oracle 和 22 个 FCStd 双向 round-trip 场景关闭。QA-03 的 Chrome 150 生产构建回放 8 个核心流程,桌面/移动截图均为有效 PNG初始几何预览来自 Bitbybit OCCT任务态 canvas 仍通过像素和 WebGL 检查。 QA-01/02 由 24 个测试套件、217 个 Node 用例、100 个成功/50 个失败 FreeCAD golden、19 类 Sketcher 成功/失败 oracle、17 个 PartDesign oracle 和 23 个 FCStd 双向 round-trip 场景关闭。QA-03 的 Chrome 150 生产构建回放 8 个核心流程,桌面/移动截图均为有效 PNG初始几何预览来自 Bitbybit OCCT任务态 canvas 仍通过像素和 WebGL 检查。
QA-04 三套固定种子 fuzz 覆盖 1000 个 FCStd parser、2000 个 geometry input、2000 个 Sketch solver 模型,均有正/负分类、输入不变性、确定性和 P95。QA-05 覆盖 1000 对象、1000000 三角形、100000 表格单元Chrome 堆约 35 MBQA-06 覆盖 Worker/WASM 取消、崩溃、恢复、stale、OPFS 迁移和事务回滚QA-07 覆盖路径/归档/XML/权限、Addon 和 Script、CSP/依赖审计。 QA-04 三套固定种子 fuzz 覆盖 1000 个 FCStd parser、2000 个 geometry input、2000 个 Sketch solver 模型,均有正/负分类、输入不变性、确定性和 P95。QA-05 覆盖 1000 对象、1000000 三角形、100000 表格单元Chrome 堆约 35 MBQA-06 覆盖 Worker/WASM 取消、崩溃、恢复、stale、OPFS 迁移和事务回滚QA-07 覆盖路径/归档/XML/权限、Addon 和 Script、CSP/依赖审计。

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

After

Width:  |  Height:  |  Size: 127 KiB

View File

@@ -255,6 +255,10 @@
"test:golden:freecad:families": "node scripts/run-freecad-golden.mjs --manifest=fixtures/freecad-golden/feature-families/manifest.json", "test:golden:freecad:families": "node scripts/run-freecad-golden.mjs --manifest=fixtures/freecad-golden/feature-families/manifest.json",
"probe:freecad-reference": "node scripts/run-freecad-reference-probe.mjs", "probe:freecad-reference": "node scripts/run-freecad-reference-probe.mjs",
"ci:real": "node scripts/run-real-verification.mjs", "ci:real": "node scripts/run-real-verification.mjs",
"ci:real:chrome": "node scripts/run-real-verification.mjs --lane=chrome --phase=execute",
"ci:check:chrome": "node scripts/run-real-verification.mjs --lane=chrome --phase=check",
"test:browser-matrix": "node scripts/run-browser-matrix.mjs",
"check:browser-matrix": "node scripts/check-browser-matrix.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:bitbybit-history && npm run check:execution-plan && npm run check:freecad-sketcher-constraints && npm run check:freecad-inventory && npm run check:freecad-type-properties && npm run check:freecad-gui-commands && npm run check:freecad-entrypoints && npm run check:freecad-golden-fixtures && npm run check:freecad-semantic-comparator && npm run check:freecad-fcstd-roundtrip && npm run check:freecad-composite-history-elementmap && npm run check:freecad-exact-history-elementmap-gate && npm run check:freecad-native-naming-evidence && npm run check:fcstd-hardening && npm run check:occt-history-artifact && npm run check:planegcs-artifact && npm run check:browser-occt && npm run check:chrome-topology && npm run check:chrome-planegcs && npm run check:chrome-fault-injection && npm run check:chrome-opfs-migration && npm run check:chrome-persistence && npm run check:chrome-app-e2e && npm run check:chrome-partdesign-lifecycle && npm run check:pwa && npm run check:chrome-offline && npm run check:chrome-geometry-features && npm run check:chrome-native-history && npm run check:chrome-native-pad-history && npm run check:chrome-native-pocket-history && npm run check:chrome-native-loft-history && npm run check:chrome-native-pipe-history && npm run check:chrome-native-revolution-history && npm run check:chrome-native-groove-history && npm run check:chrome-native-fillet-history && npm run check:chrome-native-chamfer-history && npm run check:chrome-native-hole-history && npm run check:chrome-native-draft-history && npm run check:chrome-native-thickness-history && npm run check:chrome-native-linear-pattern-history && npm run check:chrome-native-polar-pattern-history && npm run check:chrome-native-mirrored-history && npm run check:chrome-native-multi-transform-history && npm run check:chrome-sketcher-diagnostics && npm run check:chrome-profile-validation && npm run check:chrome-sketcher-editor && npm run check:chrome-sketcher-bspline && npm run check:chrome-sketcher-stress && npm run check:chrome-part-primitives && npm run check:chrome-partdesign-transform && npm run check:chrome-partdesign-loft && npm run check:partdesign-closure && npm run check:chrome-fcstd-semantic && npm run check:chrome-fcstd-roundtrip && npm run check:fcstd-closure && npm run check:chrome-spreadsheet && npm run check:chrome-plot && npm run check:chrome-draft && npm run check:chrome-techdraw && npm run check:chrome-assembly && npm run check:chrome-mesh && npm run check:chrome-inspection && npm run check:chrome-bim && npm run check:chrome-cam && npm run check:cam-parity && npm run check:cam-pipeline && npm run check:opencamlib-wasm && npm run check:camotics-native && npm run check:camotics-gui-tpl && npm run check:camotics-wasm && npm run check:chrome-camotics-wasm && npm run check:chrome-cam-native-simulation && npm run check:chrome-cam-linuxcnc-machine && npm run check:chrome-fem && npm run check:chrome-robot && npm run check:chrome-data && npm run check:chrome-addon && npm run check:chrome-script && npm run check:chrome-surface && npm run check:chrome-qa08 && npm run check:chrome-secondary-formats && npm run check:chrome-performance && npm run check:chrome-production-document && npm run check:document-closure && npm run check:chrome-engineering && npm run check:chrome-security && npm run check:chrome-fcstd-golden && npm run check:sbom && npm run check:project-migration && npm run check:security-baseline && npm run check:dependency-audit && npm run check:freecad:threeway && npm run check:occt-upstream-drift && npm run check:facade-boundary && npm run test:golden && npm run test:spreadsheet && npm run test:plot && npm run test:draft && npm run test:techdraw && npm run test:assembly && npm run test:mesh && npm run test:inspection && npm run test:bim && npm run test:cam && npm run test:camotics-wasm && npm run test:fem && npm run test:robot && npm run test:data && npm run test:addon && npm run test:script && npm run test:surface && npm run test:locale && npm run test:secondary-formats && npm run test:performance && npm run test:production-document && npm run test:engineering && npm run test:security-preflight && npm run test:fcstd-roundtrip && npm run test:facade && npm run test:fcstd-fuzz && npm run test:geometry-input-fuzz && npm run test:sketch-solver-fuzz && npm run check:quality-closure && npm run test:planegcs && npm run test:project-migration && npm run test:sketcher-stress && npm run test:topology-replay && npm run build && npm run check:built-pwa && npm run generate:release-artifacts && npm run check:release-artifacts && npm run check:release-closure && npm run test:release-signature", "verify": "npm run check:runtime && npm run check:baseline && npm run check:bitbybit-history && npm run check:execution-plan && npm run check:freecad-sketcher-constraints && npm run check:freecad-inventory && npm run check:freecad-type-properties && npm run check:freecad-gui-commands && npm run check:freecad-entrypoints && npm run check:freecad-golden-fixtures && npm run check:freecad-semantic-comparator && npm run check:freecad-fcstd-roundtrip && npm run check:freecad-composite-history-elementmap && npm run check:freecad-exact-history-elementmap-gate && npm run check:freecad-native-naming-evidence && npm run check:fcstd-hardening && npm run check:occt-history-artifact && npm run check:planegcs-artifact && npm run check:browser-occt && npm run check:chrome-topology && npm run check:chrome-planegcs && npm run check:chrome-fault-injection && npm run check:chrome-opfs-migration && npm run check:chrome-persistence && npm run check:chrome-app-e2e && npm run check:chrome-partdesign-lifecycle && npm run check:pwa && npm run check:chrome-offline && npm run check:chrome-geometry-features && npm run check:chrome-native-history && npm run check:chrome-native-pad-history && npm run check:chrome-native-pocket-history && npm run check:chrome-native-loft-history && npm run check:chrome-native-pipe-history && npm run check:chrome-native-revolution-history && npm run check:chrome-native-groove-history && npm run check:chrome-native-fillet-history && npm run check:chrome-native-chamfer-history && npm run check:chrome-native-hole-history && npm run check:chrome-native-draft-history && npm run check:chrome-native-thickness-history && npm run check:chrome-native-linear-pattern-history && npm run check:chrome-native-polar-pattern-history && npm run check:chrome-native-mirrored-history && npm run check:chrome-native-multi-transform-history && npm run check:chrome-sketcher-diagnostics && npm run check:chrome-profile-validation && npm run check:chrome-sketcher-editor && npm run check:chrome-sketcher-bspline && npm run check:chrome-sketcher-stress && npm run check:chrome-part-primitives && npm run check:chrome-partdesign-transform && npm run check:chrome-partdesign-loft && npm run check:partdesign-closure && npm run check:chrome-fcstd-semantic && npm run check:chrome-fcstd-roundtrip && npm run check:fcstd-closure && npm run check:chrome-spreadsheet && npm run check:chrome-plot && npm run check:chrome-draft && npm run check:chrome-techdraw && npm run check:chrome-assembly && npm run check:chrome-mesh && npm run check:chrome-inspection && npm run check:chrome-bim && npm run check:chrome-cam && npm run check:cam-parity && npm run check:cam-pipeline && npm run check:opencamlib-wasm && npm run check:camotics-native && npm run check:camotics-gui-tpl && npm run check:camotics-wasm && npm run check:chrome-camotics-wasm && npm run check:chrome-cam-native-simulation && npm run check:chrome-cam-linuxcnc-machine && npm run check:chrome-fem && npm run check:chrome-robot && npm run check:chrome-data && npm run check:chrome-addon && npm run check:chrome-script && npm run check:chrome-surface && npm run check:chrome-qa08 && npm run check:chrome-secondary-formats && npm run check:chrome-performance && npm run check:chrome-production-document && npm run check:document-closure && npm run check:chrome-engineering && npm run check:chrome-security && npm run check:chrome-fcstd-golden && npm run check:sbom && npm run check:project-migration && npm run check:security-baseline && npm run check:dependency-audit && npm run check:freecad:threeway && npm run check:occt-upstream-drift && npm run check:facade-boundary && npm run test:golden && npm run test:spreadsheet && npm run test:plot && npm run test:draft && npm run test:techdraw && npm run test:assembly && npm run test:mesh && npm run test:inspection && npm run test:bim && npm run test:cam && npm run test:camotics-wasm && npm run test:fem && npm run test:robot && npm run test:data && npm run test:addon && npm run test:script && npm run test:surface && npm run test:locale && npm run test:secondary-formats && npm run test:performance && npm run test:production-document && npm run test:engineering && npm run test:security-preflight && npm run test:fcstd-roundtrip && npm run test:facade && npm run test:fcstd-fuzz && npm run test:geometry-input-fuzz && npm run test:sketch-solver-fuzz && npm run check:quality-closure && npm run test:planegcs && npm run test:project-migration && npm run test:sketcher-stress && npm run test:topology-replay && npm run build && npm run check:built-pwa && npm run generate:release-artifacts && npm run check:release-artifacts && npm run check:release-closure && npm run test:release-signature",
"test:chrome-native-thickness-history": "node scripts/run-chrome-native-thickness-history.mjs", "test:chrome-native-thickness-history": "node scripts/run-chrome-native-thickness-history.mjs",

View File

@@ -0,0 +1,13 @@
import { readFile } from 'node:fs/promises'
import { resolve } from 'node:path'
const root = resolve(import.meta.dirname, '..')
const report = JSON.parse(await readFile(resolve(root, 'config/browser-matrix-verification.json'), 'utf8'))
const expected = ['firefox', 'webkit']
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.browsers?.length !== expected.length) throw new Error('Browser matrix report is incomplete.')
for (const engine of expected) {
const entry = report.browsers.find((candidate) => candidate.engine === engine)
const persistencePass = engine === 'firefox' ? entry?.opfs === true && entry?.persistence === 'opfs' : entry && ['opfs', 'memory-fallback'].includes(entry.persistence)
if (!entry || entry.status !== 'pass' || entry.pageErrors?.length !== 0 || entry.shell !== true || entry.workspace !== true || entry.crossOriginIsolated !== true || entry.sharedArrayBuffer !== true || !persistencePass || entry.workbench !== 'Part Design' || entry.canvas?.geometrySource !== 'bitbybit-occt' || entry.canvas.width < 100 || entry.canvas.height < 100 || entry.screenshotBytes < 10_000) throw new Error(`Browser matrix ${engine} evidence is incomplete.`)
}
console.log(JSON.stringify({ status: 'browser-matrix-pass', browsers: report.browsers.map(({ engine, userAgent, persistence, canvas }) => ({ engine, userAgent, persistence, canvas })) }, null, 2))

View File

@@ -21,7 +21,8 @@ async function walk(relative = '') {
} }
const violations = [] const violations = []
for (const file of await walk()) { const sourceFiles = await walk()
for (const file of sourceFiles) {
if (allowDirectRuntime.has(file)) continue if (allowDirectRuntime.has(file)) continue
const content = await readFile(new URL(file, sourceRoot), 'utf8') const content = await readFile(new URL(file, sourceRoot), 'utf8')
for (const pattern of forbidden) { for (const pattern of forbidden) {
@@ -30,9 +31,18 @@ for (const file of await walk()) {
} }
} }
const productionFacade = await readFile(new URL('facade/productionFacade.ts', sourceRoot), 'utf8')
const application = await readFile(new URL('App.tsx', sourceRoot), 'utf8')
const runtimeProfile = await readFile(new URL('facade/runtimeProfile.ts', sourceRoot), 'utf8')
if (/from\s+["']\.\/mockFacade["']/.test(productionFacade)) violations.push('facade/productionFacade.ts: production entry imports mockFacade directly')
if (/createMockFacade|facade\/mockFacade/.test(application)) violations.push('App.tsx: production application references the mock Facade')
for (const declaration of ['bitbybit-occt', 'optional-worker', 'not-exposed', 'sqlite-opfs-with-memory-fallback', 'three-webgl2']) {
if (!runtimeProfile.includes(declaration)) violations.push(`facade/runtimeProfile.ts: missing explicit runtime boundary '${declaration}'`)
}
if (violations.length) { if (violations.length) {
console.error('Facade-only boundary failed:') console.error('Facade-only boundary failed:')
violations.forEach((violation) => console.error(`- ${violation}`)) violations.forEach((violation) => console.error(`- ${violation}`))
process.exit(1) process.exit(1)
} }
console.log(`Facade-only boundary passed (${(await walk()).length} TypeScript source files checked).`) console.log(`Facade-only boundary passed (${sourceFiles.length} TypeScript source files checked).`)

View File

@@ -11,7 +11,8 @@ const golden = await load('config/chrome-fcstd-golden-verification.json')
const secondary = await load('config/chrome-secondary-formats-verification.json') const secondary = await load('config/chrome-secondary-formats-verification.json')
const fail = (message) => { throw new Error(`FCStd closure: ${message}`) } const fail = (message) => { throw new Error(`FCStd closure: ${message}`) }
if (native.status !== 'verified' || native.baselineId !== 'freecad-1.1.1' || native.freecadCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || native.scenarioCount !== 22 || native.scenarios?.length !== 22 || native.unknownDifferencesFail !== true || JSON.stringify(native.directions) !== JSON.stringify(['freecad-web-freecad', 'web-freecad-web'])) fail('native round-trip baseline or direction matrix is incomplete.') const nativeScenarioCount = native.scenarioCount
if (native.status !== 'verified' || native.baselineId !== 'freecad-1.1.1' || native.freecadCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || !Number.isSafeInteger(nativeScenarioCount) || nativeScenarioCount < 23 || native.scenarios?.length !== nativeScenarioCount || native.unknownDifferencesFail !== true || JSON.stringify(native.directions) !== JSON.stringify(['freecad-web-freecad', 'web-freecad-web'])) fail('native round-trip baseline or direction matrix is incomplete.')
if (native.scenarios.some((scenario) => scenario.status !== 'pass' || scenario.differences?.length !== 0 || scenario.domains?.length === 0 || scenario.evidence === undefined)) fail('native round-trip contains an unknown difference or missing evidence.') if (native.scenarios.some((scenario) => scenario.status !== 'pass' || scenario.differences?.length !== 0 || scenario.domains?.length === 0 || scenario.evidence === undefined)) fail('native round-trip contains an unknown difference or missing evidence.')
const proxy = native.scenarios.find((scenario) => scenario.id === 'proxy-byte-preservation')?.evidence const proxy = native.scenarios.find((scenario) => scenario.id === 'proxy-byte-preservation')?.evidence
if (!proxy || proxy.byteIdentical !== true || proxy.sourceArchiveBytes !== proxy.preservedArchiveBytes || proxy.sourceSha256 !== proxy.preservedSha256 || proxy.proxyObjectCount !== 8 || proxy.resavedProxyObjectCount !== 8) fail('unknown/proxy archive bytes were not preserved.') if (!proxy || proxy.byteIdentical !== true || proxy.sourceArchiveBytes !== proxy.preservedArchiveBytes || proxy.sourceSha256 !== proxy.preservedSha256 || proxy.proxyObjectCount !== 8 || proxy.resavedProxyObjectCount !== 8) fail('unknown/proxy archive bytes were not preserved.')
@@ -28,4 +29,4 @@ if (secondary.status !== 'pass' || secondary.formats?.descriptors !== formats.le
const hardening = spawnSync('./npmw', ['run', 'check:fcstd-hardening'], { cwd: root, encoding: 'utf8', timeout: 120_000 }) const hardening = spawnSync('./npmw', ['run', 'check:fcstd-hardening'], { cwd: root, encoding: 'utf8', timeout: 120_000 })
if (hardening.status !== 0) fail(`FC-11 hardening suite failed: ${(hardening.stdout || '') + (hardening.stderr || '')}`) if (hardening.status !== 0) fail(`FC-11 hardening suite failed: ${(hardening.stdout || '') + (hardening.stderr || '')}`)
console.log(JSON.stringify({ status: 'fcstd-closure-pass', nativeScenarios: native.scenarioCount, semanticObjects: semantic.objectNames.length, goldenModels: golden.scenarioCount, formatMatrix: secondary.formats.matrix, chromeDirections: chromeRoundTrip.directions, hardening: 'pass' }, null, 2)) console.log(JSON.stringify({ status: 'fcstd-closure-pass', nativeScenarios: nativeScenarioCount, semanticObjects: semantic.objectNames.length, goldenModels: golden.scenarioCount, formatMatrix: secondary.formats.matrix, chromeDirections: chromeRoundTrip.directions, hardening: 'pass' }, null, 2))

View File

@@ -6,7 +6,7 @@ const load = (path) => readFile(resolve(root, path), 'utf8').then(JSON.parse)
const [ const [
fcstdFuzz, geometryFuzz, sketchFuzz, successFixtures, failureFixtures, supplementalSuccessFixtures, supplementalFailureFixtures, fcstdFuzz, geometryFuzz, sketchFuzz, successFixtures, failureFixtures, supplementalSuccessFixtures, supplementalFailureFixtures,
sketchOracle, partdesignBase, partdesignLoft, partdesignDressup, partdesignTransform, partdesignFailures, partdesignRevolutionGroove, partBuilders, sketchOracle, partdesignBase, partdesignLoft, partdesignDressup, partdesignTransform, partdesignFailures, partdesignRevolutionGroove, partBuilders,
fcstdRoundTrip, app, performance, fault, opfsMigration, security, qa08, addon, script, fcstdRoundTrip, app, browserMatrix, performance, fault, opfsMigration, security, qa08, addon, script,
] = await Promise.all([ ] = await Promise.all([
load('config/qa04-fcstd-fuzz-verification.json'), load('config/qa04-fcstd-fuzz-verification.json'),
load('config/qa04-geometry-fuzz-verification.json'), load('config/qa04-geometry-fuzz-verification.json'),
@@ -25,6 +25,7 @@ const [
load('config/freecad-part-builders-oracle.json'), load('config/freecad-part-builders-oracle.json'),
load('config/freecad-fcstd-roundtrip-verification.json'), load('config/freecad-fcstd-roundtrip-verification.json'),
load('config/chrome-app-e2e-verification.json'), load('config/chrome-app-e2e-verification.json'),
load('config/browser-matrix-verification.json'),
load('config/chrome-performance-verification.json'), load('config/chrome-performance-verification.json'),
load('config/chrome-fault-injection-verification.json'), load('config/chrome-fault-injection-verification.json'),
load('config/chrome-opfs-migration-verification.json'), load('config/chrome-opfs-migration-verification.json'),
@@ -46,10 +47,11 @@ if (partdesignCases !== 23 || [partdesignBase, partdesignLoft, partdesignDressup
if (partdesignFailures.status !== 'pass' || partdesignFailures.freecadVersion !== '1.1.1' || partdesignFailures.summary?.cases !== 17 || partdesignFailures.summary.passed !== 17 || partdesignFailures.summary.rejected !== 13 || partdesignFailures.summary.acceptedEmpty !== 4 || partdesignFailures.summary.accepted !== 0) throw new Error('QA-02 PartDesign failure oracle evidence is incomplete.') if (partdesignFailures.status !== 'pass' || partdesignFailures.freecadVersion !== '1.1.1' || partdesignFailures.summary?.cases !== 17 || partdesignFailures.summary.passed !== 17 || partdesignFailures.summary.rejected !== 13 || partdesignFailures.summary.acceptedEmpty !== 4 || partdesignFailures.summary.accepted !== 0) throw new Error('QA-02 PartDesign failure oracle evidence is incomplete.')
if (partdesignRevolutionGroove.status !== 'pass' || partdesignRevolutionGroove.freecadVersion !== '1.1.1' || partdesignRevolutionGroove.summary?.cases !== 2 || partdesignRevolutionGroove.summary.passed !== 2 || partdesignRevolutionGroove.cases?.some((fixture) => fixture.shapeNull !== false || fixture.shapeValid !== true || fixture.solids !== 1)) throw new Error('QA-02 PartDesign Revolution/Groove oracle evidence is incomplete.') if (partdesignRevolutionGroove.status !== 'pass' || partdesignRevolutionGroove.freecadVersion !== '1.1.1' || partdesignRevolutionGroove.summary?.cases !== 2 || partdesignRevolutionGroove.summary.passed !== 2 || partdesignRevolutionGroove.cases?.some((fixture) => fixture.shapeNull !== false || fixture.shapeValid !== true || fixture.solids !== 1)) throw new Error('QA-02 PartDesign Revolution/Groove oracle evidence is incomplete.')
if (partBuilders.status !== 'pass' || partBuilders.freecadVersion !== '1.1.1' || partBuilders.summary?.successCases !== 6 || partBuilders.summary.successPassed !== 6 || partBuilders.summary.failureCases !== 6 || partBuilders.summary.failurePassed !== 6 || partBuilders.summary.rejected !== 6 || partBuilders.summary.acceptedEmpty !== 0) throw new Error('QA-02 Part builders oracle evidence is incomplete.') if (partBuilders.status !== 'pass' || partBuilders.freecadVersion !== '1.1.1' || partBuilders.summary?.successCases !== 6 || partBuilders.summary.successPassed !== 6 || partBuilders.summary.failureCases !== 6 || partBuilders.summary.failurePassed !== 6 || partBuilders.summary.rejected !== 6 || partBuilders.summary.acceptedEmpty !== 0) throw new Error('QA-02 Part builders oracle evidence is incomplete.')
if (fcstdRoundTrip.status !== 'verified' || fcstdRoundTrip.scenarioCount !== 22 || fcstdRoundTrip.scenarios?.some((scenario) => scenario.status !== 'pass' || scenario.differences?.length !== 0)) throw new Error('QA-02 FCStd oracle evidence is incomplete.') if (fcstdRoundTrip.status !== 'verified' || !Number.isSafeInteger(fcstdRoundTrip.scenarioCount) || fcstdRoundTrip.scenarioCount < 23 || fcstdRoundTrip.scenarios?.length !== fcstdRoundTrip.scenarioCount || fcstdRoundTrip.scenarios?.some((scenario) => scenario.status !== 'pass' || scenario.differences?.length !== 0)) throw new Error('QA-02 FCStd oracle evidence is incomplete.')
const requiredAppWorkflows = ['start-to-workspace', 'freecad-menus', 'command-search-dialog', 'preferences-about-dialogs', 'model-tree-context-menu', 'workbench-change', 'create-sketch-command', 'loft-section-task', 'drawer-collapse', 'cam-workbench-job-flow', 'opfs-save-reopen', 'import-format-selection', 'export-format-selection'] const requiredAppWorkflows = ['start-to-workspace', 'freecad-menus', 'command-search-dialog', 'preferences-about-dialogs', 'model-tree-context-menu', 'workbench-change', 'create-sketch-command', 'loft-section-task', 'drawer-collapse', 'cam-workbench-job-flow', 'opfs-save-reopen', 'import-format-selection', 'export-format-selection']
if (app.status !== 'pass' || requiredAppWorkflows.some((workflow) => !app.workflows?.includes(workflow)) || app.pageErrors?.length !== 0 || app.desktop?.kernelPreviewSource !== 'bitbybit-occt' || app.desktop.canvas?.uniqueColors < 4 || app.desktop?.commandGroups < 4 || app.desktop?.selectionView !== true || app.desktop?.statusBar !== true || app.uiParity?.commandDialog?.modal !== true || app.uiParity?.preferencesDialog?.modal !== true || app.uiParity?.aboutDialog?.modal !== true || app.uiParity?.contextMenu?.items?.length !== 4 || app.uiParity?.camMenu?.items !== 60 || app.cam?.ui?.generated !== true || app.cam.ui.toolbarCommands !== 60 || app.cam.ui.toolpathPoints < 5 || app.mobile?.viewport?.width !== 390 || Object.keys(app.screenshots ?? {}).length !== 3) throw new Error('QA-03 production Chrome E2E evidence is incomplete.') if (app.status !== 'pass' || requiredAppWorkflows.some((workflow) => !app.workflows?.includes(workflow)) || app.pageErrors?.length !== 0 || app.desktop?.kernelPreviewSource !== 'bitbybit-occt' || app.desktop.canvas?.uniqueColors < 4 || app.desktop?.commandGroups < 4 || app.desktop?.selectionView !== true || app.desktop?.statusBar !== true || app.uiParity?.commandDialog?.modal !== true || app.uiParity?.preferencesDialog?.modal !== true || app.uiParity?.aboutDialog?.modal !== true || app.uiParity?.contextMenu?.items?.length !== 4 || app.uiParity?.camMenu?.items !== 60 || app.cam?.ui?.generated !== true || app.cam.ui.toolbarCommands !== 60 || app.cam.ui.toolpathPoints < 5 || app.mobile?.viewport?.width !== 390 || Object.keys(app.screenshots ?? {}).length !== 3) throw new Error('QA-03 production Chrome E2E evidence is incomplete.')
if (browserMatrix.status !== 'pass' || browserMatrix.browsers?.length !== 2 || !['firefox', 'webkit'].every((engine) => browserMatrix.browsers.some((entry) => entry.engine === engine && entry.status === 'pass' && entry.pageErrors?.length === 0 && entry.canvas?.geometrySource === 'bitbybit-occt'))) throw new Error('QA-03 non-Chrome browser matrix evidence is incomplete.')
if (fcstdFuzz.status !== 'fcstd-parser-fuzz-pass' || fcstdFuzz.cases !== 1000 || fcstdFuzz.counts?.accepted + fcstdFuzz.counts?.rejected !== 1000 || Object.keys(fcstdFuzz.categories ?? {}).length !== 13 || fcstdFuzz.timing?.p95Ms >= 50) throw new Error('QA-04 FCStd fuzz evidence is incomplete.') if (fcstdFuzz.status !== 'fcstd-parser-fuzz-pass' || fcstdFuzz.cases !== 1000 || fcstdFuzz.counts?.accepted + fcstdFuzz.counts?.rejected !== 1000 || Object.keys(fcstdFuzz.categories ?? {}).length !== 13 || fcstdFuzz.timing?.p95Ms >= 50) throw new Error('QA-04 FCStd fuzz evidence is incomplete.')
if (geometryFuzz.status !== 'geometry-input-fuzz-pass' || geometryFuzz.cases !== 2000 || geometryFuzz.counts?.accepted + geometryFuzz.counts?.rejected !== 2000 || Object.keys(geometryFuzz.categories ?? {}).length !== 30 || geometryFuzz.timing?.p95Ms >= 10) throw new Error('QA-04 geometry fuzz evidence is incomplete.') if (geometryFuzz.status !== 'geometry-input-fuzz-pass' || geometryFuzz.cases !== 2000 || geometryFuzz.counts?.accepted + geometryFuzz.counts?.rejected !== 2000 || Object.keys(geometryFuzz.categories ?? {}).length !== 30 || geometryFuzz.timing?.p95Ms >= 10) throw new Error('QA-04 geometry fuzz evidence is incomplete.')
@@ -66,7 +68,8 @@ console.log(JSON.stringify({
tasks: 8, tasks: 8,
unitSuites: testFiles.length, unitSuites: testFiles.length,
unitTests: testCases, unitTests: testCases,
freecad: { successFixtures: 105, failureFixtures: 56, primarySuccessFixtures: 100, primaryFailureFixtures: 51, supplementalSuccessFixtures: 5, supplementalFailureFixtures: 5, sketchConstraints: 19, partdesignCases, partdesignFailureCases: partdesignFailures.summary.cases, partdesignRejectedFailures: partdesignFailures.summary.rejected, partdesignAcceptedEmpty: partdesignFailures.summary.acceptedEmpty, partdesignRevolutionGrooveCases: partdesignRevolutionGroove.summary.cases, partBuilderSuccessCases: partBuilders.summary.successCases, partBuilderFailureCases: partBuilders.summary.failureCases, fcstdRoundTrips: 22 }, freecad: { successFixtures: 105, failureFixtures: 56, primarySuccessFixtures: 100, primaryFailureFixtures: 51, supplementalSuccessFixtures: 5, supplementalFailureFixtures: 5, sketchConstraints: 19, partdesignCases, partdesignFailureCases: partdesignFailures.summary.cases, partdesignRejectedFailures: partdesignFailures.summary.rejected, partdesignAcceptedEmpty: partdesignFailures.summary.acceptedEmpty, partdesignRevolutionGrooveCases: partdesignRevolutionGroove.summary.cases, partBuilderSuccessCases: partBuilders.summary.successCases, partBuilderFailureCases: partBuilders.summary.failureCases, fcstdRoundTrips: fcstdRoundTrip.scenarioCount },
fuzz: { fcstd: fcstdFuzz.cases, geometry: geometryFuzz.cases, solverModels: sketchFuzz.models }, fuzz: { fcstd: fcstdFuzz.cases, geometry: geometryFuzz.cases, solverModels: sketchFuzz.models },
chrome: { workflows: app.workflows.length, screenshots: Object.keys(app.screenshots).length, accessibilityNodes: app.screenReader.nodes }, chrome: { workflows: app.workflows.length, screenshots: Object.keys(app.screenshots).length, accessibilityNodes: app.screenReader.nodes },
browsers: Object.fromEntries(browserMatrix.browsers.map((entry) => [entry.engine, { status: entry.status, persistence: entry.persistence, geometrySource: entry.canvas.geometrySource }])),
}, null, 2)) }, null, 2))

View File

@@ -0,0 +1,90 @@
import { createReadStream, existsSync } from 'node:fs'
import { writeFile } from 'node:fs/promises'
import { createServer } from 'node:http'
import { extname, normalize, resolve } from 'node:path'
import { firefox, webkit } from '../cnc_wams_gpt6/linuxcnc-master/web/node_modules/playwright/index.mjs'
const root = resolve(import.meta.dirname, '..')
const dist = resolve(root, 'dist')
const reportPath = resolve(root, 'config/browser-matrix-verification.json')
if (!existsSync(resolve(dist, 'index.html'))) throw new Error('Browser matrix requires a production build in dist/. Run npm run build first.')
const contentTypes = {
'.css': 'text/css; charset=utf-8',
'.html': 'text/html; charset=utf-8',
'.js': 'text/javascript; charset=utf-8',
'.json': 'application/json; charset=utf-8',
'.wasm': 'application/wasm',
}
const server = createServer((request, response) => {
response.setHeader('Cross-Origin-Opener-Policy', 'same-origin')
response.setHeader('Cross-Origin-Embedder-Policy', 'require-corp')
response.setHeader('Cross-Origin-Resource-Policy', 'same-origin')
const requestPath = decodeURIComponent((request.url || '/').split('?')[0])
const candidate = requestPath === '/' ? resolve(dist, 'index.html') : normalize(resolve(dist, `.${requestPath}`))
const file = candidate.startsWith(dist) && existsSync(candidate) ? candidate : resolve(dist, 'index.html')
response.setHeader('Content-Type', contentTypes[extname(file)] || 'application/octet-stream')
createReadStream(file).on('error', (error) => { response.writeHead(500); response.end(String(error)) }).pipe(response)
})
await new Promise((resolveServer, rejectServer) => {
server.once('error', rejectServer)
server.listen(0, '127.0.0.1', resolveServer)
})
const baseUrl = `http://127.0.0.1:${server.address().port}`
const requested = process.argv.find((argument) => argument.startsWith('--browser='))?.slice('--browser='.length)
const engines = requested ? requested.split(',') : ['firefox', 'webkit']
const browserTypes = { firefox, webkit }
const executablePaths = { firefox: process.env.FIREFOX_BIN, webkit: process.env.WEBKIT_BIN }
const results = []
try {
for (const engine of engines) {
const browserType = browserTypes[engine]
if (!browserType) throw new Error(`Unsupported browser matrix engine: ${engine}`)
const browser = await browserType.launch({ headless: true, ...(executablePaths[engine] ? { executablePath: executablePaths[engine] } : {}) })
const page = await browser.newPage({ viewport: { width: 1280, height: 900 } })
const pageErrors = []
page.on('pageerror', (error) => pageErrors.push(error.message))
try {
await page.goto(`${baseUrl}/workspace/pump-housing`, { waitUntil: 'domcontentloaded', timeout: 60_000 })
await page.waitForSelector('.app-shell', { timeout: 30_000 })
await page.waitForFunction(() => document.querySelector('.three-viewport-host canvas')?.dataset.geometrySource === 'bitbybit-occt', undefined, { timeout: 120_000 })
const state = await page.evaluate(async () => {
const canvas = document.querySelector('.three-viewport-host canvas')
let opfs = false
try { opfs = Boolean(await navigator.storage?.getDirectory?.()) } catch { opfs = false }
return {
userAgent: navigator.userAgent,
crossOriginIsolated,
sharedArrayBuffer: typeof SharedArrayBuffer === 'function',
opfs,
persistence: opfs ? 'opfs' : 'memory-fallback',
shell: Boolean(document.querySelector('.app-shell')),
workspace: Boolean(document.querySelector('.workspace-page')),
workbench: document.querySelector('select[aria-label="Workbench"]')?.value || '',
canvas: {
width: canvas?.width || 0,
height: canvas?.height || 0,
geometrySource: canvas?.dataset.geometrySource || '',
},
}
})
const screenshot = await page.screenshot({ type: 'png' })
const persistencePass = engine === 'firefox' ? state.opfs && state.persistence === 'opfs' : ['opfs', 'memory-fallback'].includes(state.persistence)
const pass = pageErrors.length === 0 && state.shell && state.workspace && state.crossOriginIsolated && state.sharedArrayBuffer && persistencePass && state.workbench === 'Part Design' && state.canvas.width >= 100 && state.canvas.height >= 100 && state.canvas.geometrySource === 'bitbybit-occt' && screenshot.byteLength > 10_000
results.push({ engine, status: pass ? 'pass' : 'fail', ...state, screenshotBytes: screenshot.byteLength, pageErrors })
} catch (error) {
results.push({ engine, status: 'fail', pageErrors: [...pageErrors, error instanceof Error ? error.message : String(error)] })
} finally {
await browser.close()
}
}
} finally {
await new Promise((resolveClose) => server.close(resolveClose))
}
const report = { schemaVersion: 1, generatedAt: new Date().toISOString(), status: results.every((entry) => entry.status === 'pass') ? 'pass' : 'fail', browsers: results }
await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`)
console.log(JSON.stringify(report, null, 2))
if (report.status !== 'pass') throw new Error('Non-Chrome browser matrix failed.')

View File

@@ -13,6 +13,15 @@ const validLanes = new Set(['all', 'chrome', 'oracle', 'wasm'])
if (!validLanes.has(requestedLane)) { if (!validLanes.has(requestedLane)) {
throw new Error(`Unknown lane "${requestedLane}". Expected one of: ${[...validLanes].join(', ')}`) throw new Error(`Unknown lane "${requestedLane}". Expected one of: ${[...validLanes].join(', ')}`)
} }
const phaseArg = process.argv.find((arg) => arg.startsWith('--phase='))
const requestedPhase = phaseArg ? phaseArg.slice('--phase='.length) : 'all'
const validPhases = new Set(['all', 'execute', 'check'])
if (!validPhases.has(requestedPhase)) {
throw new Error(`Unknown phase "${requestedPhase}". Expected one of: ${[...validPhases].join(', ')}`)
}
if (requestedPhase !== 'all' && requestedLane !== 'chrome') {
throw new Error(`Phase "${requestedPhase}" is currently supported only for the chrome lane.`)
}
const chromeTests = Object.keys(scripts) const chromeTests = Object.keys(scripts)
.filter((name) => name.startsWith('test:chrome-')) .filter((name) => name.startsWith('test:chrome-'))
@@ -92,6 +101,17 @@ const lanes = {
} }
const selectedLanes = requestedLane === 'all' ? ['oracle', 'wasm', 'chrome'] : [requestedLane] const selectedLanes = requestedLane === 'all' ? ['oracle', 'wasm', 'chrome'] : [requestedLane]
const chromeExecution = ['build', ...chromeTests]
const chromeChecks = [
...chromeTests
.map((name) => `check:${name.slice('test:'.length)}`)
.filter((name) => scripts[name]),
'check:freecad-tsn-stage-evidence',
]
const commandsFor = (lane) => {
if (lane !== 'chrome' || requestedPhase === 'all') return lanes[lane]
return requestedPhase === 'execute' ? chromeExecution : chromeChecks
}
const results = [] const results = []
function runScript(name, laneEnvironment = {}) { function runScript(name, laneEnvironment = {}) {
@@ -118,12 +138,13 @@ try {
FREECAD_REFERENCE_CMD: resolve(root, '.cache/freecad/install-desktop/bin/FreeCAD'), FREECAD_REFERENCE_CMD: resolve(root, '.cache/freecad/install-desktop/bin/FreeCAD'),
} }
: {} : {}
for (const name of lanes[lane]) runScript(name, laneEnvironment) for (const name of commandsFor(lane)) runScript(name, laneEnvironment)
} }
} finally { } finally {
const outputDir = resolve(root, '.cache', 'ci') const outputDir = resolve(root, '.cache', 'ci')
mkdirSync(outputDir, { recursive: true }) mkdirSync(outputDir, { recursive: true })
writeFileSync(resolve(outputDir, 'real-verification.json'), `${JSON.stringify({ generatedAt: new Date().toISOString(), lanes: selectedLanes, results }, null, 2)}\n`) const summaryName = requestedPhase === 'all' ? 'real-verification.json' : `real-verification-${requestedPhase}.json`
writeFileSync(resolve(outputDir, summaryName), `${JSON.stringify({ generatedAt: new Date().toISOString(), lanes: selectedLanes, phase: requestedPhase, results }, null, 2)}\n`)
} }
console.log(`\n[real-verification] completed ${results.length} commands across ${selectedLanes.join(', ')}.`) console.log(`\n[real-verification] completed ${results.length} ${requestedPhase} commands across ${selectedLanes.join(', ')}.`)

View File

@@ -8,6 +8,16 @@ export type BodyFeatureState = {
solid: boolean solid: boolean
} }
const nonSolidBodyTypeIds = new Set([
'Sketcher::SketchObject',
'PartDesign::Body',
'PartDesign::Plane',
'PartDesign::Line',
'PartDesign::Point',
'PartDesign::ShapeBinder',
'PartDesign::SubShapeBinder',
])
export const resolveBodyTip = (features: readonly BodyFeatureState[]): string | null => { export const resolveBodyTip = (features: readonly BodyFeatureState[]): string | null => {
for (let index = features.length - 1; index >= 0; index -= 1) { for (let index = features.length - 1; index >= 0; index -= 1) {
const feature = features[index] const feature = features[index]
@@ -29,7 +39,7 @@ export const redirectBodyTips = (document: DocumentSnapshot, objectStates: Recor
typeId: object?.typeId ?? '', typeId: object?.typeId ?? '',
suppressed: objectStates[id] === 'suppressed', suppressed: objectStates[id] === 'suppressed',
upstreamSuppressed: objectStates[id] === 'upstream-suppressed', upstreamSuppressed: objectStates[id] === 'upstream-suppressed',
solid: Boolean(object && object.typeId !== 'Sketcher::SketchObject' && object.typeId !== 'PartDesign::Body'), solid: Boolean(object && !nonSolidBodyTypeIds.has(object.typeId)),
} }
}) })
const nextTip = resolveBodyTip(features) const nextTip = resolveBodyTip(features)

View File

@@ -238,6 +238,11 @@ const recognizedTypeIds = new Set([
'PartDesign::LinearPattern', 'PartDesign::LinearPattern',
'PartDesign::PolarPattern', 'PartDesign::PolarPattern',
'PartDesign::Hole', 'PartDesign::Hole',
'PartDesign::Plane',
'PartDesign::Line',
'PartDesign::Point',
'PartDesign::ShapeBinder',
'PartDesign::SubShapeBinder',
'Sketcher::SketchObject', 'Sketcher::SketchObject',
'Path::Feature', 'Path::Feature',
]) ])

View File

@@ -1,5 +1,7 @@
export { createBlankDocument, createMockFacade, createPumpHousingDemoDocument, createWebCadFacade } from './mockFacade' export { createBlankDocument, createMockFacade, createPumpHousingDemoDocument, createWebCadFacade } from './mockFacade'
export type { WebCadFacadeOptions } from './mockFacade' export type { WebCadFacadeOptions } from './mockFacade'
export { createFacadeRuntimeProfile } from './runtimeProfile'
export type { FacadeRuntimeMode, FacadeRuntimeProfile } from './runtimeProfile'
export { createProductionFacade } from './productionFacade' export { createProductionFacade } from './productionFacade'
export type { ProductionFacadeOptions } from './productionFacade' export type { ProductionFacadeOptions } from './productionFacade'
export { buildDiagnosticTree, buildRecomputeDiagnostics } from './diagnostics' export { buildDiagnosticTree, buildRecomputeDiagnostics } from './diagnostics'
@@ -75,7 +77,8 @@ export { createNativeOcctStepHistoryBridge, mapNativeOcctHistoryRecords } from '
export type { NativeOcctHistoryOperation, NativeOcctHistoryRecord, NativeOcctHistoryResponse, NativeOcctHistoryStage, NativeOcctHistoryStepGeometry, NativeOcctHistoryStepProvider } from './nativeHistoryProvider' export type { NativeOcctHistoryOperation, NativeOcctHistoryRecord, NativeOcctHistoryResponse, NativeOcctHistoryStage, NativeOcctHistoryStepGeometry, NativeOcctHistoryStepProvider } from './nativeHistoryProvider'
export { DirectNativeOcctHistoryProvider, NativeOcctHistoryCoordinator, NativeOcctHistoryUnavailableError, UnavailableNativeOcctHistoryProvider } from './nativeHistoryProtocol' export { DirectNativeOcctHistoryProvider, NativeOcctHistoryCoordinator, NativeOcctHistoryUnavailableError, UnavailableNativeOcctHistoryProvider } from './nativeHistoryProtocol'
export { NATIVE_OCCT_HISTORY_PROTOCOL_VERSION } from './nativeHistoryProtocol' export { NATIVE_OCCT_HISTORY_PROTOCOL_VERSION } from './nativeHistoryProtocol'
export type { NativeOcctHistoryCapabilities, NativeOcctHistoryExecution, NativeOcctHistoryInputTransport, NativeOcctHistoryProvider, NativeOcctHistoryProtocolResponse, NativeOcctHistoryRequest, NativeOcctHistoryStageTransport } from './nativeHistoryProtocol' export { NATIVE_OCCT_NAMING_ABI_UNAVAILABLE, nativeNamingAbiCapabilities } from './nativeHistoryProtocol'
export type { NativeNamingAbiCapabilities, NativeOcctHistoryCapabilities, NativeOcctHistoryExecution, NativeOcctHistoryInputTransport, NativeOcctHistoryProvider, NativeOcctHistoryProtocolResponse, NativeOcctHistoryRequest, NativeOcctHistoryStageTransport } from './nativeHistoryProtocol'
export { assertNativeNamingEvidence, createFinalShapeOnlyNamingEvidence, createNativeStageNamingEvidence, hasNativeMappedNameEvidence, validateNativeNamingEvidence } from './nativeNamingEvidence' export { assertNativeNamingEvidence, createFinalShapeOnlyNamingEvidence, createNativeStageNamingEvidence, hasNativeMappedNameEvidence, validateNativeNamingEvidence } from './nativeNamingEvidence'
export type { NativeMappedNameRef, NativeMappedNameRelation, NativeNamingEvidenceIssue, NativeNamingEvidenceReport, NativeNamingEvidenceStatus, NativeStageNamingEvidence } from './nativeNamingEvidence' export type { NativeMappedNameRef, NativeMappedNameRelation, NativeNamingEvidenceIssue, NativeNamingEvidenceReport, NativeNamingEvidenceStatus, NativeStageNamingEvidence } from './nativeNamingEvidence'
export { NativeOcctHistoryWorkerProvider } from './nativeHistoryWorkerClient' export { NativeOcctHistoryWorkerProvider } from './nativeHistoryWorkerClient'

View File

@@ -31,6 +31,7 @@ import type {
TopoRefValue, TopoRefValue,
Unsubscribe, Unsubscribe,
} from './types' } from './types'
import { createFacadeRuntimeProfile, type FacadeRuntimeMode } from './runtimeProfile'
import { ATTACHMENT_MAP_MODES, validateAttachmentOffset, validateAttachmentSupport } from './attachment' import { ATTACHMENT_MAP_MODES, validateAttachmentOffset, validateAttachmentSupport } from './attachment'
import { redirectBodyTips } from './bodyRules' import { redirectBodyTips } from './bodyRules'
import { createSqliteProjectPersistence, ProjectAutosaveScheduler } from './projectStore' import { createSqliteProjectPersistence, ProjectAutosaveScheduler } from './projectStore'
@@ -315,7 +316,7 @@ const collectDependencyEdges = (document: Pick<DocumentSnapshot, 'objects'>): De
if (property.type === 'App::PropertyLinkList' && Array.isArray(property.value)) for (const [index, targetId] of property.value.entries()) { if (property.type === 'App::PropertyLinkList' && Array.isArray(property.value)) for (const [index, targetId] of property.value.entries()) {
if (typeof targetId === 'string' && objectIds.has(targetId)) edges.push({ sourceId: object.id, targetId, relation: 'link', propertyName: property.name, reference: `${property.name}[${index}]` }) if (typeof targetId === 'string' && objectIds.has(targetId)) edges.push({ sourceId: object.id, targetId, relation: 'link', propertyName: property.name, reference: `${property.name}[${index}]` })
} }
if (property.name === 'Support' && property.value && typeof property.value === 'object' && !Array.isArray(property.value) && 'objectId' in property.value && typeof property.value.objectId === 'string' && objectIds.has(property.value.objectId)) { if (property.type === 'App::PropertyLink' && property.name === 'Support' && property.value && typeof property.value === 'object' && !Array.isArray(property.value) && 'objectId' in property.value && typeof property.value.objectId === 'string' && objectIds.has(property.value.objectId)) {
const subElement = 'subElement' in property.value ? property.value.subElement : null const subElement = 'subElement' in property.value ? property.value.subElement : null
if (subElement && typeof subElement === 'object' && !Array.isArray(subElement) && 'schemaVersion' in subElement && typeof subElement.persistentId === 'string') edges.push({ sourceId: object.id, targetId: property.value.objectId, relation: 'topo-ref', propertyName: property.name, reference: subElement.persistentId }) if (subElement && typeof subElement === 'object' && !Array.isArray(subElement) && 'schemaVersion' in subElement && typeof subElement.persistentId === 'string') edges.push({ sourceId: object.id, targetId: property.value.objectId, relation: 'topo-ref', propertyName: property.name, reference: subElement.persistentId })
else edges.push({ sourceId: object.id, targetId: property.value.objectId, relation: 'link', propertyName: property.name }) else edges.push({ sourceId: object.id, targetId: property.value.objectId, relation: 'link', propertyName: property.name })
@@ -386,15 +387,16 @@ export type WebCadFacadeOptions = {
initialDocument?: DocumentSnapshot initialDocument?: DocumentSnapshot
initialWorkbench?: WorkbenchId initialWorkbench?: WorkbenchId
initialSelectedObjectIds?: string[] initialSelectedObjectIds?: string[]
runtimeMode?: FacadeRuntimeMode
} }
const selectionRequired = new Set(['pad', 'pocket', 'revolution', 'groove', 'additive-loft', 'subtractive-loft', 'additive-pipe', 'subtractive-pipe', 'fillet', 'chamfer', 'draft', 'thickness', 'mirrored', 'multi-transform', 'extrude-part', 'revolution-part', 'loft-part', 'sweep-part', 'union', 'cut', 'intersection', 'check-shape', 'hole', 'linear-pattern', 'polar-pattern', 'measure-distance', 'measure-angle', 'measure-area', 'solve-sketch']) const selectionRequired = new Set(['pad', 'pocket', 'revolution', 'groove', 'additive-loft', 'subtractive-loft', 'additive-pipe', 'subtractive-pipe', 'shape-binder', 'fillet', 'chamfer', 'draft', 'thickness', 'mirrored', 'multi-transform', 'extrude-part', 'revolution-part', 'loft-part', 'sweep-part', 'union', 'cut', 'intersection', 'check-shape', 'hole', 'linear-pattern', 'polar-pattern', 'measure-distance', 'measure-angle', 'measure-area', 'solve-sketch'])
const systemCommands = new Set(['new-document', 'save', 'select-object']) const systemCommands = new Set(['new-document', 'save', 'select-object'])
const camCommandIds = new Set(workbenchDefinitions.CAM.groups.flatMap((group) => group.commands.map((entry) => entry.id))) const camCommandIds = new Set(workbenchDefinitions.CAM.groups.flatMap((group) => group.commands.map((entry) => entry.id)))
const camSimulationCommands = new Set(['CAM_SimTools', 'CAM_Simulator', 'CAM_SimulatorGL']) const camSimulationCommands = new Set(['CAM_SimTools', 'CAM_Simulator', 'CAM_SimulatorGL'])
const camCopyCommands = new Set(['CAM_OperationCopy', 'CAM_Copy', 'CAM_SimpleCopy']) const camCopyCommands = new Set(['CAM_OperationCopy', 'CAM_Copy', 'CAM_SimpleCopy'])
const implementedCommandIds = new Set(['new-document', 'save', 'select-object', 'create-body', 'create-sketch', 'new-sketch', 'pad', 'pocket', 'revolution', 'groove', 'additive-loft', 'subtractive-loft', 'additive-pipe', 'subtractive-pipe', 'fillet', 'chamfer', 'draft', 'thickness', 'mirrored', 'multi-transform', 'linear-pattern', 'polar-pattern', 'hole', 'primitive', 'helix', 'extrude-part', 'revolution-part', 'loft-part', 'sweep-part', 'union', 'cut', 'intersection', 'fillet-part', 'chamfer-part', 'check-shape', 'solve-sketch', ...camCommandIds]) const implementedCommandIds = new Set(['new-document', 'save', 'select-object', 'create-body', 'create-sketch', 'new-sketch', 'shape-binder', 'datum', 'pad', 'pocket', 'revolution', 'groove', 'additive-loft', 'subtractive-loft', 'additive-pipe', 'subtractive-pipe', 'fillet', 'chamfer', 'draft', 'thickness', 'mirrored', 'multi-transform', 'linear-pattern', 'polar-pattern', 'hole', 'primitive', 'helix', 'extrude-part', 'revolution-part', 'loft-part', 'sweep-part', 'union', 'cut', 'intersection', 'fillet-part', 'chamfer-part', 'check-shape', 'solve-sketch', ...camCommandIds])
const partDesignCommands = new Set(['create-body', 'create-sketch', 'pad', 'pocket', 'revolution', 'groove', 'additive-loft', 'subtractive-loft', 'additive-pipe', 'subtractive-pipe', 'fillet', 'chamfer', 'draft', 'thickness', 'mirrored', 'multi-transform', 'linear-pattern', 'polar-pattern', 'hole']) const partDesignCommands = new Set(['create-body', 'create-sketch', 'shape-binder', 'datum', 'pad', 'pocket', 'revolution', 'groove', 'additive-loft', 'subtractive-loft', 'additive-pipe', 'subtractive-pipe', 'fillet', 'chamfer', 'draft', 'thickness', 'mirrored', 'multi-transform', 'linear-pattern', 'polar-pattern', 'hole'])
const partCommands = new Set(['primitive', 'helix', 'extrude-part', 'revolution-part', 'loft-part', 'sweep-part', 'union', 'cut', 'intersection', 'fillet-part', 'chamfer-part', 'check-shape']) const partCommands = new Set(['primitive', 'helix', 'extrude-part', 'revolution-part', 'loft-part', 'sweep-part', 'union', 'cut', 'intersection', 'fillet-part', 'chamfer-part', 'check-shape'])
const partProfileCommands = new Set(['extrude-part', 'revolution-part', 'loft-part', 'sweep-part', 'additive-loft', 'subtractive-loft', 'additive-pipe', 'subtractive-pipe']) const partProfileCommands = new Set(['extrude-part', 'revolution-part', 'loft-part', 'sweep-part', 'additive-loft', 'subtractive-loft', 'additive-pipe', 'subtractive-pipe'])
const loftCommands = new Set(['loft-part', 'additive-loft', 'subtractive-loft']) const loftCommands = new Set(['loft-part', 'additive-loft', 'subtractive-loft'])
@@ -447,6 +449,8 @@ const shapeTypeIds = new Set([
const featureCommands: Record<string, { label: string; detail: string }> = { const featureCommands: Record<string, { label: string; detail: string }> = {
'create-body': { label: 'Body', detail: 'Part Design body' }, 'create-body': { label: 'Body', detail: 'Part Design body' },
'create-sketch': { label: 'Sketch', detail: 'Fully constrained' }, 'create-sketch': { label: 'Sketch', detail: 'Fully constrained' },
'shape-binder': { label: 'Shape Binder', detail: 'Cross-body support reference' },
datum: { label: 'Datum Plane', detail: 'Attached datum reference' },
pad: { label: 'Pad', detail: 'Length 42 mm' }, pad: { label: 'Pad', detail: 'Length 42 mm' },
pocket: { label: 'Pocket', detail: 'Through all' }, pocket: { label: 'Pocket', detail: 'Through all' },
revolution: { label: 'Revolution', detail: 'Angle 360 deg' }, revolution: { label: 'Revolution', detail: 'Angle 360 deg' },
@@ -675,6 +679,7 @@ const markDocumentTouched = (document: DocumentSnapshot, objectIds: Iterable<str
} }
export function createWebCadFacade(options: WebCadFacadeOptions = {}): BitBybitWebCadFacade { export function createWebCadFacade(options: WebCadFacadeOptions = {}): BitBybitWebCadFacade {
const runtime = createFacadeRuntimeProfile(options.runtimeMode ?? 'production')
const projectPersistence = createSqliteProjectPersistence() const projectPersistence = createSqliteProjectPersistence()
const geometryRuntime = new BitbybitGeometryRuntime() const geometryRuntime = new BitbybitGeometryRuntime()
const camJob = createCamJob('Job', 'CAM Job', { min: [0, 0, 0], max: [10, 10, 10], mode: 'from-base-bound-box' }) const camJob = createCamJob('Job', 'CAM Job', { min: [0, 0, 0], max: [10, 10, 10], mode: 'from-base-bound-box' })
@@ -743,26 +748,40 @@ export function createWebCadFacade(options: WebCadFacadeOptions = {}): BitBybitW
} }
const appendFeature = (document: DocumentSnapshot, commandId: string, draft: Record<string, unknown> = {}): { document: DocumentSnapshot; objectId: string } => { const appendFeature = (document: DocumentSnapshot, commandId: string, draft: Record<string, unknown> = {}): { document: DocumentSnapshot; objectId: string } => {
const primitiveType = commandId === 'primitive' && ['Box', 'Cylinder', 'Sphere', 'Ellipsoid', 'Cone', 'Torus', 'Prism', 'Wedge'].includes(String(draft.primitiveType)) ? String(draft.primitiveType) : 'Box' const primitiveType = commandId === 'primitive' && ['Box', 'Cylinder', 'Sphere', 'Ellipsoid', 'Cone', 'Torus', 'Prism', 'Wedge'].includes(String(draft.primitiveType)) ? String(draft.primitiveType) : 'Box'
const definition = commandId === 'primitive' ? { label: primitiveType, detail: primitiveType === 'Box' ? '10 × 10 × 10 mm' : `${primitiveType} primitive` } : featureCommands[commandId] const requestedDatumType = draft.datumType === 'Line' || draft.datumType === 'Point' ? draft.datumType : 'Plane'
const definition = commandId === 'primitive'
? { label: primitiveType, detail: primitiveType === 'Box' ? '10 × 10 × 10 mm' : `${primitiveType} primitive` }
: commandId === 'datum'
? { label: `Datum ${requestedDatumType}`, detail: 'Attached datum reference' }
: featureCommands[commandId]
if (!definition) return { document, objectId: '' } if (!definition) return { document, objectId: '' }
const objectId = nextFeatureId(definition.label) const objectId = nextFeatureId(definition.label)
const type = commandId === 'create-sketch' ? 'sketch' : commandId === 'create-body' ? 'body' : 'feature' const type = commandId === 'create-sketch' ? 'sketch' : commandId === 'create-body' ? 'body' : 'feature'
const item: ModelTreeItem = { id: objectId, label: definition.label, type, state: type === 'body' ? 'active' : 'valid', detail: definition.detail } const item: ModelTreeItem = { id: objectId, label: definition.label, type, state: type === 'body' ? 'active' : 'valid', detail: definition.detail }
const tree: ModelTreeItem[] = document.tree.map((entry) => ({ ...entry, children: entry.children ? [...entry.children] : undefined })) const tree: ModelTreeItem[] = document.tree.map((entry) => ({ ...entry, children: entry.children ? [...entry.children] : undefined }))
const isPartObject = partCommands.has(commandId) const isPartObject = partCommands.has(commandId)
if (type === 'body' || isPartObject) tree.push({ ...item, children: type === 'body' ? [] : undefined }) if (type === 'body') {
for (const body of tree.filter((entry) => entry.type === 'body')) if (body.state === 'active') body.state = 'valid'
tree.push({ ...item, children: [] })
} else if (isPartObject) tree.push({ ...item, children: undefined })
else { else {
const body = tree.find((entry) => entry.type === 'body') const body = [...tree].reverse().find((entry) => entry.type === 'body' && entry.state === 'active') ?? tree.find((entry) => entry.type === 'body')
if (body) body.children = [...(body.children || []), objectId] if (body) body.children = [...(body.children || []), objectId]
tree.push(item) tree.push(item)
} }
const objectSnapshot = createObjectSnapshot(item) const objectSnapshot = createObjectSnapshot(item)
if (commandId === 'create-body') {
const tip = objectSnapshot.properties.find((property) => property.name === 'Tip')
if (tip) tip.value = null
}
const sourceId = typeof draft.source === 'string' && document.objects.some((object) => object.id === draft.source) ? draft.source : '' const sourceId = typeof draft.source === 'string' && document.objects.some((object) => object.id === draft.source) ? draft.source : ''
const sourceObject = sourceId ? document.objects.find((object) => object.id === sourceId) : undefined const sourceObject = sourceId ? document.objects.find((object) => object.id === sourceId) : undefined
const currentSubshape = state.selectedSubshape const currentSubshape = state.selectedSubshape
const selectedEdge = currentSubshape && currentSubshape.objectId === sourceObject?.id && currentSubshape.ref.kind === 'edge' ? currentSubshape : null const selectedEdge = currentSubshape && currentSubshape.objectId === sourceObject?.id && currentSubshape.ref.kind === 'edge' ? currentSubshape : null
const selectedFace = currentSubshape && currentSubshape.objectId === sourceObject?.id && currentSubshape.ref.kind === 'face' ? currentSubshape : null const selectedFace = currentSubshape && currentSubshape.objectId === sourceObject?.id && currentSubshape.ref.kind === 'face' ? currentSubshape : null
const bodyTipValue = document.objects.find((object) => object.typeId === 'PartDesign::Body')?.properties.find((property) => property.name === 'Tip')?.value const activeBodyItem = [...tree].reverse().find((entry) => entry.type === 'body' && entry.state === 'active') ?? tree.find((entry) => entry.type === 'body')
if (['datum', 'shape-binder'].includes(commandId) && !activeBodyItem) throw new Error(`${definition.label} requires an active Body.`)
const bodyTipValue = document.objects.find((object) => object.id === activeBodyItem?.id)?.properties.find((property) => property.name === 'Tip')?.value
const bodyTip = typeof bodyTipValue === 'string' && document.objects.some((object) => object.id === bodyTipValue && shapeTypeIds.has(object.typeId)) ? bodyTipValue : null const bodyTip = typeof bodyTipValue === 'string' && document.objects.some((object) => object.id === bodyTipValue && shapeTypeIds.has(object.typeId)) ? bodyTipValue : null
const sectionDraft = Array.isArray(draft.sections) && draft.sections.every((entry) => typeof entry === 'string') ? [...draft.sections] as string[] : sourceObject?.typeId === 'Sketcher::SketchObject' ? [sourceObject.id] : [] const sectionDraft = Array.isArray(draft.sections) && draft.sections.every((entry) => typeof entry === 'string') ? [...draft.sections] as string[] : sourceObject?.typeId === 'Sketcher::SketchObject' ? [sourceObject.id] : []
const pipeProfile = typeof draft.profile === 'string' ? draft.profile : sourceObject?.typeId === 'Sketcher::SketchObject' ? sourceObject.id : null const pipeProfile = typeof draft.profile === 'string' ? draft.profile : sourceObject?.typeId === 'Sketcher::SketchObject' ? sourceObject.id : null
@@ -789,6 +808,33 @@ export function createWebCadFacade(options: WebCadFacadeOptions = {}): BitBybitW
signature: selectedFace.ref.signature, signature: selectedFace.ref.signature,
candidates: selectedFace.ref.candidates ? [...selectedFace.ref.candidates] : undefined, candidates: selectedFace.ref.candidates ? [...selectedFace.ref.candidates] : undefined,
} : null } : null
if (commandId === 'datum') {
const datumType = requestedDatumType
const typeId = `PartDesign::${datumType}`
const support = selectedFaceRef ?? selectedEdgeRef ?? (sourceObject ? { schemaVersion: 1 as const, objectId: sourceObject.id, subElements: [] } : null)
objectSnapshot.typeId = typeId
objectSnapshot.properties = [
...commonProperties({ ...item, label: `Datum ${datumType}` }).map((property) => property.name === 'TypeId' ? { ...property, value: typeId } : property),
{ name: 'Support', label: 'Support', group: 'Attachment', scope: 'data', type: 'App::PropertyLinkSub', value: support, recompute: true },
{ name: 'MapMode', label: 'Map mode', group: 'Attachment', scope: 'data', type: 'App::PropertyEnumeration', value: selectedFaceRef ? 'FlatFace' : selectedEdgeRef ? 'NormalToEdge' : support ? 'ObjectXY' : 'Deactivated', options: [...ATTACHMENT_MAP_MODES], recompute: true },
{ name: 'AttachmentOffset', label: 'Attachment offset', group: 'Attachment', scope: 'data', type: 'App::PropertyPlacement', value: { position: { x: 0, y: 0, z: 0 }, rotation: { axis: { x: 0, y: 0, z: 1 }, angle: 0 } }, recompute: true },
{ name: 'DatumType', label: 'Datum type', group: 'Datum', scope: 'data', type: 'App::PropertyEnumeration', value: datumType, options: ['Plane', 'Line', 'Point'], recompute: true },
...viewProperties(),
]
}
if (commandId === 'shape-binder') {
const support = selectedFaceRef ?? selectedEdgeRef ?? (sourceObject ? { schemaVersion: 1 as const, objectId: sourceObject.id, subElements: [] } : null)
if (!support) throw new RangeError('Shape Binder requires a source object or stable sub-shape.')
objectSnapshot.typeId = 'PartDesign::ShapeBinder'
objectSnapshot.properties = [
...commonProperties(item).map((property) => property.name === 'TypeId' ? { ...property, value: 'PartDesign::ShapeBinder' } : property),
{ name: 'Support', label: 'Support', group: 'Binder', scope: 'data', type: 'App::PropertyLinkSub', value: support, recompute: true },
{ name: 'BindMode', label: 'Bind mode', group: 'Binder', scope: 'data', type: 'App::PropertyEnumeration', value: 'Synchronized', options: ['Synchronized', 'Frozen'], recompute: true },
{ name: 'TraceSupport', label: 'Trace support', group: 'Binder', scope: 'data', type: 'App::PropertyBool', value: true, recompute: true },
{ name: 'ClaimChildren', label: 'Claim children', group: 'Binder', scope: 'data', type: 'App::PropertyBool', value: false, recompute: true },
...viewProperties(),
]
}
if (commandId === 'extrude-part') { if (commandId === 'extrude-part') {
objectSnapshot.typeId = 'Part::Extrusion' objectSnapshot.typeId = 'Part::Extrusion'
objectSnapshot.properties = [ objectSnapshot.properties = [
@@ -963,7 +1009,7 @@ export function createWebCadFacade(options: WebCadFacadeOptions = {}): BitBybitW
if (Number(objectSnapshot.properties.find((property) => property.name === 'Depth')?.value) <= 0) throw new RangeError('Hole depth must be greater than zero.') if (Number(objectSnapshot.properties.find((property) => property.name === 'Depth')?.value) <= 0) throw new RangeError('Hole depth must be greater than zero.')
} }
const objects = [...document.objects.map((object) => ({ ...object, properties: object.properties.map((property) => ({ ...property, value: clonePropertyValue(property.value) })), sketch: object.sketch ? cloneSketch(object.sketch) : undefined })), objectSnapshot] const objects = [...document.objects.map((object) => ({ ...object, properties: object.properties.map((property) => ({ ...property, value: clonePropertyValue(property.value) })), sketch: object.sketch ? cloneSketch(object.sketch) : undefined })), objectSnapshot]
const tipObject = type === 'feature' && partDesignCommands.has(commandId) && commandId !== 'create-sketch' ? objects.find((object) => object.id === 'body') : undefined const tipObject = type === 'feature' && partDesignCommands.has(commandId) && !['create-sketch', 'datum', 'shape-binder'].includes(commandId) ? objects.find((object) => object.id === activeBodyItem?.id) : undefined
if (tipObject) { if (tipObject) {
const tip = tipObject.properties.find((property) => property.name === 'Tip') const tip = tipObject.properties.find((property) => property.name === 'Tip')
if (tip) tip.value = objectId if (tip) tip.value = objectId
@@ -1404,6 +1450,7 @@ export function createWebCadFacade(options: WebCadFacadeOptions = {}): BitBybitW
} }
const facade: BitBybitWebCadFacade = { const facade: BitBybitWebCadFacade = {
runtime,
app: { document: { getActive: () => getState().document, getObject: (objectId) => { const object = state.document.objects.find((candidate) => candidate.id === objectId); return object ? { ...object, properties: object.properties.map((property) => ({ ...property, options: property.options ? [...property.options] : undefined })), sketch: object.sketch ? cloneSketch(object.sketch) : undefined, topology: object.topology ? cloneObjectTopologySnapshot(object.topology) : undefined } : null }, create: (label) => { recomputeCoordinator.cancel(); clearFeatureShapes(); commit({ ...state, document: nextBlankDocument(label), selectedObjectId: '', selectedObjectIds: [] }); return getState().document }, load: loadDocument, markDirty: () => { commit({ ...state, document: { ...state.document, dirty: true } }) }, setProperty, reorderBodyFeature, removeObject, resolveTopologyReference, setExpression, recompute: recomputeDocument, recomputeAsync: recomputeDocumentAsync, cancelRecompute: () => recomputeCoordinator.cancel(), getDependencies: () => (state.document.dependencies ?? []).map((edge) => ({ ...edge })) }, expression: { evaluate: (expression, variables = {}) => evaluateQuantityExpression(expression, new Map(Object.entries(variables))), dimensionForUnit: quantityDimensionForUnit }, sketcher: { get: getSketch, addGeometry: addSketchGeometry, projectGeometry: projectSketch, carbonCopy: carbonCopySketch, addExternalGeometry: addSketchExternalGeometry, addConstraint: addSketchConstraint, solve: solveSketchObject } }, app: { document: { getActive: () => getState().document, getObject: (objectId) => { const object = state.document.objects.find((candidate) => candidate.id === objectId); return object ? { ...object, properties: object.properties.map((property) => ({ ...property, options: property.options ? [...property.options] : undefined })), sketch: object.sketch ? cloneSketch(object.sketch) : undefined, topology: object.topology ? cloneObjectTopologySnapshot(object.topology) : undefined } : null }, create: (label) => { recomputeCoordinator.cancel(); clearFeatureShapes(); commit({ ...state, document: nextBlankDocument(label), selectedObjectId: '', selectedObjectIds: [] }); return getState().document }, load: loadDocument, markDirty: () => { commit({ ...state, document: { ...state.document, dirty: true } }) }, setProperty, reorderBodyFeature, removeObject, resolveTopologyReference, setExpression, recompute: recomputeDocument, recomputeAsync: recomputeDocumentAsync, cancelRecompute: () => recomputeCoordinator.cancel(), getDependencies: () => (state.document.dependencies ?? []).map((edge) => ({ ...edge })) }, expression: { evaluate: (expression, variables = {}) => evaluateQuantityExpression(expression, new Map(Object.entries(variables))), dimensionForUnit: quantityDimensionForUnit }, sketcher: { get: getSketch, addGeometry: addSketchGeometry, projectGeometry: projectSketch, carbonCopy: carbonCopySketch, addExternalGeometry: addSketchExternalGeometry, addConstraint: addSketchConstraint, solve: solveSketchObject } },
history: { canUndo: () => undoStack.length > 0, canRedo: () => redoStack.length > 0, undo: () => { const previous = undoStack.pop(); if (!previous) return; clearFeatureShapes(); redoStack.push(getState()); state = previous; emitState(); notify('Undo applied') }, redo: () => { const next = redoStack.pop(); if (!next) return; clearFeatureShapes(); undoStack.push(getState()); state = next; emitState(); notify('Redo applied') } }, history: { canUndo: () => undoStack.length > 0, canRedo: () => redoStack.length > 0, undo: () => { const previous = undoStack.pop(); if (!previous) return; clearFeatureShapes(); redoStack.push(getState()); state = previous; emitState(); notify('Undo applied') }, redo: () => { const next = redoStack.pop(); if (!next) return; clearFeatureShapes(); undoStack.push(getState()); state = next; emitState(); notify('Redo applied') } },
gui: { workbench: { list: () => Object.keys(workbenchDefinitions) as WorkbenchId[], getActive: () => state.activeWorkbench, setActive }, command: { getState: (commandId) => commandState(commandId, state.activeWorkbench, state.selectedObjectId, state.document.objects.find((object) => object.id === state.selectedObjectId), state.selectedSubshape), list: (workbench) => workbenchDefinitions[workbench].groups.flatMap((group) => group.commands), execute } }, gui: { workbench: { list: () => Object.keys(workbenchDefinitions) as WorkbenchId[], getActive: () => state.activeWorkbench, setActive }, command: { getState: (commandId) => commandState(commandId, state.activeWorkbench, state.selectedObjectId, state.document.objects.find((object) => object.id === state.selectedObjectId), state.selectedSubshape), list: (workbench) => workbenchDefinitions[workbench].groups.flatMap((group) => group.commands), execute } },
@@ -1473,6 +1520,7 @@ export function createWebCadFacade(options: WebCadFacadeOptions = {}): BitBybitW
export function createMockFacade(): BitBybitWebCadFacade { export function createMockFacade(): BitBybitWebCadFacade {
return createWebCadFacade({ return createWebCadFacade({
runtimeMode: 'mock',
initialDocument: createPumpHousingDemoDocument(), initialDocument: createPumpHousingDemoDocument(),
initialSelectedObjectIds: PUMP_HOUSING_DEMO_TEMPLATE.selectedObjectIds, initialSelectedObjectIds: PUMP_HOUSING_DEMO_TEMPLATE.selectedObjectIds,
}) })

View File

@@ -2,6 +2,18 @@ import type { NativeOcctHistoryOperation, NativeOcctHistoryResponse, NativeOcctH
export const NATIVE_OCCT_HISTORY_PROTOCOL_VERSION = 1 as const export const NATIVE_OCCT_HISTORY_PROTOCOL_VERSION = 1 as const
export type NativeNamingAbiCapabilities = {
mappedNameRef: 'available' | 'optional' | 'unavailable'
stringHasher: 'available' | 'opaque-preserved' | 'unavailable'
tokenGeneration: 'native-only' | 'forbidden'
}
export const NATIVE_OCCT_NAMING_ABI_UNAVAILABLE: NativeNamingAbiCapabilities = Object.freeze({
mappedNameRef: 'unavailable',
stringHasher: 'unavailable',
tokenGeneration: 'forbidden',
})
export type NativeOcctHistoryCapabilities = { export type NativeOcctHistoryCapabilities = {
providerId: string providerId: string
providerVersion: string providerVersion: string
@@ -9,9 +21,16 @@ export type NativeOcctHistoryCapabilities = {
availability: 'available' | 'unavailable' availability: 'available' | 'unavailable'
operations: NativeOcctHistoryOperation[] operations: NativeOcctHistoryOperation[]
transport: 'step-text' transport: 'step-text'
/** Private FreeCAD naming callbacks are intentionally never synthesized by this bridge. */
naming?: NativeNamingAbiCapabilities
reason?: string reason?: string
} }
export const nativeNamingAbiCapabilities = (capabilities: NativeOcctHistoryCapabilities): NativeNamingAbiCapabilities => ({
...NATIVE_OCCT_NAMING_ABI_UNAVAILABLE,
...(capabilities.naming ?? {}),
})
export type NativeOcctHistoryRequest = { export type NativeOcctHistoryRequest = {
protocolVersion: typeof NATIVE_OCCT_HISTORY_PROTOCOL_VERSION protocolVersion: typeof NATIVE_OCCT_HISTORY_PROTOCOL_VERSION
requestId: string requestId: string

View File

@@ -1,7 +1,7 @@
/// <reference lib="webworker" /> /// <reference lib="webworker" />
import type { NativeOcctHistoryStepProvider } from './nativeHistoryProvider' import type { NativeOcctHistoryStepProvider } from './nativeHistoryProvider'
import type { NativeOcctHistoryCapabilities, NativeOcctHistoryRequest, NativeOcctHistoryProtocolResponse } from './nativeHistoryProtocol' import { NATIVE_OCCT_NAMING_ABI_UNAVAILABLE, type NativeOcctHistoryCapabilities, type NativeOcctHistoryRequest, type NativeOcctHistoryProtocolResponse } from './nativeHistoryProtocol'
type WorkerRequest = { type: 'initialize'; moduleUrl: string } | { type: 'capture'; request: NativeOcctHistoryRequest } | { type: 'cancel'; requestId: string } | { type: 'dispose' } 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 } type WorkerResponse = { type: 'ready'; capabilities: NativeOcctHistoryCapabilities } | { type: 'response'; response: NativeOcctHistoryProtocolResponse } | { type: 'error'; requestId?: string; error: string }
@@ -32,6 +32,7 @@ const initialize = async (moduleUrl: string) => {
availability: 'available', availability: 'available',
operations: ['fuse', 'cut', 'common', 'rotate', 'pad', 'pocket', 'loft', 'pipe', 'revolution', 'groove', 'fillet', 'chamfer', 'hole', 'draft', 'thickness', 'linear-pattern', 'polar-pattern', 'mirrored', 'multi-transform'], operations: ['fuse', 'cut', 'common', 'rotate', 'pad', 'pocket', 'loft', 'pipe', 'revolution', 'groove', 'fillet', 'chamfer', 'hole', 'draft', 'thickness', 'linear-pattern', 'polar-pattern', 'mirrored', 'multi-transform'],
transport: 'step-text', transport: 'step-text',
naming: NATIVE_OCCT_NAMING_ABI_UNAVAILABLE,
}, },
}) })
} }
@@ -80,7 +81,7 @@ scope.onmessage = ({ data }: MessageEvent<WorkerRequest>) => {
: provider.booleanHistoryFromStep(request.objectStep, request.toolStep || '', request.operation) : provider.booleanHistoryFromStep(request.objectStep, request.toolStep || '', request.operation)
if (!history) throw new Error('Native OCCT history provider does not expose the requested feature operation.') if (!history) throw new Error('Native OCCT history provider does not expose the requested feature operation.')
if (cancelled.delete(data.request.requestId)) return if (cancelled.delete(data.request.requestId)) return
send({ type: 'response', 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: history.occtVersion, availability: 'available', operations: ['fuse', 'cut', 'common', 'rotate', 'pad', 'pocket', 'loft', 'pipe', 'revolution', 'groove', 'fillet', 'chamfer', 'hole', 'draft', 'thickness', 'linear-pattern', 'polar-pattern', 'mirrored', 'multi-transform'], transport: 'step-text' }, history } }) send({ type: 'response', 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: history.occtVersion, availability: 'available', operations: ['fuse', 'cut', 'common', 'rotate', 'pad', 'pocket', 'loft', 'pipe', 'revolution', 'groove', 'fillet', 'chamfer', 'hole', 'draft', 'thickness', 'linear-pattern', 'polar-pattern', 'mirrored', 'multi-transform'], transport: 'step-text', naming: NATIVE_OCCT_NAMING_ABI_UNAVAILABLE }, history } })
} catch (error) { } catch (error) {
send({ type: 'error', requestId: data.type === 'capture' ? data.request.requestId : undefined, error: error instanceof Error ? error.message : String(error) }) send({ type: 'error', requestId: data.type === 'capture' ? data.request.requestId : undefined, error: error instanceof Error ? error.message : String(error) })
} }

View File

@@ -1,8 +1,8 @@
import { createWebCadFacade, type WebCadFacadeOptions } from './mockFacade' import { createWebCadFacade, type WebCadFacadeOptions } from './webCadFacade'
import type { BitBybitWebCadFacade } from './types' import type { BitBybitWebCadFacade } from './types'
export type ProductionFacadeOptions = WebCadFacadeOptions export type ProductionFacadeOptions = WebCadFacadeOptions
export function createProductionFacade(options: ProductionFacadeOptions = {}): BitBybitWebCadFacade { export function createProductionFacade(options: ProductionFacadeOptions = {}): BitBybitWebCadFacade {
return createWebCadFacade(options) return createWebCadFacade({ ...options, runtimeMode: 'production' })
} }

View File

@@ -0,0 +1,39 @@
export type FacadeRuntimeMode = 'production' | 'mock'
export type FacadeRuntimeProfile = {
mode: FacadeRuntimeMode
geometry: {
engine: 'bitbybit-occt'
execution: 'facade-runtime'
compatibility: 'none' | 'demo-bootstrap'
}
nativeHistory: {
transport: 'optional-worker'
availability: 'configured-at-application-boundary' | 'not-configured'
}
naming: {
nativeAbi: 'not-exposed'
preservation: 'opaque-preserved'
}
persistence: 'sqlite-opfs-with-memory-fallback'
viewport: 'three-webgl2'
}
export const createFacadeRuntimeProfile = (mode: FacadeRuntimeMode): FacadeRuntimeProfile => ({
mode,
geometry: {
engine: 'bitbybit-occt',
execution: 'facade-runtime',
compatibility: mode === 'mock' ? 'demo-bootstrap' : 'none',
},
nativeHistory: {
transport: 'optional-worker',
availability: 'not-configured',
},
naming: {
nativeAbi: 'not-exposed',
preservation: 'opaque-preserved',
},
persistence: 'sqlite-opfs-with-memory-fallback',
viewport: 'three-webgl2',
})

View File

@@ -5,6 +5,7 @@ import type { SketchConstraint, SketchExternalGeometry, SketchGeometry, SketchSn
import type { RecomputeExecutionOptions, RecomputeExecutionResult } from './recomputeEngine' import type { RecomputeExecutionOptions, RecomputeExecutionResult } from './recomputeEngine'
import type { FcstdArchiveLimits, FcstdInspection, FcstdInstantiatedShape, FcstdPathEdit, FcstdShapeResourcePayload, FcstdStoredShapeResource, FcstdWriteOptions } from './fcstd' import type { FcstdArchiveLimits, FcstdInspection, FcstdInstantiatedShape, FcstdPathEdit, FcstdShapeResourcePayload, FcstdStoredShapeResource, FcstdWriteOptions } from './fcstd'
import type { NativeOcctHistoryCapabilities, NativeOcctHistoryProvider } from './nativeHistoryProtocol' import type { NativeOcctHistoryCapabilities, NativeOcctHistoryProvider } from './nativeHistoryProtocol'
import type { FacadeRuntimeProfile } from './runtimeProfile'
import type { CamApi } from './cam' import type { CamApi } from './cam'
import type { NativeStageNamingEvidence } from './nativeNamingEvidence' import type { NativeStageNamingEvidence } from './nativeNamingEvidence'
@@ -851,6 +852,7 @@ export type ExecuteCommandInput = {
} }
export interface BitBybitWebCadFacade { export interface BitBybitWebCadFacade {
readonly runtime: FacadeRuntimeProfile
readonly app: { readonly app: {
document: { document: {
getActive(): DocumentSnapshot getActive(): DocumentSnapshot

View File

@@ -0,0 +1,3 @@
// Stable production-facing entry for the shared Facade core.
export { createWebCadFacade } from './mockFacade'
export type { WebCadFacadeOptions } from './mockFacade'

View File

@@ -16,7 +16,7 @@ import { createFinalShapeOnlyNamingEvidence, createNativeStageNamingEvidence, va
import { runTopologyMutationReplay } from '../src/facade/topologyReplay' import { runTopologyMutationReplay } from '../src/facade/topologyReplay'
import { captureNativeTopologyHistory, captureNativeTopologyHistoryStages, captureSignatureTopologyHistory, composeNativeTopologyHistoryLineage } from '../src/facade/topologyHistory' import { captureNativeTopologyHistory, captureNativeTopologyHistoryStages, captureSignatureTopologyHistory, composeNativeTopologyHistoryLineage } from '../src/facade/topologyHistory'
import { createNativeOcctStepHistoryBridge, mapNativeOcctHistoryRecords } from '../src/facade/nativeHistoryProvider' import { createNativeOcctStepHistoryBridge, mapNativeOcctHistoryRecords } from '../src/facade/nativeHistoryProvider'
import { DirectNativeOcctHistoryProvider, NativeOcctHistoryCoordinator, NATIVE_OCCT_HISTORY_PROTOCOL_VERSION, type NativeOcctHistoryProvider } from '../src/facade/nativeHistoryProtocol' import { DirectNativeOcctHistoryProvider, NativeOcctHistoryCoordinator, NATIVE_OCCT_HISTORY_PROTOCOL_VERSION, nativeNamingAbiCapabilities, type NativeOcctHistoryProvider } from '../src/facade/nativeHistoryProtocol'
import { NativeOcctHistoryWorkerProvider } from '../src/facade/nativeHistoryWorkerClient' import { NativeOcctHistoryWorkerProvider } from '../src/facade/nativeHistoryWorkerClient'
import { assessResourceQuota, planResourceSweep } from '../src/facade/resourcePolicy' import { assessResourceQuota, planResourceSweep } from '../src/facade/resourcePolicy'
import { applySketchAutoConstraints, cloneSketch, createSketch, deleteSketchGeometry, dragSketchPoint, editBsplineGeometry, editSketchBspline, extendSketchLine, replaySketchEditorEvents, setSketchConstruction, SketchEditorInteractionSession, sketchGeometrySignature, solveSketch, splitSketchLine, suggestSketchAutoConstraints, trimSketchLine, validateSketchGeometry, type SketchGeometry } from '../src/facade/sketcher' import { applySketchAutoConstraints, cloneSketch, createSketch, deleteSketchGeometry, dragSketchPoint, editBsplineGeometry, editSketchBspline, extendSketchLine, replaySketchEditorEvents, setSketchConstruction, SketchEditorInteractionSession, sketchGeometrySignature, solveSketch, splitSketchLine, suggestSketchAutoConstraints, trimSketchLine, validateSketchGeometry, type SketchGeometry } from '../src/facade/sketcher'
@@ -65,6 +65,14 @@ test('facade exposes a stable initial document projection', () => {
assert.equal(state.document.id, 'doc-pump-housing') assert.equal(state.document.id, 'doc-pump-housing')
assert.equal(state.document.tree.find((item) => item.id === 'body')?.type, 'body') assert.equal(state.document.tree.find((item) => item.id === 'body')?.type, 'body')
assert.equal(facade.gui.workbench.getActive(), 'Part Design') assert.equal(facade.gui.workbench.getActive(), 'Part Design')
assert.deepEqual(facade.runtime, {
mode: 'mock',
geometry: { engine: 'bitbybit-occt', execution: 'facade-runtime', compatibility: 'demo-bootstrap' },
nativeHistory: { transport: 'optional-worker', availability: 'not-configured' },
naming: { nativeAbi: 'not-exposed', preservation: 'opaque-preserved' },
persistence: 'sqlite-opfs-with-memory-fallback',
viewport: 'three-webgl2',
})
assert.equal(facade.geometry.capabilities().provider, 'Bitbybit OCCT') assert.equal(facade.geometry.capabilities().provider, 'Bitbybit OCCT')
assert.deepEqual({ shapeCount: facade.geometry.capabilities().shapeCount, kernelReferenceCount: facade.geometry.capabilities().kernelReferenceCount, releasedShapeCount: facade.geometry.capabilities().releasedShapeCount }, { shapeCount: 0, kernelReferenceCount: 0, releasedShapeCount: 0 }) assert.deepEqual({ shapeCount: facade.geometry.capabilities().shapeCount, kernelReferenceCount: facade.geometry.capabilities().kernelReferenceCount, releasedShapeCount: facade.geometry.capabilities().releasedShapeCount }, { shapeCount: 0, kernelReferenceCount: 0, releasedShapeCount: 0 })
}) })
@@ -77,6 +85,9 @@ test('production facade starts from an empty document unless an application boot
assert.deepEqual(state.document.tree, []) assert.deepEqual(state.document.tree, [])
assert.deepEqual(state.document.objects, []) assert.deepEqual(state.document.objects, [])
assert.deepEqual(state.selectedObjectIds, []) assert.deepEqual(state.selectedObjectIds, [])
assert.equal(facade.runtime.mode, 'production')
assert.equal(facade.runtime.geometry.compatibility, 'none')
assert.equal(facade.runtime.naming.nativeAbi, 'not-exposed')
facade.geometry.dispose() facade.geometry.dispose()
}) })
@@ -686,6 +697,12 @@ test('native OCCT history protocol validates STEP context and isolates stale gen
assert.equal(staleExecution.status, 'stale') assert.equal(staleExecution.status, 'stale')
}) })
test('native naming ABI capabilities forbid synthetic FreeCAD tokens unless a provider declares native evidence', () => {
const base = { providerId: 'occt-native.history-step', providerVersion: '8.0.0-embind', occtVersion: '8.0.0', availability: 'available' as const, operations: ['cut' as const], transport: 'step-text' as const }
assert.deepEqual(nativeNamingAbiCapabilities(base), { mappedNameRef: 'unavailable', stringHasher: 'unavailable', tokenGeneration: 'forbidden' })
assert.deepEqual(nativeNamingAbiCapabilities({ ...base, naming: { mappedNameRef: 'optional', stringHasher: 'opaque-preserved', tokenGeneration: 'native-only' } }), { mappedNameRef: 'optional', stringHasher: 'opaque-preserved', tokenGeneration: 'native-only' })
})
test('native OCCT history coordinator reports timeout and cancellation', async () => { 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 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' }) 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' })
@@ -5446,6 +5463,41 @@ test('Part workbench commands create primitive and boolean document objects', ()
assert.equal(facade.gui.command.getState('check-shape').status, 'disabled') assert.equal(facade.gui.command.getState('check-shape').status, 'disabled')
}) })
test('Datum and ShapeBinder stay in the active Body without stealing its Tip and survive FCStd metadata projection', () => {
const facade = createMockFacade()
facade.gui.command.execute({ commandId: 'create-body' })
facade.task.apply()
const initial = facade.app.document.getActive()
assert.equal(initial.tree.find((item) => item.id === 'body')?.state, 'valid')
assert.equal(initial.tree.find((item) => item.id === 'body001')?.state, 'active')
assert.equal(facade.app.document.getObject('body001')?.properties.find((property) => property.name === 'Tip')?.value, null)
facade.gui.command.execute({ commandId: 'datum' })
facade.task.update({ datumType: 'Line' })
facade.task.apply()
const datum = facade.app.document.getObject('datum-line')
assert.equal(datum?.typeId, 'PartDesign::Line')
assert.equal(datum?.properties.find((property) => property.name === 'DatumType')?.value, 'Line')
assert.equal(facade.app.document.getActive().tree.find((item) => item.id === 'body001')?.children?.includes('datum-line'), true)
assert.equal(facade.app.document.getObject('body001')?.properties.find((property) => property.name === 'Tip')?.value, null)
facade.selection.select('pad')
facade.gui.command.execute({ commandId: 'shape-binder' })
facade.task.apply()
const binder = facade.app.document.getObject('shape-binder')
assert.equal(binder?.typeId, 'PartDesign::ShapeBinder')
assert.deepEqual(binder?.properties.find((property) => property.name === 'Support')?.value, { schemaVersion: 1, objectId: 'pad', subElements: [] })
assert.equal(facade.app.document.getActive().tree.find((item) => item.id === 'body001')?.children?.includes('shape-binder'), true)
assert.equal(facade.app.document.getObject('body001')?.properties.find((property) => property.name === 'Tip')?.value, null)
assert.deepEqual(facade.app.document.getDependencies().filter((edge) => edge.sourceId === 'shape-binder').map((edge) => edge.targetId), ['pad'])
const inspection = inspectFcstdArchive(serializeFcstdMetadataArchive(facade.app.document.getActive()))
assert.equal(inspection.objects.find((object) => object.name === 'datum-line')?.support, 'recognized')
assert.equal(inspection.objects.find((object) => object.name === 'shape-binder')?.support, 'recognized')
facade.geometry.dispose()
})
test('Part Design loft and pipe tasks preserve ordered Body Tip bases', () => { test('Part Design loft and pipe tasks preserve ordered Body Tip bases', () => {
const facade = createMockFacade() const facade = createMockFacade()
facade.gui.command.execute({ commandId: 'create-sketch' }) facade.gui.command.execute({ commandId: 'create-sketch' })