feat: establish production facade composition root
This commit is contained in:
58
.github/workflows/real-verification.yml
vendored
Normal file
58
.github/workflows/real-verification.yml
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
name: real-verification
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
# These jobs deliberately use the CAD self-hosted runner. The runner image must
|
||||
# contain Chrome, CMake/Ninja, Emscripten, and the pinned CAMotics/OpenCAMLib/
|
||||
# LinuxCNC worktrees described in docs/ci-real-verification.zh-CN.md.
|
||||
jobs:
|
||||
chrome:
|
||||
runs-on: [self-hosted, linux, cad]
|
||||
timeout-minutes: 90
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22.23.2
|
||||
cache: npm
|
||||
- run: ./npmw ci --ignore-scripts
|
||||
- run: ./npmw run ci:real -- --lane=chrome
|
||||
env:
|
||||
CHROME_BIN: ${{ vars.CHROME_BIN }}
|
||||
|
||||
freecad-oracle:
|
||||
runs-on: [self-hosted, linux, cad]
|
||||
timeout-minutes: 180
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22.23.2
|
||||
cache: npm
|
||||
- run: ./npmw ci --ignore-scripts
|
||||
- run: ./npmw run ci:real -- --lane=oracle
|
||||
env:
|
||||
FREECAD_ORACLE_PROFILE: headless
|
||||
|
||||
wasm:
|
||||
runs-on: [self-hosted, linux, cad]
|
||||
timeout-minutes: 120
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22.23.2
|
||||
cache: npm
|
||||
- run: ./npmw ci --ignore-scripts
|
||||
- run: ./npmw run ci:real -- --lane=wasm
|
||||
env:
|
||||
CHROME_BIN: ${{ vars.CHROME_BIN }}
|
||||
EMSDK: ${{ vars.EMSDK }}
|
||||
OCCT_SOURCE_DIR: ${{ vars.OCCT_SOURCE_DIR }}
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -10,6 +10,9 @@ CAMotics/
|
||||
OpenCAMLib/
|
||||
cnc_wams_gpt6/
|
||||
working_ocl/
|
||||
|
||||
# Ephemeral output from real CI lanes.
|
||||
.cache/ci/
|
||||
native/occt-history/dist/
|
||||
public/native/occt-history/
|
||||
__pycache__/
|
||||
|
||||
23
docs/ci-real-verification.zh-CN.md
Normal file
23
docs/ci-real-verification.zh-CN.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# 真实验证 CI
|
||||
|
||||
`npm run verify` 负责检查仓库中已生成的证据和静态门禁;它不应被误解为重新启动浏览器、FreeCAD 或 WASM。真实验证入口是:
|
||||
|
||||
```bash
|
||||
./npmw run ci:real -- --lane=chrome
|
||||
./npmw run ci:real -- --lane=oracle
|
||||
./npmw run ci:real -- --lane=wasm
|
||||
```
|
||||
|
||||
`--lane=all` 会按 oracle、WASM、Chrome 顺序执行全部 lane。任一命令退出非零都会使整个 lane 失败;运行摘要写入 `.cache/ci/real-verification.json`,该文件不作为仓库基线证据提交。
|
||||
|
||||
## Runner 要求
|
||||
|
||||
工作流使用标签为 `self-hosted, linux, cad` 的 runner,因为 FreeCAD 原生编译和外围 CAM harness 需要较大的本地工具链。runner 必须提供:
|
||||
|
||||
- Node `22.23.2`、CMake、Ninja、C/C++ 编译器和 Python;
|
||||
- Chrome/Chromium(通过 `CHROME_BIN` 指定)以及可用的无头模式;
|
||||
- Emscripten(通过 `EMSDK` 指定)和 OCCT 源码目录(通过 `OCCT_SOURCE_DIR` 指定);
|
||||
- `CAMotics/`、`OpenCAMLib/`、`cnc_wams_gpt6/`、`working_ocl/` 外部工作树。它们不进入 Git,版本由 `config/` 中的锁定清单和 drift check 校验;
|
||||
- FreeCAD 构建所需的系统开发包。`ci:real -- --lane=oracle` 会重新抓取锁定的 FreeCAD 源码、配置、编译、安装并执行 oracle,不接受旧报告代替。
|
||||
|
||||
工作流中的 `actions/checkout` 不会恢复被 `.gitignore` 忽略的外部工作树;runner 镜像或预热步骤必须提前准备它们。缺少任一依赖时,harness 应失败并暴露缺失项,而不是降级为读取历史 JSON。
|
||||
@@ -251,6 +251,7 @@
|
||||
"test:golden:freecad": "node scripts/run-freecad-golden.mjs",
|
||||
"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",
|
||||
"ci:real": "node scripts/run-real-verification.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",
|
||||
"test:chrome-native-thickness-history": "node scripts/run-chrome-native-thickness-history.mjs",
|
||||
|
||||
101
scripts/run-real-verification.mjs
Normal file
101
scripts/run-real-verification.mjs
Normal file
@@ -0,0 +1,101 @@
|
||||
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
const npmw = resolve(root, 'npmw')
|
||||
const packageJson = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8'))
|
||||
const scripts = packageJson.scripts ?? {}
|
||||
|
||||
const laneArg = process.argv.find((arg) => arg.startsWith('--lane='))
|
||||
const requestedLane = laneArg ? laneArg.slice('--lane='.length) : 'all'
|
||||
const validLanes = new Set(['all', 'chrome', 'oracle', 'wasm'])
|
||||
if (!validLanes.has(requestedLane)) {
|
||||
throw new Error(`Unknown lane "${requestedLane}". Expected one of: ${[...validLanes].join(', ')}`)
|
||||
}
|
||||
|
||||
const chromeTests = Object.keys(scripts)
|
||||
.filter((name) => name.startsWith('test:chrome-'))
|
||||
.sort()
|
||||
|
||||
const lanes = {
|
||||
chrome: [
|
||||
'build',
|
||||
...chromeTests,
|
||||
...chromeTests
|
||||
.map((name) => `check:${name.slice('test:'.length)}`)
|
||||
.filter((name) => scripts[name]),
|
||||
],
|
||||
oracle: [
|
||||
'fetch:freecad-source',
|
||||
'configure:freecad-native',
|
||||
'build:freecad-native',
|
||||
'probe:freecad-reference',
|
||||
'test:golden:freecad',
|
||||
'test:golden:freecad:families',
|
||||
'test:golden:freecad:failures',
|
||||
'test:golden:freecad:family-failures',
|
||||
'probe:freecad-sketcher-constraints',
|
||||
'probe:freecad-partdesign-profiles',
|
||||
'probe:freecad-partdesign-base',
|
||||
'probe:freecad-partdesign-loft',
|
||||
'probe:freecad-partdesign-dressup',
|
||||
'probe:freecad-partdesign-transform',
|
||||
'probe:freecad-partdesign-failures',
|
||||
'probe:freecad-partdesign-revolution-groove',
|
||||
'probe:freecad-part-builders',
|
||||
'probe:freecad-composite-history-elementmap',
|
||||
'check:freecad-oracle-coverage',
|
||||
'check:freecad-golden-coverage',
|
||||
'check:freecad-sketcher-constraints',
|
||||
'check:freecad-fcstd-roundtrip',
|
||||
'check:freecad-composite-history-elementmap',
|
||||
'check:freecad-exact-history-elementmap-gate',
|
||||
'check:freecad-native-naming-evidence',
|
||||
],
|
||||
wasm: [
|
||||
'check:occt-history-artifact',
|
||||
'test:browser-occt',
|
||||
'check:browser-occt',
|
||||
'test:planegcs',
|
||||
'check:planegcs-artifact',
|
||||
'test:chrome-planegcs',
|
||||
'test:chrome-native-history',
|
||||
'test:chrome-camotics-wasm',
|
||||
'test:chrome-cam-native-simulation',
|
||||
'check:chrome-planegcs',
|
||||
'check:chrome-native-history',
|
||||
'check:chrome-camotics-wasm',
|
||||
'check:chrome-cam-native-simulation',
|
||||
],
|
||||
}
|
||||
|
||||
const selectedLanes = requestedLane === 'all' ? ['oracle', 'wasm', 'chrome'] : [requestedLane]
|
||||
const results = []
|
||||
|
||||
function runScript(name) {
|
||||
if (!scripts[name]) throw new Error(`Required npm script is missing: ${name}`)
|
||||
const startedAt = Date.now()
|
||||
console.log(`\n[real-verification] ${name}`)
|
||||
const result = spawnSync(npmw, ['run', name], {
|
||||
cwd: root,
|
||||
env: { ...process.env, CI_REAL_VERIFICATION: '1' },
|
||||
stdio: 'inherit',
|
||||
})
|
||||
const record = { script: name, status: result.status ?? 1, durationMs: Date.now() - startedAt }
|
||||
results.push(record)
|
||||
if (record.status !== 0) throw new Error(`Real verification failed in ${name} (exit ${record.status})`)
|
||||
}
|
||||
|
||||
try {
|
||||
for (const lane of selectedLanes) {
|
||||
console.log(`\n[real-verification] lane=${lane}`)
|
||||
for (const name of lanes[lane]) runScript(name)
|
||||
}
|
||||
} finally {
|
||||
const outputDir = resolve(root, '.cache', 'ci')
|
||||
mkdirSync(outputDir, { recursive: true })
|
||||
writeFileSync(resolve(outputDir, 'real-verification.json'), `${JSON.stringify({ generatedAt: new Date().toISOString(), lanes: selectedLanes, results }, null, 2)}\n`)
|
||||
}
|
||||
|
||||
console.log(`\n[real-verification] completed ${results.length} commands across ${selectedLanes.join(', ')}.`)
|
||||
@@ -59,7 +59,7 @@ import {
|
||||
ZoomOut,
|
||||
} from 'lucide-react'
|
||||
import { menuDefinitions, workbenchDefinitions, type MenuName, type WorkbenchId } from './freecadManifest'
|
||||
import { createLinuxcncIframeAdapter, createMockFacade, NativeOcctHistoryWorkerProvider, prepareCamPipeline, submitCamPipelineToLinuxcnc, type BitBybitViewportAdapter, type BitBybitWebCadFacade, type CamAxisConfiguration, type CamCoolantMode, type CamCutDirection, type CamDressupKind, type CamKinematicOptions, type CamOperationKind, type CamPostprocessor, type CamPropertyValue, type CamRotaryAxis, type CamSnapshot, type CamStockMode, type DiagnosticTreeNode, type DocumentSnapshot, type FcstdInspection, type ModelTreeItem, type MultiTransformStep, type MultiTransformValue, type ObjectPropertySnapshot, type PlacementValue, type ProjectSummary, type PropertyValue, type ShapeHandle } from './facade'
|
||||
import { createLinuxcncIframeAdapter, createProductionFacade, createPumpHousingDemoDocument, NativeOcctHistoryWorkerProvider, prepareCamPipeline, submitCamPipelineToLinuxcnc, type BitBybitViewportAdapter, type BitBybitWebCadFacade, type CamAxisConfiguration, type CamCoolantMode, type CamCutDirection, type CamDressupKind, type CamKinematicOptions, type CamOperationKind, type CamPostprocessor, type CamPropertyValue, type CamRotaryAxis, type CamSnapshot, type CamStockMode, type DiagnosticTreeNode, type DocumentSnapshot, type FcstdInspection, type ModelTreeItem, type MultiTransformStep, type MultiTransformValue, type ObjectPropertySnapshot, type PlacementValue, type ProjectSummary, type PropertyValue, type ShapeHandle } from './facade'
|
||||
import type { LinkSubListValue, LinkSubValue, ShapeResourceValue, TopoRefValue } from './facade/types'
|
||||
|
||||
type Page = 'start' | 'projects' | 'workspace' | 'import' | 'export' | 'settings' | 'help' | 'diagnostics' | 'sync'
|
||||
@@ -133,7 +133,7 @@ function App() {
|
||||
const [rightTab, setRightTab] = useState<'data' | 'view'>('data')
|
||||
const [bottomOpen, setBottomOpen] = useState(true)
|
||||
const [notice, setNotice] = useState('')
|
||||
const facade = useMemo(() => createMockFacade(), [])
|
||||
const facade = useMemo(() => createProductionFacade({ initialDocument: createPumpHousingDemoDocument(), initialSelectedObjectIds: ['pad'] }), [])
|
||||
const nativeHistory = useMemo(() => new NativeOcctHistoryWorkerProvider(), [])
|
||||
const [facadeState, setFacadeState] = useState(() => facade.getState())
|
||||
const [diagnosticFlash, setDiagnosticFlash] = useState<{ objectId: string; token: number } | null>(null)
|
||||
|
||||
31
src/facade/documentTemplates.ts
Normal file
31
src/facade/documentTemplates.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import type { ModelTreeItem } from './types'
|
||||
|
||||
export type DocumentTemplate = {
|
||||
id: string
|
||||
label: string
|
||||
version: number
|
||||
dirty: boolean
|
||||
readOnly: boolean
|
||||
units: string
|
||||
tree: ModelTreeItem[]
|
||||
selectedObjectIds: string[]
|
||||
}
|
||||
|
||||
export const PUMP_HOUSING_DEMO_TEMPLATE: DocumentTemplate = {
|
||||
id: 'doc-pump-housing',
|
||||
label: 'Pump Housing',
|
||||
version: 18,
|
||||
dirty: true,
|
||||
readOnly: false,
|
||||
units: 'mm',
|
||||
selectedObjectIds: ['pad'],
|
||||
tree: [
|
||||
{ id: 'origin', label: 'Origin', type: 'folder', children: ['XY_Plane', 'XZ_Plane', 'YZ_Plane'] },
|
||||
{ id: 'body', label: 'Body', type: 'body', state: 'active', children: ['sketch', 'pad', 'pocket', 'fillet'] },
|
||||
{ id: 'sketch', label: 'Sketch', type: 'sketch', state: 'valid', detail: 'Fully constrained' },
|
||||
{ id: 'pad', label: 'Pad', type: 'feature', state: 'valid', detail: 'Length 42 mm' },
|
||||
{ id: 'pocket', label: 'Pocket', type: 'feature', state: 'warning', detail: 'Through all' },
|
||||
{ id: 'fillet', label: 'Fillet', type: 'feature', state: 'valid', detail: 'Radius 3 mm' },
|
||||
{ id: 'reference', label: 'Reference geometry', type: 'folder', children: ['DatumPlane', 'DatumAxis'] },
|
||||
],
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
export { createMockFacade } from './mockFacade'
|
||||
export { createBlankDocument, createMockFacade, createPumpHousingDemoDocument, createWebCadFacade } from './mockFacade'
|
||||
export type { WebCadFacadeOptions } from './mockFacade'
|
||||
export { createProductionFacade } from './productionFacade'
|
||||
export type { ProductionFacadeOptions } from './productionFacade'
|
||||
export { buildDiagnosticTree, buildRecomputeDiagnostics } from './diagnostics'
|
||||
export { createSqliteProjectPersistence, PersistenceWriteQueue, ProjectAutosaveScheduler, SqliteProjectPersistence } from './projectStore'
|
||||
export type { SqliteProjectPersistenceOptions } from './projectStore'
|
||||
|
||||
@@ -44,16 +44,7 @@ import { decodeFcstdPathProperty, extractFcstdShapeResources, inspectFcstdArchiv
|
||||
import { buildDiagnosticTree, buildRecomputeDiagnostics, cloneDiagnostic, replaceRecomputeDiagnostics } from './diagnostics'
|
||||
import { cloneObjectTopologySnapshot, migrateDocumentTopologyReferences, parseTopoRef, resolveDocumentTopologyReference } from './topologyReferences'
|
||||
import { createCamJob } from './cam'
|
||||
|
||||
const initialTree: ModelTreeItem[] = [
|
||||
{ id: 'origin', label: 'Origin', type: 'folder', children: ['XY_Plane', 'XZ_Plane', 'YZ_Plane'] },
|
||||
{ id: 'body', label: 'Body', type: 'body', state: 'active', children: ['sketch', 'pad', 'pocket', 'fillet'] },
|
||||
{ id: 'sketch', label: 'Sketch', type: 'sketch', state: 'valid', detail: 'Fully constrained' },
|
||||
{ id: 'pad', label: 'Pad', type: 'feature', state: 'valid', detail: 'Length 42 mm' },
|
||||
{ id: 'pocket', label: 'Pocket', type: 'feature', state: 'warning', detail: 'Through all' },
|
||||
{ id: 'fillet', label: 'Fillet', type: 'feature', state: 'valid', detail: 'Radius 3 mm' },
|
||||
{ id: 'reference', label: 'Reference geometry', type: 'folder', children: ['DatumPlane', 'DatumAxis'] },
|
||||
]
|
||||
import { PUMP_HOUSING_DEMO_TEMPLATE, type DocumentTemplate } from './documentTemplates'
|
||||
|
||||
const typeIdForItem = (item: ModelTreeItem) => item.type === 'body' ? 'PartDesign::Body' : item.type === 'sketch' ? 'Sketcher::SketchObject' : item.id.startsWith('box') ? 'Part::Box' : item.id.startsWith('cylinder') ? 'Part::Cylinder' : item.id.startsWith('sphere') ? 'Part::Sphere' : item.id.startsWith('ellipsoid') ? 'Part::Ellipsoid' : item.id.startsWith('cone') ? 'Part::Cone' : item.id.startsWith('torus') ? 'Part::Torus' : item.id.startsWith('helix') ? 'Part::Helix' : item.id.startsWith('prism') ? 'Part::Prism' : item.id.startsWith('wedge') ? 'Part::Wedge' : item.id.startsWith('union') ? 'Part::Fuse' : item.id.startsWith('cut') ? 'Part::Cut' : item.id.startsWith('intersection') ? 'Part::Common' : item.id.startsWith('pad') ? 'PartDesign::Pad' : item.id.startsWith('pocket') ? 'PartDesign::Pocket' : item.id.startsWith('revolution') ? 'PartDesign::Revolution' : item.id.startsWith('groove') ? 'PartDesign::Groove' : item.id.startsWith('fillet') ? 'PartDesign::Fillet' : item.id.startsWith('chamfer') ? 'PartDesign::Chamfer' : item.id.startsWith('mirrored') ? 'PartDesign::Mirrored' : item.id.startsWith('multi-transform') ? 'PartDesign::MultiTransform' : item.id.startsWith('linear-pattern') ? 'PartDesign::LinearPattern' : item.id.startsWith('polar-pattern') ? 'PartDesign::PolarPattern' : item.id.startsWith('hole') ? 'PartDesign::Hole' : item.type === 'feature' ? 'PartDesign::Feature' : 'App::DocumentObjectGroup'
|
||||
|
||||
@@ -367,14 +358,36 @@ const expressionReferences = (expression: string): string[] => {
|
||||
}
|
||||
}
|
||||
|
||||
const createDocument = (label = 'Pump Housing'): DocumentSnapshot => {
|
||||
const objects = initialTree.map(createObjectSnapshot)
|
||||
const document: DocumentSnapshot = { id: 'doc-pump-housing', label, version: 18, dirty: true, readOnly: false, units: 'mm', tree: initialTree.map((item) => ({ ...item, children: item.children ? [...item.children] : undefined })), objects }
|
||||
const createDocumentFromTemplate = (template: DocumentTemplate): DocumentSnapshot => {
|
||||
const tree = template.tree.map((item) => ({ ...item, children: item.children ? [...item.children] : undefined }))
|
||||
const objects = tree.map(createObjectSnapshot)
|
||||
const document: DocumentSnapshot = { id: template.id, label: template.label, version: template.version, dirty: template.dirty, readOnly: template.readOnly, units: template.units, tree, objects }
|
||||
document.dependencies = collectDependencyEdges(document)
|
||||
document.recompute = createRecomputeSnapshot(objects.map((object) => object.id))
|
||||
return document
|
||||
}
|
||||
|
||||
export const createBlankDocument = (label = 'Untitled document', id = 'doc-untitled'): DocumentSnapshot => ({
|
||||
id,
|
||||
label,
|
||||
version: 0,
|
||||
dirty: true,
|
||||
readOnly: false,
|
||||
units: 'mm',
|
||||
tree: [],
|
||||
objects: [],
|
||||
dependencies: [],
|
||||
recompute: createRecomputeSnapshot([]),
|
||||
})
|
||||
|
||||
export const createPumpHousingDemoDocument = (): DocumentSnapshot => createDocumentFromTemplate(PUMP_HOUSING_DEMO_TEMPLATE)
|
||||
|
||||
export type WebCadFacadeOptions = {
|
||||
initialDocument?: DocumentSnapshot
|
||||
initialWorkbench?: WorkbenchId
|
||||
initialSelectedObjectIds?: string[]
|
||||
}
|
||||
|
||||
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 systemCommands = new Set(['new-document', 'save', 'select-object'])
|
||||
const camCommandIds = new Set(workbenchDefinitions.CAM.groups.flatMap((group) => group.commands.map((entry) => entry.id)))
|
||||
@@ -661,13 +674,18 @@ const markDocumentTouched = (document: DocumentSnapshot, objectIds: Iterable<str
|
||||
return plan
|
||||
}
|
||||
|
||||
export function createMockFacade(): BitBybitWebCadFacade {
|
||||
export function createWebCadFacade(options: WebCadFacadeOptions = {}): BitBybitWebCadFacade {
|
||||
const projectPersistence = createSqliteProjectPersistence()
|
||||
const geometryRuntime = new BitbybitGeometryRuntime()
|
||||
const camJob = createCamJob('Job', 'CAM Job', { min: [0, 0, 0], max: [10, 10, 10], mode: 'from-base-bound-box' })
|
||||
const featureShapes = new Map<string, ShapeHandle>()
|
||||
const autosave = new ProjectAutosaveScheduler((document) => projectPersistence.save(document))
|
||||
let state: FacadeState = { apiVersion: '0.1', activeWorkbench: 'Part Design', selectedObjectId: 'pad', selectedObjectIds: ['pad'], document: createDocument(), persistence: projectPersistence.capabilities(), task: null, lastNotice: '', diagnostics: [] }
|
||||
const initialDocument = cloneDocumentSnapshot(options.initialDocument ?? createBlankDocument())
|
||||
const availableObjectIds = new Set(initialDocument.objects.map((object) => object.id))
|
||||
const initialSelectedObjectIds = [...new Set(options.initialSelectedObjectIds ?? [])].filter((objectId) => availableObjectIds.has(objectId))
|
||||
let documentSequence = 0
|
||||
const nextBlankDocument = (label = 'Untitled document') => createBlankDocument(label, `doc-untitled-${++documentSequence}`)
|
||||
let state: FacadeState = { apiVersion: '0.1', activeWorkbench: options.initialWorkbench ?? 'Part Design', selectedObjectId: initialSelectedObjectIds[0] ?? '', selectedObjectIds: initialSelectedObjectIds, document: initialDocument, persistence: projectPersistence.capabilities(), task: null, lastNotice: '', diagnostics: [] }
|
||||
const recomputeCoordinator = new RecomputeCoordinator(
|
||||
createFacadeGeometryRecomputeExecutor(geometryRuntime, featureShapes),
|
||||
(documentId) => state.document.id === documentId ? state.document.version : null,
|
||||
@@ -1320,7 +1338,7 @@ export function createMockFacade(): BitBybitWebCadFacade {
|
||||
emit({ type: 'diagnostic.added', diagnostic, context }); emit({ type: 'command.failed', commandId, context, message: status.reason }); notify(status.reason || 'Command is disabled'); return requestId
|
||||
}
|
||||
emit({ type: 'command.started', commandId, context })
|
||||
if (commandId === 'new-document') { clearFeatureShapes(); commit({ ...state, document: createDocument('Untitled document'), selectedObjectId: '', selectedObjectIds: [] }) }
|
||||
if (commandId === 'new-document') { clearFeatureShapes(); commit({ ...state, document: nextBlankDocument(), selectedObjectId: '', selectedObjectIds: [] }) }
|
||||
else if (commandId === 'save') {
|
||||
const savedDocument = { ...state.document, dirty: false, version: state.document.version + 1 }
|
||||
commit({ ...state, document: savedDocument })
|
||||
@@ -1386,7 +1404,7 @@ export function createMockFacade(): BitBybitWebCadFacade {
|
||||
}
|
||||
|
||||
const facade: BitBybitWebCadFacade = {
|
||||
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: createDocument(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') } },
|
||||
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 } },
|
||||
selection: { getObjectId: () => state.selectedObjectId, getObjectIds: () => [...(state.selectedObjectIds ?? (state.selectedObjectId ? [state.selectedObjectId] : []))], getSubshape: () => cloneSelection(state.selectedSubshape) ?? null, getPreselection: () => cloneSelection(state.preselectedSubshape) ?? null, select, selectObjects, selectSubshape, preselectSubshape, clear: () => select('') },
|
||||
@@ -1452,3 +1470,10 @@ export function createMockFacade(): BitBybitWebCadFacade {
|
||||
void projectPersistence.initialize().then((nextCapabilities) => { state = { ...state, persistence: nextCapabilities }; emitState() }).catch((error: unknown) => { const diagnostic: Diagnostic = { id: `diag-${++requestSequence}`, source: 'persistence', severity: 'warning', code: 'PERSISTENCE_INIT_FAILED', message: error instanceof Error ? error.message : String(error), documentId: state.document.id, documentVersion: state.document.version }; state = { ...state, diagnostics: [...state.diagnostics, diagnostic] }; emit({ type: 'diagnostic.added', diagnostic, context: { apiVersion: state.apiVersion, requestId: `req-${++requestSequence}`, documentId: state.document.id, documentVersion: state.document.version, workbench: state.activeWorkbench } }) })
|
||||
return facade
|
||||
}
|
||||
|
||||
export function createMockFacade(): BitBybitWebCadFacade {
|
||||
return createWebCadFacade({
|
||||
initialDocument: createPumpHousingDemoDocument(),
|
||||
initialSelectedObjectIds: PUMP_HOUSING_DEMO_TEMPLATE.selectedObjectIds,
|
||||
})
|
||||
}
|
||||
|
||||
8
src/facade/productionFacade.ts
Normal file
8
src/facade/productionFacade.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { createWebCadFacade, type WebCadFacadeOptions } from './mockFacade'
|
||||
import type { BitBybitWebCadFacade } from './types'
|
||||
|
||||
export type ProductionFacadeOptions = WebCadFacadeOptions
|
||||
|
||||
export function createProductionFacade(options: ProductionFacadeOptions = {}): BitBybitWebCadFacade {
|
||||
return createWebCadFacade(options)
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { test } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { strToU8, unzipSync, zipSync } from 'fflate'
|
||||
import { createMockFacade } from '../src/facade/mockFacade'
|
||||
import { createProductionFacade } from '../src/facade/productionFacade'
|
||||
import { PROJECT_SCHEMA_MIGRATIONS, PROJECT_SCHEMA_SQL, PROJECT_SCHEMA_VERSION, runProjectSchemaMigrations } from '../src/facade/projectSchema'
|
||||
import { createSqliteProjectPersistence, PersistenceWriteQueue, ProjectAutosaveScheduler } from '../src/facade/projectStore'
|
||||
import { DependencyGraph } from '../src/facade/dependencyGraph'
|
||||
@@ -68,6 +69,17 @@ test('facade exposes a stable initial document projection', () => {
|
||||
assert.deepEqual({ shapeCount: facade.geometry.capabilities().shapeCount, kernelReferenceCount: facade.geometry.capabilities().kernelReferenceCount, releasedShapeCount: facade.geometry.capabilities().releasedShapeCount }, { shapeCount: 0, kernelReferenceCount: 0, releasedShapeCount: 0 })
|
||||
})
|
||||
|
||||
test('production facade starts from an empty document unless an application bootstrap is supplied', () => {
|
||||
const facade = createProductionFacade()
|
||||
const state = facade.getState()
|
||||
assert.equal(state.document.id, 'doc-untitled')
|
||||
assert.equal(state.document.label, 'Untitled document')
|
||||
assert.deepEqual(state.document.tree, [])
|
||||
assert.deepEqual(state.document.objects, [])
|
||||
assert.deepEqual(state.selectedObjectIds, [])
|
||||
facade.geometry.dispose()
|
||||
})
|
||||
|
||||
test('geometry boundary validates dimensions before invoking OCCT', () => {
|
||||
assert.throws(() => validateBoxInput({ width: 0, length: 2, height: 3, documentId: 'doc', documentVersion: 1 }), /width/)
|
||||
assert.throws(() => validateBoxInput({ width: 1, length: 2, height: 3, center: [0, Number.NaN, 0], documentId: 'doc', documentVersion: 1 }), /center/)
|
||||
|
||||
Reference in New Issue
Block a user