feat: advance FreeCAD parity and native OCCT history

This commit is contained in:
2026-08-03 08:04:10 -04:00
parent 2ce261b982
commit e5a5d74dbc
32 changed files with 1883 additions and 30 deletions

View File

@@ -0,0 +1,83 @@
import { readFile } from 'node:fs/promises'
import { createHash } from 'node:crypto'
import { execFile } from 'node:child_process'
import { fileURLToPath } from 'node:url'
import { promisify } from 'node:util'
import { resolve } from 'node:path'
const root = resolve(fileURLToPath(new URL('..', import.meta.url)))
const run = promisify(execFile)
const packageJson = JSON.parse(await readFile(resolve(root, 'node_modules/@bitbybit-dev/occt/package.json'), 'utf8'))
const matrix = JSON.parse(await readFile(resolve(root, 'config/compatibility-matrix.json'), 'utf8'))
const sourcePath = resolve(root, matrix.bitbybitSource.localSnapshot)
const { stdout: sourceCommitOutput } = await run('git', ['-C', sourcePath, 'rev-parse', 'HEAD'])
const sourceCommit = sourceCommitOutput.trim()
if (sourceCommit !== matrix.bitbybitSource.commit) throw new Error(`Bitbybit source commit mismatch: expected ${matrix.bitbybitSource.commit}, got ${sourceCommit}.`)
if (packageJson.version !== matrix.bitbybitSource.version) throw new Error(`Bitbybit package version mismatch: expected ${matrix.bitbybitSource.version}, got ${packageJson.version}.`)
const sourceCppFiles = (await run('git', ['-C', sourcePath, 'ls-tree', '-r', '--name-only', 'HEAD'])).stdout
.split('\n').filter((file) => /\.(c|cc|cpp|cxx|h|hh|hpp)$/.test(file))
if (sourceCppFiles.length !== 0) throw new Error(`Bitbybit source snapshot unexpectedly contains native source files (${sourceCppFiles.length}); review the custom binding build inputs.`)
const declarationPath = resolve(root, 'node_modules/@bitbybit-dev/occt/bitbybit-dev-occt/bitbybit-dev-occt.d.ts')
const declarations = await readFile(declarationPath, 'utf8')
const interfaceBody = (name) => {
const match = declarations.match(new RegExp(`export interface ${name} extends ClassHandle \\{([\\s\\S]*?)\\n\\}`))
if (!match) throw new Error(`Bitbybit declaration is missing interface ${name}.`)
return match[1]
}
const hasMethod = (body, name) => new RegExp(`\\n\\s*${name}\\(`).test(body)
const assert = (condition, message) => {
if (!condition) throw new Error(message)
}
const booleanInterfaces = ['BRepAlgoAPI_Fuse', 'BRepAlgoAPI_Cut', 'BRepAlgoAPI_Common']
const booleanHistorySurface = Object.fromEntries(booleanInterfaces.map((name) => {
const body = interfaceBody(name)
return [name, {
generated: hasMethod(body, 'Generated'),
modified: hasMethod(body, 'Modified'),
deleted: hasMethod(body, 'IsDeleted'),
hasGenerated: hasMethod(body, 'HasGenerated')
}]
}))
const builderBody = interfaceBody('BOPAlgo_Builder')
const listBody = interfaceBody('TopTools_ListOfShape')
for (const [name, surface] of Object.entries(booleanHistorySurface)) {
assert(!surface.generated && !surface.modified && !surface.deleted, `${name} unexpectedly exposes native Generated/Modified/IsDeleted; review the adapter contract.`)
assert(surface.hasGenerated, `${name} must retain HasGenerated for capability reporting.`)
}
assert(hasMethod(builderBody, 'Modified'), 'BOPAlgo_Builder.Modified(shape) is required for the partial history surface.')
for (const method of ['Size', 'IsEmpty', 'Append', 'First']) {
assert(hasMethod(listBody, method), `TopTools_ListOfShape.${method}() is required for the partial history surface.`)
}
console.log(JSON.stringify({
provider: 'bitbybit-occt',
version: packageJson.version,
sourceCommit,
sourceForm: matrix.bitbybitSource.sourceForm,
missingBuildInputs: matrix.bitbybitSource.missingBuildInputs,
declarationPath: 'node_modules/@bitbybit-dev/occt/bitbybit-dev-occt/bitbybit-dev-occt.d.ts',
nativeBooleanHistory: { generated: false, modified: false, deleted: false },
partialBuilderHistory: { modified: true, listIndexAccess: false },
nativeExtension: await (async () => {
const result = { status: 'not-built' }
for (const [key, relativePath, expectedHash] of [
['javascript', matrix.nativeOcctHistory.artifact.javascript, matrix.nativeOcctHistory.artifact.javascriptSha256],
['wasm', matrix.nativeOcctHistory.artifact.wasm, matrix.nativeOcctHistory.artifact.wasmSha256]
]) {
try {
const bytes = await readFile(resolve(root, relativePath))
const hash = createHash('sha256').update(bytes).digest('hex')
if (hash !== expectedHash) throw new Error(`${key} hash mismatch: expected ${expectedHash}, got ${hash}.`)
result[key] = 'hash-ok'
} catch (error) {
if (error?.code !== 'ENOENT') throw error
}
}
if (result.javascript === 'hash-ok' && result.wasm === 'hash-ok') result.status = 'built-hash-ok'
return result
})(),
status: 'audit-pass'
}, null, 2))

View File

@@ -5,7 +5,7 @@ const sourceRoot = new URL('../src/', import.meta.url)
const forbidden = [
/from\s+["']three(?:\/|["'])/, /from\s+["']@types\/three/, /from\s+["']sqlite3?/, /from\s+["']@sqlite/, /from\s+["']opfs/, /SharedArrayBuffer/, /\bpostMessage\s*\(/,
]
const allowDirectRuntime = new Set(['facade/threeViewport.ts', 'facade/threeViewport.tsx', 'facade/persistenceWorker.ts', 'facade/projectStore.ts', 'facade/geometryWorker.ts'])
const allowDirectRuntime = new Set(['facade/threeViewport.ts', 'facade/threeViewport.tsx', 'facade/persistenceWorker.ts', 'facade/projectStore.ts', 'facade/geometryWorker.ts', 'facade/nativeHistoryWorkerClient.ts', 'facade/nativeHistoryWorkerEntry.ts'])
async function walk(relative = '') {
const directory = new URL(relative, sourceRoot)

View File

@@ -0,0 +1,68 @@
import importlib
import json
import FreeCAD as App
def property_metadata(obj):
properties = []
for name in obj.PropertiesList:
try:
type_id = obj.getTypeIdOfProperty(name)
except Exception:
type_id = "unknown"
try:
group = obj.getGroupOfProperty(name)
except Exception:
group = ""
try:
status = list(obj.getPropertyStatus(name))
except Exception:
status = []
properties.append({"name": name, "typeId": type_id, "group": group, "status": status})
return properties
def probe_object(document, type_id):
try:
obj = document.addObject(type_id, "Reference" + type_id.replace(":", "_"))
except Exception as error:
return {"typeId": type_id, "available": False, "error": str(error)}
return {
"typeId": type_id,
"available": True,
"runtimeTypeId": obj.TypeId,
"properties": property_metadata(obj),
}
def probe_module(name):
try:
module = importlib.import_module(name)
return {"name": name, "available": True, "file": getattr(module, "__file__", None)}
except Exception as error:
return {"name": name, "available": False, "error": str(error)}
document = App.newDocument("ReferenceProbe")
version = App.Version()
result = {
"schemaVersion": 1,
"baselineId": "freecad-1.1.1",
"freecadVersion": ".".join(str(value) for value in version[:3]),
"revision": str(version[3]),
"gitBranch": str(version[6]) if len(version) > 6 else "",
"gitCommit": str(version[7]) if len(version) > 7 else "",
"guiUp": bool(getattr(App, "GuiUp", False)),
"buildPurpose": "headless-reference-oracle",
"modules": [probe_module(name) for name in [
"Part", "Material", "Measure", "Sketcher", "PartDesign", "TechDraw",
"Spreadsheet", "Draft", "Mesh", "Fem", "CAM", "Assembly",
]],
"objects": [probe_object(document, type_id) for type_id in [
"Part::Box", "Part::Cylinder", "Part::Sphere", "Part::Cone",
"Part::Feature", "PartDesign::Feature", "Sketcher::SketchObject",
]],
}
App.closeDocument(document.Name)
print("FREECAD_REFERENCE_RESULT=" + json.dumps(result, sort_keys=True, separators=(",", ":")))

View File

@@ -0,0 +1,29 @@
import { spawnSync } from 'node:child_process'
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const localOracle = resolve(root, '.cache/freecad/install-native/bin/FreeCADCmd')
const candidates = process.env.FREECAD_CMD ? [process.env.FREECAD_CMD] : [localOracle, 'FreeCADCmd', 'freecadcmd']
let command = null
for (const candidate of candidates) {
const probe = spawnSync(candidate, ['--version'], { encoding: 'utf8', timeout: 15000 })
if (!probe.error || probe.error.code !== 'ENOENT') { command = candidate; break }
}
if (!command) throw new Error('FreeCADCmd 1.1.1 is unavailable. Set FREECAD_CMD to the locked oracle executable.')
const execution = spawnSync(command, [resolve(root, 'scripts/freecad-reference-probe.py')], {
cwd: root,
encoding: 'utf8',
timeout: 120000,
maxBuffer: 20 * 1024 * 1024,
env: { ...process.env },
})
const output = `${execution.stdout || ''}\n${execution.stderr || ''}`
const resultLine = output.split(/\r?\n/).find((line) => line.startsWith('FREECAD_REFERENCE_RESULT='))
if (execution.error || execution.status !== 0 || !resultLine) {
throw new Error(`FreeCAD reference probe failed with status ${execution.status}: ${execution.error?.message || output.trim()}`)
}
const result = JSON.parse(resultLine.slice('FREECAD_REFERENCE_RESULT='.length))
if (result.freecadVersion !== '1.1.1') throw new Error(`Expected FreeCAD 1.1.1, received ${result.freecadVersion}`)
console.log(JSON.stringify({ command, result }, null, 2))