Files
Web_FreeCAD_Bitbybit/scripts/run-freecad-property-area-roundtrip.ts
wangdequan d11566403d
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
feat: close ordered pairs and property codec batches
2026-08-17 04:58:46 -04:00

107 lines
7.0 KiB
TypeScript

import { createHash } from 'node:crypto'
import { isDeepStrictEqual } from 'node:util'
import { existsSync } from 'node:fs'
import { mkdir, readFile, writeFile } from 'node:fs/promises'
import { spawnSync } from 'node:child_process'
import { resolve } from 'node:path'
import { unzipSync } from 'fflate'
import { createWebCadFacade } from '../src/facade/mockFacade'
const root = resolve(new URL('..', import.meta.url).pathname)
const executable = process.env.FREECAD_CMD || resolve(root, '.cache/freecad/install-desktop/bin/FreeCADCmd')
const sysroot = resolve(root, '.cache/freecad/sysroot')
const scriptPath = resolve(root, 'scripts/freecad-property-area-roundtrip.py')
const outputDirectory = resolve(root, '.cache/freecad/property-area-roundtrip')
const nativePath = resolve(outputDirectory, 'native-initial.FCStd')
const webPath = resolve(outputDirectory, 'web-edited.FCStd')
const resavedPath = resolve(outputDirectory, 'native-resaved.FCStd')
const reportPath = resolve(root, 'config/freecad-property-area-roundtrip.json')
if (!existsSync(executable)) throw new Error(`FreeCAD PropertyArea roundtrip executable is missing: ${executable}`)
await mkdir(outputDirectory, { recursive: true })
const runNative = (mode: 'create' | 'verify', path: string, resaved = '') => {
const execution = spawnSync(executable, ['--python-path', resolve(sysroot, 'usr/lib/python3/dist-packages'), scriptPath], {
cwd: root,
encoding: 'utf8',
timeout: 120_000,
maxBuffer: 32 * 1024 * 1024,
env: {
...process.env,
FREECAD_PROPERTY_AREA_ROUNDTRIP_MODE: mode,
FREECAD_PROPERTY_AREA_ROUNDTRIP_PATH: path,
FREECAD_PROPERTY_AREA_ROUNDTRIP_RESAVED_PATH: resaved,
PYTHONPATH: `${resolve(sysroot, 'usr/lib/python3/dist-packages')}${process.env.PYTHONPATH ? `:${process.env.PYTHONPATH}` : ''}`,
LD_LIBRARY_PATH: `${resolve(sysroot, 'usr/lib/x86_64-linux-gnu')}${process.env.LD_LIBRARY_PATH ? `:${process.env.LD_LIBRARY_PATH}` : ''}`,
},
})
const output = `${execution.stdout || ''}\n${execution.stderr || ''}`
const marker = 'FREECAD_PROPERTY_AREA_ROUNDTRIP_RESULT='
const line = output.split(/\r?\n/).find((candidate) => candidate.includes(marker))
if (execution.error || execution.status !== 0 || !line) throw new Error(`FreeCAD PropertyArea ${mode} probe failed with status ${execution.status}: ${execution.error?.message || output.trim()}`)
return JSON.parse(line.slice(line.indexOf(marker) + marker.length))
}
const native = runNative('create', nativePath)
const nativeBytes = new Uint8Array(await readFile(nativePath))
const facade = createWebCadFacade({ runtimeMode: 'mock' })
const inspectedBefore = facade.project.fcstd.inspect(nativeBytes)
const initialElements = { schemaVersion: 1 as const, entries: [{ objectId: 'AreaBox', subElement: 'Face1' }] }
const editedElements = { schemaVersion: 1 as const, entries: [{ objectId: 'AreaBox', subElement: 'Face1' }, { objectId: 'AreaBox', subElement: 'Face2' }] }
const editedBytes = facade.project.fcstd.rewriteMeasureArea(nativeBytes, {
objectName: 'AreaProbe',
elements: editedElements,
area: 20,
expectedElements: initialElements,
expectedArea: 10,
})
await facade.project.dispose()
await writeFile(webPath, editedBytes)
const inspectedAfter = facade.project.fcstd.inspect(editedBytes)
const initialFiles = unzipSync(nativeBytes)
const editedFiles = unzipSync(editedBytes)
const opaquePaths = Object.keys(initialFiles).filter((path) => path.toLowerCase() !== 'document.xml')
const opaqueEntriesPreserved = opaquePaths.every((path) => Buffer.from(initialFiles[path]).equals(Buffer.from(editedFiles[path] ?? new Uint8Array())))
const withoutEditedProperties = (inspection: typeof inspectedBefore) => inspection.objects.map((object) => ({ ...object, properties: object.name === 'AreaProbe' ? object.properties.filter((property) => property.name !== 'Area' && property.name !== 'Elements') : object.properties }))
const semanticObjectsPreserved = isDeepStrictEqual(withoutEditedProperties(inspectedBefore), withoutEditedProperties(inspectedAfter))
const nativeAfter = runNative('verify', webPath, resavedPath)
const resavedBytes = new Uint8Array(await readFile(resavedPath))
const expectedNativeElements = [{ object: 'AreaBox', typeId: 'Part::Box', subElements: ['Face1', 'Face2'] }]
const nativeOutputMatches = [nativeAfter.result.reopened, nativeAfter.result.resaved].every((snapshot: any) => snapshot.measure.area.numeric === 20 && isDeepStrictEqual(snapshot.measure.elements, expectedNativeElements))
const measureProperty = (inspection: typeof inspectedBefore, name: string) => inspection.objects.find((object) => object.name === 'AreaProbe')?.properties.find((property) => property.name === name)
const sourceGeometryPreserved = isDeepStrictEqual(native.result.source, nativeAfter.result.reopened.source) && isDeepStrictEqual(native.result.source, nativeAfter.result.resaved.source)
const zeroUnknownDrift = opaqueEntriesPreserved && semanticObjectsPreserved && nativeOutputMatches && sourceGeometryPreserved
const report = {
schemaVersion: 1,
status: zeroUnknownDrift ? 'pass' : 'fail',
baselineId: 'freecad-1.1.1-property-area-roundtrip',
freecadVersion: native.freecadVersion,
gitCommit: native.gitCommit,
archives: {
nativeInitial: { path: '.cache/freecad/property-area-roundtrip/native-initial.FCStd', bytes: nativeBytes.byteLength, sha256: createHash('sha256').update(nativeBytes).digest('hex') },
webEdited: { path: '.cache/freecad/property-area-roundtrip/web-edited.FCStd', bytes: editedBytes.byteLength, sha256: createHash('sha256').update(editedBytes).digest('hex') },
nativeResaved: { path: '.cache/freecad/property-area-roundtrip/native-resaved.FCStd', bytes: resavedBytes.byteLength, sha256: createHash('sha256').update(resavedBytes).digest('hex') },
},
nativeInitial: native.result,
web: {
before: { area: measureProperty(inspectedBefore, 'Area'), elements: measureProperty(inspectedBefore, 'Elements') },
after: { area: measureProperty(inspectedAfter, 'Area'), elements: measureProperty(inspectedAfter, 'Elements') },
objectSetPreserved: inspectedBefore.objects.map((object) => `${object.name}:${object.typeId}`).join('|') === inspectedAfter.objects.map((object) => `${object.name}:${object.typeId}`).join('|'),
semanticObjectsPreserved,
opaquePathsPreserved: opaquePaths.length,
opaqueEntriesPreserved,
},
nativeAfter: nativeAfter.result,
classification: {
webArea: Number(measureProperty(inspectedAfter, 'Area')?.value),
webElementCount: measureProperty(inspectedAfter, 'Elements')?.linkSubs?.length,
reopenedArea: nativeAfter.result.reopened.measure.area.numeric,
resavedArea: nativeAfter.result.resaved.measure.area.numeric,
nativeOutputMatches,
sourceGeometryPreserved,
unknownSemanticDrift: !zeroUnknownDrift,
zeroUnknownDrift,
},
}
await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`)
console.log(JSON.stringify({ status: report.status, output: 'config/freecad-property-area-roundtrip.json', classification: report.classification, opaqueEntriesPreserved: report.web.opaqueEntriesPreserved }, null, 2))