feat: advance FreeCAD parity and native OCCT history
This commit is contained in:
83
scripts/check-bitbybit-history-surface.mjs
Normal file
83
scripts/check-bitbybit-history-surface.mjs
Normal file
@@ -0,0 +1,83 @@
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { execFile } from 'node:child_process'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { promisify } from 'node:util'
|
||||
import { resolve } from 'node:path'
|
||||
|
||||
const root = resolve(fileURLToPath(new URL('..', import.meta.url)))
|
||||
const run = promisify(execFile)
|
||||
const packageJson = JSON.parse(await readFile(resolve(root, 'node_modules/@bitbybit-dev/occt/package.json'), 'utf8'))
|
||||
const matrix = JSON.parse(await readFile(resolve(root, 'config/compatibility-matrix.json'), 'utf8'))
|
||||
const sourcePath = resolve(root, matrix.bitbybitSource.localSnapshot)
|
||||
const { stdout: sourceCommitOutput } = await run('git', ['-C', sourcePath, 'rev-parse', 'HEAD'])
|
||||
const sourceCommit = sourceCommitOutput.trim()
|
||||
if (sourceCommit !== matrix.bitbybitSource.commit) throw new Error(`Bitbybit source commit mismatch: expected ${matrix.bitbybitSource.commit}, got ${sourceCommit}.`)
|
||||
if (packageJson.version !== matrix.bitbybitSource.version) throw new Error(`Bitbybit package version mismatch: expected ${matrix.bitbybitSource.version}, got ${packageJson.version}.`)
|
||||
const sourceCppFiles = (await run('git', ['-C', sourcePath, 'ls-tree', '-r', '--name-only', 'HEAD'])).stdout
|
||||
.split('\n').filter((file) => /\.(c|cc|cpp|cxx|h|hh|hpp)$/.test(file))
|
||||
if (sourceCppFiles.length !== 0) throw new Error(`Bitbybit source snapshot unexpectedly contains native source files (${sourceCppFiles.length}); review the custom binding build inputs.`)
|
||||
const declarationPath = resolve(root, 'node_modules/@bitbybit-dev/occt/bitbybit-dev-occt/bitbybit-dev-occt.d.ts')
|
||||
const declarations = await readFile(declarationPath, 'utf8')
|
||||
|
||||
const interfaceBody = (name) => {
|
||||
const match = declarations.match(new RegExp(`export interface ${name} extends ClassHandle \\{([\\s\\S]*?)\\n\\}`))
|
||||
if (!match) throw new Error(`Bitbybit declaration is missing interface ${name}.`)
|
||||
return match[1]
|
||||
}
|
||||
const hasMethod = (body, name) => new RegExp(`\\n\\s*${name}\\(`).test(body)
|
||||
const assert = (condition, message) => {
|
||||
if (!condition) throw new Error(message)
|
||||
}
|
||||
|
||||
const booleanInterfaces = ['BRepAlgoAPI_Fuse', 'BRepAlgoAPI_Cut', 'BRepAlgoAPI_Common']
|
||||
const booleanHistorySurface = Object.fromEntries(booleanInterfaces.map((name) => {
|
||||
const body = interfaceBody(name)
|
||||
return [name, {
|
||||
generated: hasMethod(body, 'Generated'),
|
||||
modified: hasMethod(body, 'Modified'),
|
||||
deleted: hasMethod(body, 'IsDeleted'),
|
||||
hasGenerated: hasMethod(body, 'HasGenerated')
|
||||
}]
|
||||
}))
|
||||
const builderBody = interfaceBody('BOPAlgo_Builder')
|
||||
const listBody = interfaceBody('TopTools_ListOfShape')
|
||||
|
||||
for (const [name, surface] of Object.entries(booleanHistorySurface)) {
|
||||
assert(!surface.generated && !surface.modified && !surface.deleted, `${name} unexpectedly exposes native Generated/Modified/IsDeleted; review the adapter contract.`)
|
||||
assert(surface.hasGenerated, `${name} must retain HasGenerated for capability reporting.`)
|
||||
}
|
||||
assert(hasMethod(builderBody, 'Modified'), 'BOPAlgo_Builder.Modified(shape) is required for the partial history surface.')
|
||||
for (const method of ['Size', 'IsEmpty', 'Append', 'First']) {
|
||||
assert(hasMethod(listBody, method), `TopTools_ListOfShape.${method}() is required for the partial history surface.`)
|
||||
}
|
||||
|
||||
console.log(JSON.stringify({
|
||||
provider: 'bitbybit-occt',
|
||||
version: packageJson.version,
|
||||
sourceCommit,
|
||||
sourceForm: matrix.bitbybitSource.sourceForm,
|
||||
missingBuildInputs: matrix.bitbybitSource.missingBuildInputs,
|
||||
declarationPath: 'node_modules/@bitbybit-dev/occt/bitbybit-dev-occt/bitbybit-dev-occt.d.ts',
|
||||
nativeBooleanHistory: { generated: false, modified: false, deleted: false },
|
||||
partialBuilderHistory: { modified: true, listIndexAccess: false },
|
||||
nativeExtension: await (async () => {
|
||||
const result = { status: 'not-built' }
|
||||
for (const [key, relativePath, expectedHash] of [
|
||||
['javascript', matrix.nativeOcctHistory.artifact.javascript, matrix.nativeOcctHistory.artifact.javascriptSha256],
|
||||
['wasm', matrix.nativeOcctHistory.artifact.wasm, matrix.nativeOcctHistory.artifact.wasmSha256]
|
||||
]) {
|
||||
try {
|
||||
const bytes = await readFile(resolve(root, relativePath))
|
||||
const hash = createHash('sha256').update(bytes).digest('hex')
|
||||
if (hash !== expectedHash) throw new Error(`${key} hash mismatch: expected ${expectedHash}, got ${hash}.`)
|
||||
result[key] = 'hash-ok'
|
||||
} catch (error) {
|
||||
if (error?.code !== 'ENOENT') throw error
|
||||
}
|
||||
}
|
||||
if (result.javascript === 'hash-ok' && result.wasm === 'hash-ok') result.status = 'built-hash-ok'
|
||||
return result
|
||||
})(),
|
||||
status: 'audit-pass'
|
||||
}, null, 2))
|
||||
@@ -5,7 +5,7 @@ const sourceRoot = new URL('../src/', import.meta.url)
|
||||
const forbidden = [
|
||||
/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)
|
||||
|
||||
68
scripts/freecad-reference-probe.py
Normal file
68
scripts/freecad-reference-probe.py
Normal file
@@ -0,0 +1,68 @@
|
||||
import importlib
|
||||
import json
|
||||
|
||||
import FreeCAD as App
|
||||
|
||||
|
||||
def property_metadata(obj):
|
||||
properties = []
|
||||
for name in obj.PropertiesList:
|
||||
try:
|
||||
type_id = obj.getTypeIdOfProperty(name)
|
||||
except Exception:
|
||||
type_id = "unknown"
|
||||
try:
|
||||
group = obj.getGroupOfProperty(name)
|
||||
except Exception:
|
||||
group = ""
|
||||
try:
|
||||
status = list(obj.getPropertyStatus(name))
|
||||
except Exception:
|
||||
status = []
|
||||
properties.append({"name": name, "typeId": type_id, "group": group, "status": status})
|
||||
return properties
|
||||
|
||||
|
||||
def probe_object(document, type_id):
|
||||
try:
|
||||
obj = document.addObject(type_id, "Reference" + type_id.replace(":", "_"))
|
||||
except Exception as error:
|
||||
return {"typeId": type_id, "available": False, "error": str(error)}
|
||||
return {
|
||||
"typeId": type_id,
|
||||
"available": True,
|
||||
"runtimeTypeId": obj.TypeId,
|
||||
"properties": property_metadata(obj),
|
||||
}
|
||||
|
||||
|
||||
def probe_module(name):
|
||||
try:
|
||||
module = importlib.import_module(name)
|
||||
return {"name": name, "available": True, "file": getattr(module, "__file__", None)}
|
||||
except Exception as error:
|
||||
return {"name": name, "available": False, "error": str(error)}
|
||||
|
||||
|
||||
document = App.newDocument("ReferenceProbe")
|
||||
version = App.Version()
|
||||
result = {
|
||||
"schemaVersion": 1,
|
||||
"baselineId": "freecad-1.1.1",
|
||||
"freecadVersion": ".".join(str(value) for value in version[:3]),
|
||||
"revision": str(version[3]),
|
||||
"gitBranch": str(version[6]) if len(version) > 6 else "",
|
||||
"gitCommit": str(version[7]) if len(version) > 7 else "",
|
||||
"guiUp": bool(getattr(App, "GuiUp", False)),
|
||||
"buildPurpose": "headless-reference-oracle",
|
||||
"modules": [probe_module(name) for name in [
|
||||
"Part", "Material", "Measure", "Sketcher", "PartDesign", "TechDraw",
|
||||
"Spreadsheet", "Draft", "Mesh", "Fem", "CAM", "Assembly",
|
||||
]],
|
||||
"objects": [probe_object(document, type_id) for type_id in [
|
||||
"Part::Box", "Part::Cylinder", "Part::Sphere", "Part::Cone",
|
||||
"Part::Feature", "PartDesign::Feature", "Sketcher::SketchObject",
|
||||
]],
|
||||
}
|
||||
App.closeDocument(document.Name)
|
||||
print("FREECAD_REFERENCE_RESULT=" + json.dumps(result, sort_keys=True, separators=(",", ":")))
|
||||
29
scripts/run-freecad-reference-probe.mjs
Normal file
29
scripts/run-freecad-reference-probe.mjs
Normal file
@@ -0,0 +1,29 @@
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const localOracle = resolve(root, '.cache/freecad/install-native/bin/FreeCADCmd')
|
||||
const candidates = process.env.FREECAD_CMD ? [process.env.FREECAD_CMD] : [localOracle, 'FreeCADCmd', 'freecadcmd']
|
||||
let command = null
|
||||
for (const candidate of candidates) {
|
||||
const probe = spawnSync(candidate, ['--version'], { encoding: 'utf8', timeout: 15000 })
|
||||
if (!probe.error || probe.error.code !== 'ENOENT') { command = candidate; break }
|
||||
}
|
||||
if (!command) throw new Error('FreeCADCmd 1.1.1 is unavailable. Set FREECAD_CMD to the locked oracle executable.')
|
||||
|
||||
const execution = spawnSync(command, [resolve(root, 'scripts/freecad-reference-probe.py')], {
|
||||
cwd: root,
|
||||
encoding: 'utf8',
|
||||
timeout: 120000,
|
||||
maxBuffer: 20 * 1024 * 1024,
|
||||
env: { ...process.env },
|
||||
})
|
||||
const output = `${execution.stdout || ''}\n${execution.stderr || ''}`
|
||||
const resultLine = output.split(/\r?\n/).find((line) => line.startsWith('FREECAD_REFERENCE_RESULT='))
|
||||
if (execution.error || execution.status !== 0 || !resultLine) {
|
||||
throw new Error(`FreeCAD reference probe failed with status ${execution.status}: ${execution.error?.message || output.trim()}`)
|
||||
}
|
||||
const result = JSON.parse(resultLine.slice('FREECAD_REFERENCE_RESULT='.length))
|
||||
if (result.freecadVersion !== '1.1.1') throw new Error(`Expected FreeCAD 1.1.1, received ${result.freecadVersion}`)
|
||||
console.log(JSON.stringify({ command, result }, null, 2))
|
||||
Reference in New Issue
Block a user