import { spawnSync } from 'node:child_process' import { createHash } from 'node:crypto' import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { fileURLToPath } from 'node:url' import { join, resolve } from 'node:path' import { unzipSync } from 'fflate' import { decodeFcstdPropertyValue, inspectFcstdArchive, rewriteFcstdMetadataArchive, serializeFcstdMetadataArchive } from '../src/facade/fcstd' import { createSketch } from '../src/facade/sketcher' import type { DocumentSnapshot, ShapeResourceValue } from '../src/facade/types' const root = fileURLToPath(new URL('..', import.meta.url)) const executable = resolve(root, '.cache/freecad/install-desktop/bin/FreeCAD') const examplePath = resolve(root, '.cache/freecad/install-desktop/share/examples/PartDesignExample.FCStd') const probePath = resolve(root, 'scripts/freecad-fcstd-gui-probe.py') const pythonPath = resolve(root, '.cache/freecad/sysroot/usr/lib/python3/dist-packages') const libraryPath = resolve(root, '.cache/freecad/sysroot/usr/lib/x86_64-linux-gnu') const matplotlibConfig = resolve(root, '.cache/freecad/sysroot/etc/matplotlibrc') const fail = (message: string): never => { throw new Error(`FreeCAD FCStd native oracle: ${message}`) } const near = (actual: number | null, expected: number, tolerance = 1e-8) => typeof actual === 'number' && Number.isFinite(actual) && Math.abs(actual - expected) <= tolerance type ProbeResult = { freecadVersion: string shapeNull: boolean | null shapeValid: boolean | null solidCount: number | null faceCount: number | null volume: number | null boundingBox: { min: number[]; max: number[] } | null shapeError: string | null properties: Record geometryCount: number | null constraintCount: number | null constructionGeometry: number[] | null constraintNames: string[] | null constraintTypes: string[] | null externalGeometryCount: number | null externalGeoCount: number | null externalTypes: number[] | null externalLinks: Array<{ objectName: string; subElements: string[] }> | null externalStates: Array<{ ref: string; id: number; geometryType: string; flags: number }> | null mapMode: string | null attachmentSupport: Array<{ objectName: string; subElements: string[] }> | null attachmentOffset: { position: number[]; axis: number[]; angle: number } | null } const runFreecadProbe = (archivePath: string, objectName: string, extraEnv: NodeJS.ProcessEnv = {}): ProbeResult => { const child = spawnSync('xvfb-run', ['-a', executable, '--python-path', pythonPath, probePath], { cwd: root, encoding: 'utf8', maxBuffer: 16 * 1024 * 1024, timeout: 30_000, env: { ...process.env, PYTHONPATH: pythonPath, LD_LIBRARY_PATH: libraryPath, MATPLOTLIBRC: matplotlibConfig, MPLBACKEND: 'Agg', FREECAD_FCSTD_PATH: archivePath, FREECAD_FCSTD_OBJECT: objectName, ...extraEnv, }, }) const output = `${child.stdout ?? ''}\n${child.stderr ?? ''}` if (child.status !== 0) fail(`FreeCAD exited with ${child.status}: ${output.trim()}`) if (/Invalid Document\.xml|Reading failed from embedded file|Property .* already exists/i.test(output)) fail(`FreeCAD reported an archive read failure: ${output.trim()}`) const marker = output.match(/FREECAD_FCSTD_GUI_RESULT=(\{[^\r\n]+\})/) if (!marker) fail(`probe result marker is missing: ${output.trim()}`) return JSON.parse(marker[1]) as ProbeResult } const temporaryDirectory = await mkdtemp(join(tmpdir(), 'bitbybit-fcstd-oracle-')) try { const sourceBytes = new Uint8Array(await readFile(examplePath)) const sourceInspection = inspectFcstdArchive(sourceBytes) const proxyPreservedBytes = rewriteFcstdMetadataArchive(sourceBytes, sourceInspection.proxyDocument) const sha256 = (bytes: Uint8Array) => createHash('sha256').update(bytes).digest('hex') const sourceSha256 = sha256(sourceBytes) const proxyPreservedSha256 = sha256(proxyPreservedBytes) if (sourceSha256 !== proxyPreservedSha256 || !Buffer.from(sourceBytes).equals(Buffer.from(proxyPreservedBytes))) fail('read-only proxy archive was not preserved byte-for-byte.') const proxyPreservedPath = join(temporaryDirectory, 'proxy-preserved.fcstd') const proxyResavedPath = join(temporaryDirectory, 'proxy-freecad-resaved.fcstd') await writeFile(proxyPreservedPath, proxyPreservedBytes) const proxyResult = runFreecadProbe(proxyPreservedPath, 'Body', { FREECAD_FCSTD_RESAVE_PATH: proxyResavedPath }) if (proxyResult.freecadVersion !== '1.1.1' || proxyResult.shapeNull !== false || proxyResult.shapeValid !== true || proxyResult.faceCount !== 23) fail(`unexpected byte-preserved proxy archive result: ${JSON.stringify(proxyResult)}`) const proxyResavedInspection = inspectFcstdArchive(new Uint8Array(await readFile(proxyResavedPath))) if (sourceInspection.compatibility.proxyObjects !== 8 || sourceInspection.compatibility.blockedObjects !== 0 || proxyResavedInspection.objects.length !== sourceInspection.objects.length || proxyResavedInspection.compatibility.proxyObjects !== sourceInspection.compatibility.proxyObjects) fail('FreeCAD resave did not preserve the native proxy object inventory.') const sourceObject = sourceInspection.objects.find((object) => object.name === 'Body') ?? fail('PartDesignExample Body is missing.') const sourceShapeProperty = sourceObject.properties.find((property) => property.name === 'Shape') ?? fail('PartDesignExample Body.Shape is missing.') const decoded = decodeFcstdPropertyValue(sourceShapeProperty) if (!decoded.decoded || !decoded.value || typeof decoded.value !== 'object') fail('Body.Shape could not be decoded.') const shapeValue = decoded.value as ShapeResourceValue if (shapeValue.hasherIndex !== 0 || shapeValue.elementMapResource !== 'Body.Shape.Map.txt') fail('Body.Shape native history pointers do not match the locked oracle.') const sourceFiles = unzipSync(sourceBytes) const document: DocumentSnapshot = { id: 'fcstd-native-oracle', label: 'FCStd native oracle', version: 1, dirty: false, readOnly: false, units: 'mm', tree: [{ id: 'Body', label: 'Body', type: 'feature', state: 'up-to-date' }], objects: [{ id: 'Body', typeId: 'Part::Feature', properties: [{ name: 'Shape', label: 'Shape', group: 'Base', scope: 'data', type: 'Part::PropertyPartShape', value: shapeValue }], }], dependencies: [], recompute: { generation: 0, status: 'idle', objectStates: { Body: 'up-to-date' }, dirtyObjects: [], order: [], errors: [] }, } const requiredResources = ['StringHasher.Table.txt', 'Body.Shape.brp', 'Body.Shape.Map.txt'] as const for (const path of requiredResources) if (!sourceFiles[path]) fail(`locked oracle resource is missing: ${path}`) const opaqueEntries: Record = Object.fromEntries(requiredResources.map((path) => [path, new Uint8Array(sourceFiles[path])])) const webArchive = serializeFcstdMetadataArchive(document, { opaqueEntries }) const webArchivePath = join(temporaryDirectory, 'web.fcstd') const resavedPath = join(temporaryDirectory, 'freecad-resaved.fcstd') await writeFile(webArchivePath, webArchive) const result = runFreecadProbe(webArchivePath, 'Body', { FREECAD_FCSTD_RESAVE_PATH: resavedPath }) if (result.freecadVersion !== '1.1.1' || result.shapeNull !== false || result.shapeValid !== true || result.shapeError !== null || result.solidCount !== 1 || result.faceCount !== 23 || !near(result.volume, 583719.3669394433, 1e-6)) fail(`unexpected desktop Shape result: ${JSON.stringify(result)}`) const resavedInspection = inspectFcstdArchive(new Uint8Array(await readFile(resavedPath))) const resavedShape = resavedInspection.objects.find((object) => object.name === 'Body')?.properties.find((property) => property.name === 'Shape')?.shapeResource const resavedMap = resavedInspection.elementMapResources.find((resource) => resource.path === 'Body.Shape.Map.txt') const resavedHasher = resavedInspection.stringHasherResource if (resavedShape?.hasherIndex !== 0 || resavedShape.elementMap !== '1.15.70200.5' || resavedShape.elementMapResource !== 'Body.Shape.Map.txt' || resavedShape.elementMapEntries?.[0]?.key !== 'Dummy' || resavedShape.elementMapEntries[0].value !== 'Dummy') fail('FreeCAD resave did not preserve Shape history pointers.') if (resavedMap?.postfixCount !== 63 || resavedMap.mapCount !== 2 || resavedHasher?.status !== 'available') fail('FreeCAD resave did not preserve ElementMap2/StringHasher resources.') const primitiveDocument: DocumentSnapshot = { id: 'fcstd-native-part-box', label: 'FCStd native Part Box', version: 1, dirty: false, readOnly: false, units: 'mm', tree: ['Box', 'Cylinder', 'Sphere', 'Ellipsoid', 'Cone', 'Torus', 'Prism', 'Wedge', 'Tool', 'Fuse', 'Cut', 'Common'].map((id) => ({ id, label: id, type: 'feature', state: 'up-to-date' })), objects: [ { id: 'Box', typeId: 'Part::Box', properties: [ { name: 'Length', label: 'Length', group: 'Box', scope: 'data', type: 'App::PropertyLength', value: 2 }, { name: 'Width', label: 'Width', group: 'Box', scope: 'data', type: 'App::PropertyLength', value: 3 }, { name: 'Height', label: 'Height', group: 'Box', scope: 'data', type: 'App::PropertyLength', value: 4 }, { name: 'WebMode', label: 'Web mode', group: 'Web', scope: 'data', type: 'App::PropertyEnumeration', value: 'Manufacturing', options: ['Design', 'Manufacturing', 'Inspection'] }, { name: 'WebProgress', label: 'Web progress', group: 'Web', scope: 'data', type: 'App::PropertyPercent', value: 35 }, ] }, { id: 'Cylinder', typeId: 'Part::Cylinder', properties: [ { name: 'Radius', label: 'Radius', group: 'Cylinder', scope: 'data', type: 'App::PropertyLength', value: 2 }, { name: 'Height', label: 'Height', group: 'Cylinder', scope: 'data', type: 'App::PropertyLength', value: 5 }, { name: 'Angle', label: 'Angle', group: 'Cylinder', scope: 'data', type: 'App::PropertyAngle', value: 180 }, ] }, { id: 'Sphere', typeId: 'Part::Sphere', properties: [{ name: 'Radius', label: 'Radius', group: 'Sphere', scope: 'data', type: 'App::PropertyLength', value: 3 }] }, { id: 'Ellipsoid', typeId: 'Part::Ellipsoid', properties: [ { name: 'Radius1', label: 'Z radius', group: 'Ellipsoid', scope: 'data', type: 'App::PropertyLength', value: 2 }, { name: 'Radius2', label: 'X radius', group: 'Ellipsoid', scope: 'data', type: 'App::PropertyLength', value: 4 }, { name: 'Radius3', label: 'Y radius', group: 'Ellipsoid', scope: 'data', type: 'App::PropertyLength', value: 0 }, { name: 'Angle1', label: 'Lower angle', group: 'Ellipsoid', scope: 'data', type: 'App::PropertyAngle', value: -90 }, { name: 'Angle2', label: 'Upper angle', group: 'Ellipsoid', scope: 'data', type: 'App::PropertyAngle', value: 90 }, { name: 'Angle3', label: 'Azimuth angle', group: 'Ellipsoid', scope: 'data', type: 'App::PropertyAngle', value: 360 }, ] }, { id: 'Cone', typeId: 'Part::Cone', properties: [ { name: 'Radius1', label: 'Radius 1', group: 'Cone', scope: 'data', type: 'App::PropertyLength', value: 4 }, { name: 'Radius2', label: 'Radius 2', group: 'Cone', scope: 'data', type: 'App::PropertyLength', value: 2 }, { name: 'Height', label: 'Height', group: 'Cone', scope: 'data', type: 'App::PropertyLength', value: 6 }, { name: 'Angle', label: 'Angle', group: 'Cone', scope: 'data', type: 'App::PropertyAngle', value: 360 }, ] }, { id: 'Torus', typeId: 'Part::Torus', properties: [ { name: 'Radius1', label: 'Major radius', group: 'Torus', scope: 'data', type: 'App::PropertyLength', value: 10 }, { name: 'Radius2', label: 'Minor radius', group: 'Torus', scope: 'data', type: 'App::PropertyLength', value: 2 }, { name: 'Angle1', label: 'Lower angle', group: 'Torus', scope: 'data', type: 'App::PropertyAngle', value: -180 }, { name: 'Angle2', label: 'Upper angle', group: 'Torus', scope: 'data', type: 'App::PropertyAngle', value: 180 }, { name: 'Angle3', label: 'Azimuth angle', group: 'Torus', scope: 'data', type: 'App::PropertyAngle', value: 270 }, ] }, { id: 'Prism', typeId: 'Part::Prism', properties: [ { name: 'Polygon', label: 'Polygon sides', group: 'Prism', scope: 'data', type: 'App::PropertyInteger', value: 6 }, { name: 'Circumradius', label: 'Circumradius', group: 'Prism', scope: 'data', type: 'App::PropertyLength', value: 2 }, { name: 'Height', label: 'Height', group: 'Prism', scope: 'data', type: 'App::PropertyLength', value: 10 }, { name: 'FirstAngle', label: 'First angle', group: 'Prism', scope: 'data', type: 'App::PropertyAngle', value: 10 }, { name: 'SecondAngle', label: 'Second angle', group: 'Prism', scope: 'data', type: 'App::PropertyAngle', value: -5 }, ] }, { id: 'Wedge', typeId: 'Part::Wedge', properties: [ { name: 'Xmin', label: 'X minimum', group: 'Wedge', scope: 'data', type: 'App::PropertyLength', value: 0 }, { name: 'Ymin', label: 'Y minimum', group: 'Wedge', scope: 'data', type: 'App::PropertyLength', value: 0 }, { name: 'Zmin', label: 'Z minimum', group: 'Wedge', scope: 'data', type: 'App::PropertyLength', value: 0 }, { name: 'X2min', label: 'X2 minimum', group: 'Wedge', scope: 'data', type: 'App::PropertyLength', value: 0 }, { name: 'Z2min', label: 'Z2 minimum', group: 'Wedge', scope: 'data', type: 'App::PropertyLength', value: 0 }, { name: 'Xmax', label: 'X maximum', group: 'Wedge', scope: 'data', type: 'App::PropertyLength', value: 10 }, { name: 'Ymax', label: 'Y maximum', group: 'Wedge', scope: 'data', type: 'App::PropertyLength', value: 10 }, { name: 'Zmax', label: 'Z maximum', group: 'Wedge', scope: 'data', type: 'App::PropertyLength', value: 10 }, { name: 'X2max', label: 'X2 maximum', group: 'Wedge', scope: 'data', type: 'App::PropertyLength', value: 8 }, { name: 'Z2max', label: 'Z2 maximum', group: 'Wedge', scope: 'data', type: 'App::PropertyLength', value: 8 }, ] }, { id: 'Tool', typeId: 'Part::Box', properties: [ { name: 'Length', label: 'Length', group: 'Box', scope: 'data', type: 'App::PropertyLength', value: 1 }, { name: 'Width', label: 'Width', group: 'Box', scope: 'data', type: 'App::PropertyLength', value: 1 }, { name: 'Height', label: 'Height', group: 'Box', scope: 'data', type: 'App::PropertyLength', value: 1 }, ] }, ...['Fuse', 'Cut', 'Common'].map((id) => ({ id, typeId: `Part::${id}`, properties: [ { name: 'Base', label: 'Base', group: 'Boolean', scope: 'data' as const, type: 'App::PropertyLink' as const, value: 'Box' }, { name: 'Tool', label: 'Tool', group: 'Boolean', scope: 'data' as const, type: 'App::PropertyLink' as const, value: 'Tool' }, { name: 'Refine', label: 'Refine shape', group: 'Boolean', scope: 'data' as const, type: 'App::PropertyBool' as const, value: id === 'Common' }, ] })), ], dependencies: ['Fuse', 'Cut', 'Common'].flatMap((sourceId) => [ { sourceId, targetId: 'Box', relation: 'link' as const, propertyName: 'Base' }, { sourceId, targetId: 'Tool', relation: 'link' as const, propertyName: 'Tool' }, ]), recompute: { generation: 0, status: 'idle', objectStates: Object.fromEntries(['Box', 'Cylinder', 'Sphere', 'Ellipsoid', 'Cone', 'Torus', 'Prism', 'Wedge', 'Tool', 'Fuse', 'Cut', 'Common'].map((id) => [id, 'up-to-date' as const])), dirtyObjects: [], order: [], errors: [] }, } const primitiveArchivePath = join(temporaryDirectory, 'native-part-box.fcstd') const primitiveResavedPath = join(temporaryDirectory, 'native-part-resaved.fcstd') await writeFile(primitiveArchivePath, serializeFcstdMetadataArchive(primitiveDocument)) const primitiveCases = [ { objectName: 'Box', expectedVolume: 24, expectedFaces: 6, propertyNames: 'Length,Width,Height,WebMode,WebProgress', properties: { Length: 2, Width: 3, Height: 4, WebMode: 'Manufacturing', WebProgress: 35 } }, { objectName: 'Cylinder', expectedVolume: 10 * Math.PI, propertyNames: 'Radius,Height,Angle', properties: { Radius: 2, Height: 5, Angle: 180 } }, { objectName: 'Sphere', expectedVolume: 36 * Math.PI, propertyNames: 'Radius', properties: { Radius: 3 } }, { objectName: 'Ellipsoid', expectedVolume: 133.9826640573845, expectedFaces: 1, propertyNames: 'Radius1,Radius2,Radius3,Angle1,Angle2,Angle3', properties: { Radius1: 2, Radius2: 4, Radius3: 0, Angle1: -90, Angle2: 90, Angle3: 360 } }, { objectName: 'Cone', expectedVolume: 56 * Math.PI, propertyNames: 'Radius1,Radius2,Height,Angle', properties: { Radius1: 4, Radius2: 2, Height: 6, Angle: 360 } }, { objectName: 'Torus', expectedVolume: 60 * Math.PI ** 2, propertyNames: 'Radius1,Radius2,Angle1,Angle2,Angle3', properties: { Radius1: 10, Radius2: 2, Angle1: -180, Angle2: 180, Angle3: 270 } }, { objectName: 'Prism', expectedVolume: 60 * Math.sqrt(3), expectedFaces: 8, propertyNames: 'Polygon,Circumradius,Height,FirstAngle,SecondAngle', properties: { Polygon: 6, Circumradius: 2, Height: 10, FirstAngle: 10, SecondAngle: -5 } }, { objectName: 'Wedge', expectedVolume: 2440 / 3, expectedFaces: 6, propertyNames: 'Xmin,Ymin,Zmin,Z2min,X2min,Xmax,Ymax,Zmax,Z2max,X2max', properties: { Xmin: 0, Ymin: 0, Zmin: 0, Z2min: 0, X2min: 0, Xmax: 10, Ymax: 10, Zmax: 10, Z2max: 8, X2max: 8 } }, { objectName: 'Fuse', expectedVolume: 24, propertyNames: 'Base,Tool,Refine', properties: { Base: 'Box', Tool: 'Tool', Refine: false } }, { objectName: 'Cut', expectedVolume: 23, propertyNames: 'Base,Tool,Refine', properties: { Base: 'Box', Tool: 'Tool', Refine: false } }, { objectName: 'Common', expectedVolume: 1, propertyNames: 'Base,Tool,Refine', properties: { Base: 'Box', Tool: 'Tool', Refine: true } }, ] const primitiveResults: Record = {} for (const oracleCase of primitiveCases) { const primitiveResult = runFreecadProbe(primitiveArchivePath, oracleCase.objectName, { FREECAD_FCSTD_RECOMPUTE: '1', FREECAD_FCSTD_PROPERTIES: oracleCase.propertyNames, ...(oracleCase.objectName === 'Common' ? { FREECAD_FCSTD_RESAVE_PATH: primitiveResavedPath } : {}) }) primitiveResults[oracleCase.objectName] = primitiveResult if (primitiveResult.freecadVersion !== '1.1.1' || primitiveResult.shapeNull !== false || primitiveResult.shapeValid !== true || primitiveResult.shapeError !== null || primitiveResult.solidCount !== 1 || (oracleCase.expectedFaces !== undefined && primitiveResult.faceCount !== oracleCase.expectedFaces) || !near(primitiveResult.volume, oracleCase.expectedVolume, 1e-8)) fail(`unexpected native Part::${oracleCase.objectName} result: ${JSON.stringify(primitiveResult)}`) if (Object.entries(oracleCase.properties).some(([name, expected]) => primitiveResult.properties[name] !== expected) || Object.keys(primitiveResult.properties).length !== Object.keys(oracleCase.properties).length) fail(`native Part::${oracleCase.objectName} properties did not restore: ${JSON.stringify(primitiveResult.properties)}`) } const primitiveResaved = inspectFcstdArchive(new Uint8Array(await readFile(primitiveResavedPath))) const decodedProperty = (objectName: string, propertyName: string) => { const property = primitiveResaved.objects.find((object) => object.name === objectName)?.properties.find((candidate) => candidate.name === propertyName) ?? fail(`FreeCAD resave is missing ${objectName}.${propertyName}.`) const decodedPropertyValue = decodeFcstdPropertyValue(property) if (!decodedPropertyValue.decoded) fail(`FreeCAD resave could not decode ${objectName}.${propertyName}: ${decodedPropertyValue.error ?? 'unknown error'}`) return decodedPropertyValue.value } if (decodedProperty('Box', 'Length') !== 2 || decodedProperty('Box', 'Width') !== 3 || decodedProperty('Box', 'Height') !== 4 || decodedProperty('Box', 'WebMode') !== 'Manufacturing' || decodedProperty('Box', 'WebProgress') !== 35) fail('Web inspector did not recover the resaved Box parameters.') const prismRoundTripProperties = Object.fromEntries(['Polygon', 'Circumradius', 'Height', 'FirstAngle', 'SecondAngle'].map((name) => { const native = primitiveResaved.objects.find((object) => object.name === 'Prism')?.properties.find((property) => property.name === name) return [name, { typeId: native?.typeId, value: decodedProperty('Prism', name) }] })) if (prismRoundTripProperties.Polygon.value !== 6 || prismRoundTripProperties.Circumradius.value !== 2 || prismRoundTripProperties.Height.value !== 10 || prismRoundTripProperties.FirstAngle.value !== 10 || prismRoundTripProperties.SecondAngle.value !== -5) fail(`Web inspector did not recover the resaved Prism parameters: ${JSON.stringify(prismRoundTripProperties)}`) const wedgeValues = { Xmin: 0, Ymin: 0, Zmin: 0, Z2min: 0, X2min: 0, Xmax: 10, Ymax: 10, Zmax: 10, Z2max: 8, X2max: 8 } const wedgeRoundTripProperties = Object.fromEntries(Object.keys(wedgeValues).map((name) => { const native = primitiveResaved.objects.find((object) => object.name === 'Wedge')?.properties.find((property) => property.name === name) return [name, { typeId: native?.typeId, value: decodedProperty('Wedge', name) }] })) if (Object.entries(wedgeValues).some(([name, expected]) => wedgeRoundTripProperties[name].value !== expected)) fail(`Web inspector did not recover the resaved Wedge parameters: ${JSON.stringify(wedgeRoundTripProperties)}`) const ellipsoidValues = { Radius1: 2, Radius2: 4, Radius3: 0, Angle1: -90, Angle2: 90, Angle3: 360 } const ellipsoidRoundTripProperties = Object.fromEntries(Object.keys(ellipsoidValues).map((name) => { const native = primitiveResaved.objects.find((object) => object.name === 'Ellipsoid')?.properties.find((property) => property.name === name) return [name, { typeId: native?.typeId, value: decodedProperty('Ellipsoid', name) }] })) if (Object.entries(ellipsoidValues).some(([name, expected]) => ellipsoidRoundTripProperties[name].value !== expected)) fail(`Web inspector did not recover the resaved Ellipsoid parameters: ${JSON.stringify(ellipsoidRoundTripProperties)}`) for (const booleanName of ['Fuse', 'Cut', 'Common']) if (decodedProperty(booleanName, 'Base') !== 'Box' || decodedProperty(booleanName, 'Tool') !== 'Tool' || decodedProperty(booleanName, 'Refine') !== (booleanName === 'Common')) fail(`Web inspector did not recover the resaved ${booleanName} links and Refine value.`) for (const objectName of ['Box', 'Cylinder', 'Sphere', 'Ellipsoid', 'Cone', 'Torus', 'Prism', 'Wedge', 'Tool', 'Fuse', 'Cut', 'Common']) { const shape = primitiveResaved.objects.find((object) => object.name === objectName)?.properties.find((property) => property.name === 'Shape')?.shapeResource if (!shape || !primitiveResaved.shapeResources.some((resource) => resource.path === shape.path && resource.status === 'available')) fail(`FreeCAD resave did not produce an available ${objectName}.Shape resource.`) } const throughHoleDocument: DocumentSnapshot = { id: 'fcstd-native-part-cut-through-hole', label: 'FCStd native Part Cut through hole', version: 1, dirty: false, readOnly: false, units: 'mm', tree: ['Base', 'HoleTool', 'CutThroughHole'].map((id) => ({ id, label: id, type: 'feature', state: 'up-to-date' })), objects: [ { id: 'Base', typeId: 'Part::Box', properties: [ { name: 'Length', label: 'Length', group: 'Box', scope: 'data', type: 'App::PropertyLength', value: 20 }, { name: 'Width', label: 'Width', group: 'Box', scope: 'data', type: 'App::PropertyLength', value: 20 }, { name: 'Height', label: 'Height', group: 'Box', scope: 'data', type: 'App::PropertyLength', value: 10 }, ] }, { id: 'HoleTool', typeId: 'Part::Cylinder', properties: [ { name: 'Radius', label: 'Radius', group: 'Cylinder', scope: 'data', type: 'App::PropertyLength', value: 3 }, { name: 'Height', label: 'Height', group: 'Cylinder', scope: 'data', type: 'App::PropertyLength', value: 12 }, { name: 'Angle', label: 'Angle', group: 'Cylinder', scope: 'data', type: 'App::PropertyAngle', value: 360 }, { name: 'Placement', label: 'Placement', group: 'Base', scope: 'data', type: 'App::PropertyPlacement', value: { position: { x: 10, y: 10, z: -1 }, rotation: { axis: { x: 0, y: 0, z: 1 }, angle: 0 } } }, ] }, { id: 'CutThroughHole', typeId: 'Part::Cut', properties: [ { name: 'Base', label: 'Base', group: 'Boolean', scope: 'data', type: 'App::PropertyLink', value: 'Base' }, { name: 'Tool', label: 'Tool', group: 'Boolean', scope: 'data', type: 'App::PropertyLink', value: 'HoleTool' }, { name: 'Refine', label: 'Refine shape', group: 'Boolean', scope: 'data', type: 'App::PropertyBool', value: false }, ] }, ], dependencies: [ { sourceId: 'CutThroughHole', targetId: 'Base', relation: 'link', propertyName: 'Base' }, { sourceId: 'CutThroughHole', targetId: 'HoleTool', relation: 'link', propertyName: 'Tool' }, ], recompute: { generation: 0, status: 'idle', objectStates: { Base: 'up-to-date', HoleTool: 'up-to-date', CutThroughHole: 'up-to-date' }, dirtyObjects: [], order: [], errors: [] }, } const throughHolePath = join(temporaryDirectory, 'native-part-cut-through-hole.fcstd') const throughHoleResavedPath = join(temporaryDirectory, 'native-part-cut-through-hole-resaved.fcstd') await writeFile(throughHolePath, serializeFcstdMetadataArchive(throughHoleDocument)) const throughHoleResult = runFreecadProbe(throughHolePath, 'CutThroughHole', { FREECAD_FCSTD_RECOMPUTE: '1', FREECAD_FCSTD_PROPERTIES: 'Base,Tool,Refine', FREECAD_FCSTD_RESAVE_PATH: throughHoleResavedPath, }) const throughHoleExpectedVolume = 4000 - 90 * Math.PI if (throughHoleResult.freecadVersion !== '1.1.1' || throughHoleResult.shapeNull !== false || throughHoleResult.shapeValid !== true || throughHoleResult.shapeError !== null || throughHoleResult.solidCount !== 1 || throughHoleResult.faceCount !== 7 || !near(throughHoleResult.volume, throughHoleExpectedVolume, 1e-8) || !throughHoleResult.boundingBox || JSON.stringify(throughHoleResult.boundingBox.min) !== '[0,0,0]' || JSON.stringify(throughHoleResult.boundingBox.max) !== '[20,20,10]') fail(`unexpected native Part::Cut through-hole result: ${JSON.stringify(throughHoleResult)}`) if (throughHoleResult.properties.Base !== 'Base' || throughHoleResult.properties.Tool !== 'HoleTool' || throughHoleResult.properties.Refine !== false || Object.keys(throughHoleResult.properties).length !== 3) fail(`native Part::Cut through-hole links did not restore: ${JSON.stringify(throughHoleResult.properties)}`) const throughHoleResaved = inspectFcstdArchive(new Uint8Array(await readFile(throughHoleResavedPath))) const throughHoleObject = throughHoleResaved.objects.find((object) => object.name === 'CutThroughHole') ?? fail('FreeCAD resave is missing CutThroughHole.') const throughHoleShape = throughHoleObject.properties.find((property) => property.name === 'Shape')?.shapeResource const throughHolePlacement = throughHoleResaved.objects.find((object) => object.name === 'HoleTool')?.properties.find((property) => property.name === 'Placement') if (!throughHoleShape || !throughHoleResaved.shapeResources.some((resource) => resource.path === throughHoleShape.path && resource.status === 'available')) fail('FreeCAD resave did not produce the CutThroughHole.Shape resource.') if (!throughHolePlacement || JSON.stringify(decodeFcstdPropertyValue(throughHolePlacement).value) !== '{"position":{"x":10,"y":10,"z":-1},"rotation":{"axis":{"x":0,"y":0,"z":1},"angle":0}}') fail('Web inspector did not recover the through-hole tool placement.') const sphereTrimPath = join(temporaryDirectory, 'native-sphere-trim.fcstd') const sphereTrimResavedPath = join(temporaryDirectory, 'native-sphere-trim-resaved.fcstd') const sphereTrimProperties = { Radius: 5, Angle1: -45, Angle2: 45, Angle3: 120 } const sphereTrimDocument: DocumentSnapshot = { id: 'fcstd-native-sphere-trim', label: 'FCStd native Sphere trim', version: 1, dirty: false, readOnly: false, units: 'mm', tree: [{ id: 'SphereTrim', label: 'SphereTrim', type: 'feature', state: 'up-to-date' }], objects: [{ id: 'SphereTrim', typeId: 'Part::Sphere', properties: [ { name: 'Radius', label: 'Radius', group: 'Sphere', scope: 'data', type: 'App::PropertyLength', value: sphereTrimProperties.Radius }, { name: 'Angle1', label: 'Lower angle', group: 'Sphere', scope: 'data', type: 'App::PropertyAngle', value: sphereTrimProperties.Angle1 }, { name: 'Angle2', label: 'Upper angle', group: 'Sphere', scope: 'data', type: 'App::PropertyAngle', value: sphereTrimProperties.Angle2 }, { name: 'Angle3', label: 'Azimuth angle', group: 'Sphere', scope: 'data', type: 'App::PropertyAngle', value: sphereTrimProperties.Angle3 }, ] }], dependencies: [], recompute: { generation: 0, status: 'idle', objectStates: { SphereTrim: 'up-to-date' }, dirtyObjects: [], order: [], errors: [] }, } await writeFile(sphereTrimPath, serializeFcstdMetadataArchive(sphereTrimDocument)) const sphereTrimResult = runFreecadProbe(sphereTrimPath, 'SphereTrim', { FREECAD_FCSTD_RECOMPUTE: '1', FREECAD_FCSTD_PROPERTIES: 'Radius,Angle1,Angle2,Angle3', FREECAD_FCSTD_RESAVE_PATH: sphereTrimResavedPath, }) const sphereTrimZ = 5 * Math.sin(Math.PI / 4) const sphereTrimVolume = (Math.PI * (25 * (sphereTrimZ * 2) - (sphereTrimZ ** 3 - (-sphereTrimZ) ** 3) / 3)) / 3 const sphereTrimBounds = { min: [-2.5, 0, -sphereTrimZ], max: [5, 5, sphereTrimZ] } if (sphereTrimResult.freecadVersion !== '1.1.1' || sphereTrimResult.shapeNull !== false || sphereTrimResult.shapeValid !== true || sphereTrimResult.solidCount !== 1 || !near(sphereTrimResult.volume, sphereTrimVolume, 1e-8) || !sphereTrimResult.boundingBox || sphereTrimResult.boundingBox.min.some((value, index) => !near(value, sphereTrimBounds.min[index], 1e-8)) || sphereTrimResult.boundingBox.max.some((value, index) => !near(value, sphereTrimBounds.max[index], 1e-8))) fail(`unexpected native Part::Sphere trim result: ${JSON.stringify(sphereTrimResult)}`) if (Object.entries(sphereTrimProperties).some(([name, expected]) => sphereTrimResult.properties[name] !== expected)) fail(`native Part::Sphere trim properties did not restore: ${JSON.stringify(sphereTrimResult.properties)}`) const sphereTrimResaved = inspectFcstdArchive(new Uint8Array(await readFile(sphereTrimResavedPath))) const sphereTrimObject = sphereTrimResaved.objects.find((object) => object.name === 'SphereTrim') ?? fail('FreeCAD resave is missing SphereTrim.') for (const [name, expected] of Object.entries(sphereTrimProperties)) { const property = sphereTrimObject.properties.find((candidate) => candidate.name === name) ?? fail(`FreeCAD resave is missing SphereTrim.${name}.`) const decodedValue = decodeFcstdPropertyValue(property) if (!decodedValue.decoded || decodedValue.value !== expected) fail(`Web inspector did not recover SphereTrim.${name}.`) } const featureProfile = createSketch('FeatureProfile', [ { id: 'profile-bottom', type: 'line', start: { x: 2, y: 0 }, end: { x: 4, y: 0 } }, { id: 'profile-right', type: 'line', start: { x: 4, y: 0 }, end: { x: 4, y: 3 } }, { id: 'profile-top', type: 'line', start: { x: 4, y: 3 }, end: { x: 2, y: 3 } }, { id: 'profile-left', type: 'line', start: { x: 2, y: 3 }, end: { x: 2, y: 0 } }, ]) const partFeatureDocument: DocumentSnapshot = { id: 'fcstd-native-part-features', label: 'FCStd native Part features', version: 1, dirty: false, readOnly: false, units: 'mm', tree: [ { id: 'FeatureProfile', label: 'FeatureProfile', type: 'sketch', state: 'up-to-date' }, { id: 'Extrusion', label: 'Extrusion', type: 'feature', state: 'up-to-date' }, { id: 'Revolution', label: 'Revolution', type: 'feature', state: 'up-to-date' }, ], objects: [ { id: 'FeatureProfile', typeId: 'Sketcher::SketchObject', properties: [], sketch: featureProfile }, { id: 'Extrusion', typeId: 'Part::Extrusion', properties: [ { name: 'Base', label: 'Base', group: 'Extrude', scope: 'data', type: 'App::PropertyLink', value: 'FeatureProfile' }, { name: 'Dir', label: 'Direction', group: 'Extrude', scope: 'data', type: 'App::PropertyVector', value: { x: 0, y: 0, z: 1 } }, { name: 'DirMode', label: 'Direction mode', group: 'Extrude', scope: 'data', type: 'App::PropertyEnumeration', value: 'Custom', options: ['Custom', 'Edge', 'Normal'] }, { name: 'LengthFwd', label: 'Forward length', group: 'Extrude', scope: 'data', type: 'App::PropertyDistance', value: 5 }, { name: 'LengthRev', label: 'Reverse length', group: 'Extrude', scope: 'data', type: 'App::PropertyDistance', value: 0 }, { name: 'Solid', label: 'Solid', group: 'Extrude', scope: 'data', type: 'App::PropertyBool', value: true }, { name: 'Reversed', label: 'Reversed', group: 'Extrude', scope: 'data', type: 'App::PropertyBool', value: false }, { name: 'Symmetric', label: 'Symmetric', group: 'Extrude', scope: 'data', type: 'App::PropertyBool', value: false }, { name: 'TaperAngle', label: 'Taper angle', group: 'Extrude', scope: 'data', type: 'App::PropertyAngle', value: 0 }, { name: 'TaperAngleRev', label: 'Reverse taper angle', group: 'Extrude', scope: 'data', type: 'App::PropertyAngle', value: 0 }, ] }, { id: 'Revolution', typeId: 'Part::Revolution', properties: [ { name: 'Source', label: 'Source', group: 'Revolve', scope: 'data', type: 'App::PropertyLink', value: 'FeatureProfile' }, { name: 'Base', label: 'Axis base', group: 'Revolve', scope: 'data', type: 'App::PropertyVector', value: { x: 0, y: 0, z: 0 } }, { name: 'Axis', label: 'Axis', group: 'Revolve', scope: 'data', type: 'App::PropertyVector', value: { x: 0, y: 1, z: 0 } }, { name: 'Angle', label: 'Angle', group: 'Revolve', scope: 'data', type: 'App::PropertyAngle', value: 360 }, { name: 'Symmetric', label: 'Symmetric', group: 'Revolve', scope: 'data', type: 'App::PropertyBool', value: false }, { name: 'Solid', label: 'Solid', group: 'Revolve', scope: 'data', type: 'App::PropertyBool', value: true }, ] }, ], dependencies: [ { sourceId: 'Extrusion', targetId: 'FeatureProfile', relation: 'link', propertyName: 'Base' }, { sourceId: 'Revolution', targetId: 'FeatureProfile', relation: 'link', propertyName: 'Source' }, ], recompute: { generation: 0, status: 'idle', objectStates: { FeatureProfile: 'up-to-date', Extrusion: 'up-to-date', Revolution: 'up-to-date' }, dirtyObjects: [], order: [], errors: [] }, } const partFeaturePath = join(temporaryDirectory, 'native-part-features.fcstd') const partFeatureResavedPath = join(temporaryDirectory, 'native-part-features-resaved.fcstd') await writeFile(partFeaturePath, serializeFcstdMetadataArchive(partFeatureDocument)) const extrusionResult = runFreecadProbe(partFeaturePath, 'Extrusion', { FREECAD_FCSTD_RECOMPUTE: '1', FREECAD_FCSTD_PROPERTIES: 'Base,DirMode,LengthFwd,LengthRev,Solid,Reversed,Symmetric,TaperAngle,TaperAngleRev' }) if (extrusionResult.freecadVersion !== '1.1.1' || extrusionResult.shapeNull !== false || extrusionResult.shapeValid !== true || extrusionResult.shapeError !== null || extrusionResult.solidCount !== 1 || !near(extrusionResult.volume, 30, 1e-8)) fail(`unexpected native Part::Extrusion result: ${JSON.stringify(extrusionResult)}`) if (JSON.stringify(extrusionResult.properties) !== '{"Base":"FeatureProfile","DirMode":"Custom","LengthFwd":5,"LengthRev":0,"Reversed":false,"Solid":true,"Symmetric":false,"TaperAngle":0,"TaperAngleRev":0}') fail(`native Part::Extrusion properties did not restore: ${JSON.stringify(extrusionResult.properties)}`) const revolutionResult = runFreecadProbe(partFeaturePath, 'Revolution', { FREECAD_FCSTD_RECOMPUTE: '1', FREECAD_FCSTD_PROPERTIES: 'Source,Angle,Symmetric,Solid' }) if (revolutionResult.freecadVersion !== '1.1.1' || revolutionResult.shapeNull !== false || revolutionResult.shapeValid !== true || revolutionResult.shapeError !== null || revolutionResult.solidCount !== 1 || !near(revolutionResult.volume, 36 * Math.PI, 1e-8)) fail(`unexpected native Part::Revolution result: ${JSON.stringify(revolutionResult)}`) if (JSON.stringify(revolutionResult.properties) !== '{"Angle":360,"Solid":true,"Source":"FeatureProfile","Symmetric":false}') fail(`native Part::Revolution properties did not restore: ${JSON.stringify(revolutionResult.properties)}`) const revolutionResavedResult = runFreecadProbe(partFeaturePath, 'Revolution', { FREECAD_FCSTD_RECOMPUTE: '1', FREECAD_FCSTD_PROPERTIES: 'Source,Angle,Symmetric,Solid', FREECAD_FCSTD_RESAVE_PATH: partFeatureResavedPath }) if (revolutionResavedResult.shapeValid !== true) fail('FreeCAD did not resave the verified native Part::Revolution document.') const partFeatureResaved = inspectFcstdArchive(new Uint8Array(await readFile(partFeatureResavedPath))) const resavedFeatureValue = (objectName: string, propertyName: string) => { const property = partFeatureResaved.objects.find((object) => object.name === objectName)?.properties.find((candidate) => candidate.name === propertyName) ?? fail(`FreeCAD resave is missing ${objectName}.${propertyName}.`) const decodedValue = decodeFcstdPropertyValue(property) if (!decodedValue.decoded) fail(`Web inspector could not decode ${objectName}.${propertyName}.`) return decodedValue.value } if (partFeatureResaved.objects.find((object) => object.name === 'Extrusion')?.typeId !== 'Part::Extrusion' || resavedFeatureValue('Extrusion', 'Base') !== 'FeatureProfile' || resavedFeatureValue('Extrusion', 'LengthFwd') !== 5 || resavedFeatureValue('Extrusion', 'Solid') !== true) fail('Web inspector did not recover the resaved native Part::Extrusion schema.') if (partFeatureResaved.objects.find((object) => object.name === 'Revolution')?.typeId !== 'Part::Revolution' || resavedFeatureValue('Revolution', 'Source') !== 'FeatureProfile' || resavedFeatureValue('Revolution', 'Angle') !== 360 || resavedFeatureValue('Revolution', 'Solid') !== true) fail('Web inspector did not recover the resaved native Part::Revolution schema.') for (const objectName of ['Extrusion', 'Revolution']) { const shape = partFeatureResaved.objects.find((object) => object.name === objectName)?.properties.find((property) => property.name === 'Shape')?.shapeResource if (!shape || !partFeatureResaved.shapeResources.some((resource) => resource.path === shape.path && resource.status === 'available')) fail(`FreeCAD resave did not produce an available ${objectName}.Shape resource.`) } const sketch = createSketch('Sketch', [ { id: 'bottom', type: 'line', start: { x: 0, y: 0 }, end: { x: 20, y: 0 } }, { id: 'right', type: 'line', start: { x: 20, y: 0 }, end: { x: 20, y: 10 } }, { id: 'top', type: 'line', start: { x: 20, y: 10 }, end: { x: 0, y: 10 } }, { id: 'left', type: 'line', start: { x: 0, y: 10 }, end: { x: 0, y: 0 } }, { id: 'construction-axis', type: 'line', start: { x: 10, y: 0 }, end: { x: 10, y: 10 }, construction: true }, { id: 'reference-circle', type: 'circle', center: { x: 30, y: 5 }, radius: 2 }, { id: 'datum-point', type: 'point', position: { x: 35, y: 5 } }, { id: 'profile-arc', type: 'arc', center: { x: 40, y: 5 }, radius: 2, startAngle: 0, endAngle: Math.PI }, { id: 'profile-ellipse', type: 'ellipse', center: { x: 47, y: 5 }, majorRadius: 3, minorRadius: 1.5, rotation: 0.25 }, { id: 'profile-spline', type: 'bspline', degree: 2, controlPoints: [{ x: 52, y: 3 }, { x: 54, y: 7 }, { x: 56, y: 3 }], weights: [1, 1, 1], knots: [0, 0, 0, 1, 1, 1], periodic: false }, ], [ { id: 'bottom-horizontal', type: 'horizontal', geometryId: 'bottom' }, { id: 'right-vertical', type: 'vertical', geometryId: 'right' }, { id: 'top-horizontal', type: 'horizontal', geometryId: 'top' }, { id: 'left-vertical', type: 'vertical', geometryId: 'left' }, { id: 'join-bottom-right', type: 'coincident', first: { geometryId: 'bottom', point: 'end' }, second: { geometryId: 'right', point: 'start' } }, { id: 'join-right-top', type: 'coincident', first: { geometryId: 'right', point: 'end' }, second: { geometryId: 'top', point: 'start' } }, { id: 'join-top-left', type: 'coincident', first: { geometryId: 'top', point: 'end' }, second: { geometryId: 'left', point: 'start' } }, { id: 'join-left-bottom', type: 'coincident', first: { geometryId: 'left', point: 'end' }, second: { geometryId: 'bottom', point: 'start' } }, { id: 'axis-vertical', type: 'vertical', geometryId: 'construction-axis' }, { id: 'circle-reference-radius', type: 'radius', geometryId: 'reference-circle', value: 2, driving: false }, { id: 'external-point-on-edge', type: 'pointOnObject', point: { geometryId: 'datum-point', point: 'position' }, geometryId: 'external-box-edge-projection' }, ]) const externalSource = { schemaVersion: 1 as const, objectId: 'ExternalBox', kind: 'edge' as const, persistentId: 'Edge1', topologyVersion: 3, generation: 2, status: 'stable' as const, signature: 'native-box-edge-1' } const supportSource = { schemaVersion: 1 as const, objectId: 'ExternalBox', kind: 'face' as const, persistentId: 'Face1', topologyVersion: 3, generation: 2, status: 'stable' as const, signature: 'native-box-face-1' } sketch.externalGeometry = [{ id: 'external-box-edge', source: externalSource, projection: { id: 'external-box-edge-projection', type: 'line', start: { x: 0, y: 0 }, end: { x: 20, y: 0 }, construction: true }, construction: true, }] const sketchDocument: DocumentSnapshot = { id: 'fcstd-native-sketch', label: 'FCStd native Sketch', version: 1, dirty: false, readOnly: false, units: 'mm', tree: [{ id: 'ExternalBox', label: 'ExternalBox', type: 'feature', state: 'up-to-date' }, { id: 'Sketch', label: 'Sketch', type: 'sketch', state: 'up-to-date' }], objects: [ { id: 'ExternalBox', typeId: 'Part::Box', properties: [ { name: 'Length', label: 'Length', group: 'Box', scope: 'data', type: 'App::PropertyLength', value: 20 }, { name: 'Width', label: 'Width', group: 'Box', scope: 'data', type: 'App::PropertyLength', value: 10 }, { name: 'Height', label: 'Height', group: 'Box', scope: 'data', type: 'App::PropertyLength', value: 1 }, ] }, { id: 'Sketch', typeId: 'Sketcher::SketchObject', properties: [ { name: 'Support', label: 'Support', group: 'Attachment', scope: 'data', type: 'App::PropertyLink', value: { objectId: 'ExternalBox', subElement: supportSource } }, { name: 'MapMode', label: 'Map mode', group: 'Attachment', scope: 'data', type: 'App::PropertyEnumeration', value: 'FlatFace', options: ['Deactivated', 'ObjectXY', 'ObjectXZ', 'ObjectYZ', 'FlatFace', 'NormalToEdge'] }, { name: 'AttachmentOffset', label: 'Attachment offset', group: 'Attachment', scope: 'data', type: 'App::PropertyPlacement', value: { position: { x: 0, y: 0, z: 0.25 }, rotation: { axis: { x: 0, y: 0, z: 1 }, angle: 0 } } }, ], sketch }, ], dependencies: [{ sourceId: 'Sketch', targetId: 'ExternalBox', relation: 'topo-ref', propertyName: 'ExternalGeometry:external-box-edge', reference: 'Edge1' }], recompute: { generation: 0, status: 'idle', objectStates: { ExternalBox: 'up-to-date', Sketch: 'up-to-date' }, dirtyObjects: [], order: [], errors: [] }, } const sketchArchivePath = join(temporaryDirectory, 'native-sketch.fcstd') const sketchResavedPath = join(temporaryDirectory, 'native-sketch-resaved.fcstd') await writeFile(sketchArchivePath, serializeFcstdMetadataArchive(sketchDocument)) const sketchResult = runFreecadProbe(sketchArchivePath, 'Sketch', { FREECAD_FCSTD_RECOMPUTE: '1', FREECAD_FCSTD_PROPERTIES: 'WebGeometryIds,WebExternalIds,WebExternalProjectionIds', FREECAD_FCSTD_RESAVE_PATH: sketchResavedPath }) const expectedConstraintNames = sketch.constraints.map((constraint) => constraint.id) if (sketchResult.freecadVersion !== '1.1.1' || sketchResult.geometryCount !== 10 || sketchResult.constraintCount !== 11 || sketchResult.externalGeometryCount !== 1 || sketchResult.externalGeoCount !== 3 || JSON.stringify(sketchResult.externalTypes) !== '[0]' || JSON.stringify(sketchResult.externalLinks) !== '[{"objectName":"ExternalBox","subElements":["Edge1"]}]' || sketchResult.mapMode !== 'FlatFace' || JSON.stringify(sketchResult.attachmentSupport) !== '[{"objectName":"ExternalBox","subElements":["Face1"]}]' || JSON.stringify(sketchResult.attachmentOffset?.position) !== '[0,0,0.25]' || !near(sketchResult.attachmentOffset?.angle ?? null, 0) || JSON.stringify(sketchResult.constructionGeometry) !== '[4]' || JSON.stringify(sketchResult.constraintNames) !== JSON.stringify(expectedConstraintNames) || JSON.stringify(sketchResult.properties.WebGeometryIds) !== JSON.stringify(sketch.geometry.map((geometry) => geometry.id)) || JSON.stringify(sketchResult.properties.WebExternalIds) !== '["external-box-edge"]' || JSON.stringify(sketchResult.properties.WebExternalProjectionIds) !== '["external-box-edge-projection"]') fail(`unexpected native Sketch result: ${JSON.stringify(sketchResult)}`) const resavedSketchInspection = inspectFcstdArchive(new Uint8Array(await readFile(sketchResavedPath))) const resavedSketchObject = resavedSketchInspection.objects.find((object) => object.name === 'Sketch') ?? fail('Web inspector did not find the resaved native Sketch object.') const resavedSketch = resavedSketchObject.sketch ?? fail('Web inspector did not reconstruct the resaved native Sketch.') if (JSON.stringify(resavedSketch.geometry.map((geometry) => geometry.id)) !== JSON.stringify(sketch.geometry.map((geometry) => geometry.id)) || JSON.stringify(resavedSketch.constraints.map((constraint) => constraint.id)) !== JSON.stringify(expectedConstraintNames) || resavedSketch.geometry[4]?.construction !== true || resavedSketch.externalGeometry[0]?.id !== 'external-box-edge' || resavedSketch.externalGeometry[0]?.projection.id !== 'external-box-edge-projection' || JSON.stringify(resavedSketch.externalGeometry[0]?.source) !== JSON.stringify(externalSource)) fail('Web inspector did not preserve native Sketch IDs, external TopoRef, or construction state.') const resavedMapMode = resavedSketchObject.properties.find((property) => property.name === 'MapMode') ?? fail('FreeCAD resave did not preserve Sketch.MapMode.') const resavedAttachmentSupport = resavedSketchObject.properties.find((property) => property.name === 'AttachmentSupport') ?? fail('FreeCAD resave did not preserve Sketch.AttachmentSupport.') if (decodeFcstdPropertyValue(resavedMapMode).value !== 'FlatFace' || JSON.stringify(decodeFcstdPropertyValue(resavedAttachmentSupport).value) !== '{"schemaVersion":1,"entries":[{"objectId":"ExternalBox","subElement":"Face1"}]}') fail('Web inspector did not decode the native Sketch attachment fields.') const proxySketch = resavedSketchInspection.proxyDocument.objects.find((object) => object.id === 'Sketch') ?? fail('Web proxy document did not preserve the resaved Sketch.') if (JSON.stringify(proxySketch.properties.find((property) => property.name === 'Support')?.value) !== JSON.stringify({ objectId: 'ExternalBox', subElement: supportSource }) || proxySketch.properties.find((property) => property.name === 'MapMode')?.value !== 'FlatFace') fail('Web proxy document did not recover the exact Support TopoRef or MapMode.') const faceSketch = createSketch('FaceSketch', [{ id: 'datum', type: 'point', position: { x: 0, y: 5 } }]) const faceSource = { schemaVersion: 1 as const, objectId: 'FaceBox', kind: 'face' as const, persistentId: 'Face6', topologyVersion: 5, generation: 4, status: 'stable' as const, signature: 'native-box-face-6' } faceSketch.externalGeometry = [ { id: 'face-left', source: faceSource, projection: { id: 'face-left-projection', type: 'line', start: { x: 0, y: 0 }, end: { x: 0, y: 10 }, construction: true }, construction: true }, { id: 'face-top', source: faceSource, projection: { id: 'face-top-projection', type: 'line', start: { x: 0, y: 10 }, end: { x: 20, y: 10 }, construction: true }, construction: true }, { id: 'face-right', source: faceSource, projection: { id: 'face-right-projection', type: 'line', start: { x: 20, y: 0 }, end: { x: 20, y: 10 }, construction: true }, construction: true }, { id: 'face-bottom', source: faceSource, projection: { id: 'face-bottom-projection', type: 'line', start: { x: 0, y: 0 }, end: { x: 20, y: 0 }, construction: true }, construction: true }, ] faceSketch.constraints = [{ id: 'datum-on-face-left', type: 'pointOnObject', point: { geometryId: 'datum', point: 'position' }, geometryId: 'face-left-projection' }] const faceDocument: DocumentSnapshot = { id: 'fcstd-native-face-projection', label: 'FCStd native Face projection', version: 1, dirty: false, readOnly: false, units: 'mm', tree: [{ id: 'FaceBox', label: 'FaceBox', type: 'feature', state: 'up-to-date' }, { id: 'FaceSketch', label: 'FaceSketch', type: 'sketch', state: 'up-to-date' }], objects: [ { id: 'FaceBox', typeId: 'Part::Box', properties: [ { name: 'Length', label: 'Length', group: 'Box', scope: 'data', type: 'App::PropertyLength', value: 20 }, { name: 'Width', label: 'Width', group: 'Box', scope: 'data', type: 'App::PropertyLength', value: 10 }, { name: 'Height', label: 'Height', group: 'Box', scope: 'data', type: 'App::PropertyLength', value: 1 }, ] }, { id: 'FaceSketch', typeId: 'Sketcher::SketchObject', properties: [], sketch: faceSketch }, ], dependencies: [{ sourceId: 'FaceSketch', targetId: 'FaceBox', relation: 'topo-ref', propertyName: 'ExternalGeometry:face-left', reference: 'Face6' }], recompute: { generation: 0, status: 'idle', objectStates: { FaceBox: 'up-to-date', FaceSketch: 'up-to-date' }, dirtyObjects: [], order: [], errors: [] }, } const faceArchivePath = join(temporaryDirectory, 'native-face-projection.fcstd') const faceResavedPath = join(temporaryDirectory, 'native-face-projection-resaved.fcstd') await writeFile(faceArchivePath, serializeFcstdMetadataArchive(faceDocument)) const faceResult = runFreecadProbe(faceArchivePath, 'FaceSketch', { FREECAD_FCSTD_RECOMPUTE: '1', FREECAD_FCSTD_RESAVE_PATH: faceResavedPath }) if (faceResult.freecadVersion !== '1.1.1' || faceResult.geometryCount !== 1 || faceResult.constraintCount !== 1 || faceResult.externalGeometryCount !== 1 || faceResult.externalGeoCount !== 6 || JSON.stringify(faceResult.externalTypes) !== '[0]' || JSON.stringify(faceResult.externalLinks) !== '[{"objectName":"FaceBox","subElements":["Face6"]}]' || JSON.stringify(faceResult.constraintNames) !== '["datum-on-face-left"]' || JSON.stringify(faceResult.constraintTypes) !== '["PointOnObject"]') fail(`unexpected native Face projection result: ${JSON.stringify(faceResult)}`) const resavedFace = inspectFcstdArchive(new Uint8Array(await readFile(faceResavedPath))).objects.find((object) => object.name === 'FaceSketch')?.sketch ?? fail('Web inspector did not reconstruct the resaved native Face projection Sketch.') if (JSON.stringify(resavedFace.externalGeometry.map((external) => external.id)) !== JSON.stringify(faceSketch.externalGeometry.map((external) => external.id)) || JSON.stringify(resavedFace.externalGeometry.map((external) => external.projection.id)) !== JSON.stringify(faceSketch.externalGeometry.map((external) => external.projection.id)) || resavedFace.externalGeometry.some((external) => JSON.stringify(external.source) !== JSON.stringify(faceSource)) || JSON.stringify(resavedFace.constraints) !== JSON.stringify([{ ...faceSketch.constraints[0], driving: true }])) fail(`Web inspector did not preserve the native Face projection group: ${JSON.stringify(resavedFace)}`) const modeSketch = createSketch('ModeSketch') const intersectionSource = { schemaVersion: 1 as const, objectId: 'ModeBox', kind: 'face' as const, persistentId: 'Face1', topologyVersion: 6, generation: 4, status: 'stable' as const, signature: 'native-box-face-1-intersection' } const bothSource = { schemaVersion: 1 as const, objectId: 'ModeBox', kind: 'face' as const, persistentId: 'Face6', topologyVersion: 6, generation: 4, status: 'stable' as const, signature: 'native-box-face-6-both' } const syncSource = { schemaVersion: 1 as const, objectId: 'ModeBox', kind: 'edge' as const, persistentId: 'Edge1', topologyVersion: 6, generation: 4, status: 'stable' as const, signature: 'native-box-edge-1-sync' } modeSketch.externalGeometry = [ { id: 'intersection-face', source: intersectionSource, projection: { id: 'intersection-face-line', type: 'line', start: { x: 0, y: 0 }, end: { x: 0, y: 10 }, construction: true }, construction: true, mode: 'intersection', defining: true }, { id: 'both-left', source: bothSource, projection: { id: 'both-left-line', type: 'line', start: { x: 0, y: 0 }, end: { x: 0, y: 10 }, construction: true }, construction: true, mode: 'both', defining: true }, { id: 'both-top', source: bothSource, projection: { id: 'both-top-line', type: 'line', start: { x: 0, y: 10 }, end: { x: 20, y: 10 }, construction: true }, construction: true, mode: 'both', defining: true }, { id: 'both-right', source: bothSource, projection: { id: 'both-right-line', type: 'line', start: { x: 20, y: 0 }, end: { x: 20, y: 10 }, construction: true }, construction: true, mode: 'both', defining: true }, { id: 'both-bottom', source: bothSource, projection: { id: 'both-bottom-line', type: 'line', start: { x: 0, y: 0 }, end: { x: 20, y: 0 }, construction: true }, construction: true, mode: 'both', defining: true }, { id: 'frozen-edge', source: syncSource, projection: { id: 'frozen-edge-line', type: 'line', start: { x: 0, y: 0 }, end: { x: 20, y: 0 }, construction: true }, construction: true, frozen: true }, ] const modeDocument: DocumentSnapshot = { id: 'fcstd-native-external-modes', label: 'FCStd native external modes', version: 1, dirty: false, readOnly: false, units: 'mm', tree: [{ id: 'ModeBox', label: 'ModeBox', type: 'feature', state: 'up-to-date' }, { id: 'ModeSketch', label: 'ModeSketch', type: 'sketch', state: 'up-to-date' }], objects: [ { id: 'ModeBox', typeId: 'Part::Box', properties: [ { name: 'Length', label: 'Length', group: 'Box', scope: 'data', type: 'App::PropertyLength', value: 20 }, { name: 'Width', label: 'Width', group: 'Box', scope: 'data', type: 'App::PropertyLength', value: 10 }, { name: 'Height', label: 'Height', group: 'Box', scope: 'data', type: 'App::PropertyLength', value: 1 }, ] }, { id: 'ModeSketch', typeId: 'Sketcher::SketchObject', properties: [], sketch: modeSketch }, ], dependencies: [ { sourceId: 'ModeSketch', targetId: 'ModeBox', relation: 'topo-ref', propertyName: 'ExternalGeometry:intersection-face', reference: 'Face1' }, { sourceId: 'ModeSketch', targetId: 'ModeBox', relation: 'topo-ref', propertyName: 'ExternalGeometry:both-left', reference: 'Face6' }, { sourceId: 'ModeSketch', targetId: 'ModeBox', relation: 'topo-ref', propertyName: 'ExternalGeometry:frozen-edge', reference: 'Edge1' }, ], recompute: { generation: 0, status: 'idle', objectStates: { ModeBox: 'up-to-date', ModeSketch: 'up-to-date' }, dirtyObjects: [], order: [], errors: [] }, } const modeArchivePath = join(temporaryDirectory, 'native-external-modes.fcstd') const modeResavedPath = join(temporaryDirectory, 'native-external-modes-resaved.fcstd') await writeFile(modeArchivePath, serializeFcstdMetadataArchive(modeDocument)) const modeResult = runFreecadProbe(modeArchivePath, 'ModeSketch', { FREECAD_FCSTD_RECOMPUTE: '1', FREECAD_FCSTD_RESAVE_PATH: modeResavedPath }) if (modeResult.freecadVersion !== '1.1.1' || modeResult.geometryCount !== 0 || modeResult.constraintCount !== 0 || modeResult.externalGeometryCount !== 1 || modeResult.externalGeoCount !== 8 || JSON.stringify(modeResult.externalTypes) !== '[1,2,0]' || JSON.stringify(modeResult.externalLinks) !== '[{"objectName":"ModeBox","subElements":["Face1","Face6","Edge1"]}]' || JSON.stringify(modeResult.externalStates?.map((state) => state.flags)) !== '[1,1,1,1,1,2]') fail(`unexpected native external mode result: ${JSON.stringify(modeResult)}`) const resavedModes = inspectFcstdArchive(new Uint8Array(await readFile(modeResavedPath))).objects.find((object) => object.name === 'ModeSketch')?.sketch ?? fail('Web inspector did not reconstruct the resaved native external mode Sketch.') if (JSON.stringify(resavedModes.externalGeometry.map((external) => external.id)) !== JSON.stringify(modeSketch.externalGeometry.map((external) => external.id)) || JSON.stringify(resavedModes.externalGeometry.map((external) => external.projection.id)) !== JSON.stringify(modeSketch.externalGeometry.map((external) => external.projection.id)) || JSON.stringify(resavedModes.externalGeometry.map((external) => external.mode ?? 'projection')) !== '["intersection","both","both","both","both","projection"]' || resavedModes.externalGeometry.slice(0, 5).some((external) => external.defining !== true) || resavedModes.externalGeometry[5].frozen !== true) fail(`Web inspector did not preserve native external modes and flags: ${JSON.stringify(resavedModes)}`) const snellSketch = createSketch('SnellSketch', [ { id: 'incident-ray', type: 'line', start: { x: 0, y: 0 }, end: { x: 5, y: 5 } }, { id: 'refracted-ray', type: 'line', start: { x: 5, y: 5 }, end: { x: 10, y: 2 } }, { id: 'boundary', type: 'line', start: { x: 0, y: 5 }, end: { x: 10, y: 5 }, construction: true }, ], [{ id: 'refraction', type: 'snellsLaw', first: { geometryId: 'incident-ray', point: 'end' }, second: { geometryId: 'refracted-ray', point: 'start' }, boundaryGeometryId: 'boundary', value: 1.2 }]) const snellDocument: DocumentSnapshot = { id: 'fcstd-native-snells-law', label: 'FCStd native SnellsLaw', version: 1, dirty: false, readOnly: false, units: 'mm', tree: [{ id: 'SnellSketch', label: 'SnellSketch', type: 'sketch', state: 'up-to-date' }], objects: [{ id: 'SnellSketch', typeId: 'Sketcher::SketchObject', properties: [], sketch: snellSketch }], dependencies: [], recompute: { generation: 0, status: 'idle', objectStates: { SnellSketch: 'up-to-date' }, dirtyObjects: [], order: [], errors: [] }, } const snellArchivePath = join(temporaryDirectory, 'native-snells-law.fcstd') const snellResavedPath = join(temporaryDirectory, 'native-snells-law-resaved.fcstd') await writeFile(snellArchivePath, serializeFcstdMetadataArchive(snellDocument)) const snellResult = runFreecadProbe(snellArchivePath, 'SnellSketch', { FREECAD_FCSTD_RECOMPUTE: '1', FREECAD_FCSTD_RESAVE_PATH: snellResavedPath }) if (snellResult.freecadVersion !== '1.1.1' || snellResult.geometryCount !== 3 || snellResult.constraintCount !== 1 || JSON.stringify(snellResult.constructionGeometry) !== '[2]' || JSON.stringify(snellResult.constraintNames) !== '["refraction"]' || JSON.stringify(snellResult.constraintTypes) !== '["SnellsLaw"]') fail(`unexpected native SnellsLaw result: ${JSON.stringify(snellResult)}`) const resavedSnell = inspectFcstdArchive(new Uint8Array(await readFile(snellResavedPath))).objects.find((object) => object.name === 'SnellSketch')?.sketch ?? fail('Web inspector did not reconstruct the resaved native SnellsLaw Sketch.') if (JSON.stringify(resavedSnell.geometry.map((geometry) => geometry.id)) !== JSON.stringify(snellSketch.geometry.map((geometry) => geometry.id)) || JSON.stringify(resavedSnell.constraints) !== JSON.stringify([{ ...snellSketch.constraints[0], driving: true }])) fail('Web inspector did not preserve the native SnellsLaw references or value.') const weightSketch = createSketch('WeightSketch', [{ id: 'weighted-spline', type: 'bspline', degree: 2, controlPoints: [{ x: 0, y: 0 }, { x: 3, y: 4 }, { x: 6, y: 0 }], weights: [1, 0.75, 1], knots: [0, 0, 0, 1, 1, 1], periodic: false, }], [{ id: 'middle-weight', type: 'weight', geometryId: 'weighted-spline', controlPointIndex: 1, value: 0.75 }]) const weightDocument: DocumentSnapshot = { id: 'fcstd-native-weight', label: 'FCStd native Weight', version: 1, dirty: false, readOnly: false, units: 'mm', tree: [{ id: 'WeightSketch', label: 'WeightSketch', type: 'sketch', state: 'up-to-date' }], objects: [{ id: 'WeightSketch', typeId: 'Sketcher::SketchObject', properties: [], sketch: weightSketch }], dependencies: [], recompute: { generation: 0, status: 'idle', objectStates: { WeightSketch: 'up-to-date' }, dirtyObjects: [], order: [], errors: [] }, } const weightArchivePath = join(temporaryDirectory, 'native-weight.fcstd') const weightResavedPath = join(temporaryDirectory, 'native-weight-resaved.fcstd') await writeFile(weightArchivePath, serializeFcstdMetadataArchive(weightDocument)) const weightResult = runFreecadProbe(weightArchivePath, 'WeightSketch', { FREECAD_FCSTD_RECOMPUTE: '1', FREECAD_FCSTD_RESAVE_PATH: weightResavedPath }) if (weightResult.freecadVersion !== '1.1.1' || weightResult.geometryCount !== 2 || weightResult.constraintCount !== 2 || JSON.stringify(weightResult.constructionGeometry) !== '[1]' || JSON.stringify(weightResult.constraintNames) !== '["__WebSyntheticAlignment0","middle-weight"]' || JSON.stringify(weightResult.constraintTypes) !== '["InternalAlignment","Weight"]') fail(`unexpected native B-spline Weight result: ${JSON.stringify(weightResult)}`) const resavedWeight = inspectFcstdArchive(new Uint8Array(await readFile(weightResavedPath))).objects.find((object) => object.name === 'WeightSketch')?.sketch ?? fail('Web inspector did not reconstruct the resaved native B-spline Weight Sketch.') if (JSON.stringify(resavedWeight.geometry.map((geometry) => geometry.id)) !== '["weighted-spline"]' || JSON.stringify(resavedWeight.constraints) !== JSON.stringify([{ ...weightSketch.constraints[0], driving: true }])) fail(`Web inspector did not preserve the native B-spline Weight constraint: ${JSON.stringify(resavedWeight)}`) const internalSketch = createSketch('InternalSketch', [ { id: 'ellipse', type: 'ellipse', center: { x: 5, y: 4 }, majorRadius: 3, minorRadius: 2, rotation: 0.25 }, { id: 'spline', type: 'bspline', degree: 2, controlPoints: [{ x: 0, y: 0 }, { x: 2, y: 3 }, { x: 4, y: 2 }, { x: 6, y: 0 }], weights: [1, 0.8, 1.2, 1], knots: [0, 0, 0, 0.5, 1, 1, 1], periodic: false }, ], [ { id: 'ellipse-major', type: 'internalAlignment', geometryId: 'ellipse', internalGeometryIndex: 0, alignmentType: 'ellipse-major' }, { id: 'ellipse-minor', type: 'internalAlignment', geometryId: 'ellipse', internalGeometryIndex: 0, alignmentType: 'ellipse-minor' }, { id: 'ellipse-focus-1', type: 'internalAlignment', geometryId: 'ellipse', internalGeometryIndex: 0, alignmentType: 'ellipse-focus' }, { id: 'ellipse-focus-2', type: 'internalAlignment', geometryId: 'ellipse', internalGeometryIndex: 1, alignmentType: 'ellipse-focus' }, { id: 'spline-middle-knot', type: 'internalAlignment', geometryId: 'spline', internalGeometryIndex: 1, alignmentType: 'bspline-knot' }, ]) const internalDocument: DocumentSnapshot = { id: 'fcstd-native-internal-alignment', label: 'FCStd native InternalAlignment', version: 1, dirty: false, readOnly: false, units: 'mm', tree: [{ id: 'InternalSketch', label: 'InternalSketch', type: 'sketch', state: 'up-to-date' }], objects: [{ id: 'InternalSketch', typeId: 'Sketcher::SketchObject', properties: [], sketch: internalSketch }], dependencies: [], recompute: { generation: 0, status: 'idle', objectStates: { InternalSketch: 'up-to-date' }, dirtyObjects: [], order: [], errors: [] }, } const internalArchivePath = join(temporaryDirectory, 'native-internal-alignment.fcstd') const internalResavedPath = join(temporaryDirectory, 'native-internal-alignment-resaved.fcstd') await writeFile(internalArchivePath, serializeFcstdMetadataArchive(internalDocument)) const internalResult = runFreecadProbe(internalArchivePath, 'InternalSketch', { FREECAD_FCSTD_RECOMPUTE: '1', FREECAD_FCSTD_RESAVE_PATH: internalResavedPath }) if (internalResult.freecadVersion !== '1.1.1' || internalResult.geometryCount !== 7 || internalResult.constraintCount !== 5 || JSON.stringify(internalResult.constructionGeometry) !== '[2,3,4,5,6]' || JSON.stringify(internalResult.constraintNames) !== JSON.stringify(internalSketch.constraints.map((constraint) => constraint.id)) || JSON.stringify(internalResult.constraintTypes) !== '["InternalAlignment","InternalAlignment","InternalAlignment","InternalAlignment","InternalAlignment"]') fail(`unexpected native InternalAlignment result: ${JSON.stringify(internalResult)}`) const resavedInternal = inspectFcstdArchive(new Uint8Array(await readFile(internalResavedPath))).objects.find((object) => object.name === 'InternalSketch')?.sketch ?? fail('Web inspector did not reconstruct the resaved native InternalAlignment Sketch.') if (JSON.stringify(resavedInternal.geometry.map((geometry) => geometry.id)) !== '["ellipse","spline"]' || JSON.stringify(resavedInternal.constraints) !== JSON.stringify(internalSketch.constraints.map((constraint) => ({ ...constraint, driving: true })))) fail(`Web inspector did not preserve native ellipse/B-spline InternalAlignment: ${JSON.stringify(resavedInternal)}`) const summary = { status: 'freecad-fcstd-native-oracle-pass', freecadVersion: result.freecadVersion, shape: { solidCount: result.solidCount, faceCount: result.faceCount, volume: result.volume }, primitives: Object.fromEntries(Object.entries(primitiveResults).map(([objectName, primitiveResult]) => [objectName, { properties: primitiveResult.properties, solidCount: primitiveResult.solidCount, faceCount: primitiveResult.faceCount, volume: primitiveResult.volume }])), primitiveRoundTrip: { objectCount: primitiveResaved.objects.length, shapeResourceCount: primitiveResaved.shapeResources.length, stringHasher: primitiveResaved.stringHasherResource?.status ?? 'absent' }, sketch: { geometryCount: sketchResult.geometryCount, constraintCount: sketchResult.constraintCount, constructionGeometry: sketchResult.constructionGeometry, constraintTypes: sketchResult.constraintTypes, externalGeometryCount: sketchResult.externalGeometryCount, externalGeoCount: sketchResult.externalGeoCount, externalLinks: sketchResult.externalLinks, mapMode: sketchResult.mapMode, attachmentSupport: sketchResult.attachmentSupport, attachmentOffset: sketchResult.attachmentOffset }, sketchRoundTrip: { geometryIds: resavedSketch.geometry.map((geometry) => geometry.id), constraintIds: resavedSketch.constraints.map((constraint) => constraint.id), externalIds: resavedSketch.externalGeometry.map((external) => external.id), externalSources: resavedSketch.externalGeometry.map((external) => external.source), support: proxySketch.properties.find((property) => property.name === 'Support')?.value, mapMode: proxySketch.properties.find((property) => property.name === 'MapMode')?.value }, faceProjection: { geometryCount: faceResult.geometryCount, constraintCount: faceResult.constraintCount, externalGeometryCount: faceResult.externalGeometryCount, externalGeoCount: faceResult.externalGeoCount, externalLinks: faceResult.externalLinks, roundTripIds: resavedFace.externalGeometry.map((external) => external.id) }, externalModes: { externalGeometryCount: modeResult.externalGeometryCount, externalGeoCount: modeResult.externalGeoCount, externalTypes: modeResult.externalTypes, externalStates: modeResult.externalStates, roundTripModes: resavedModes.externalGeometry.map((external) => external.mode ?? 'projection') }, snellsLaw: { geometryCount: snellResult.geometryCount, constraintCount: snellResult.constraintCount, constraintTypes: snellResult.constraintTypes, roundTrip: resavedSnell.constraints }, weight: { geometryCount: weightResult.geometryCount, constraintCount: weightResult.constraintCount, constructionGeometry: weightResult.constructionGeometry, constraintTypes: weightResult.constraintTypes, roundTrip: resavedWeight.constraints }, internalAlignment: { geometryCount: internalResult.geometryCount, constraintCount: internalResult.constraintCount, constructionGeometry: internalResult.constructionGeometry, constraintTypes: internalResult.constraintTypes, roundTrip: resavedInternal.constraints }, elementMap: { postfixCount: resavedMap.postfixCount, mapCount: resavedMap.mapCount }, stringHasherBytes: resavedHasher.byteLength, } const webStages = ['web-model', 'web-write', 'freecad-open', 'freecad-recompute', 'freecad-resave', 'web-inspect'] const sourceStages = ['freecad-source', 'web-inspect', 'web-write', 'freecad-open', 'freecad-resave', 'web-inspect-resaved'] const proxyStages = ['freecad-source', 'web-inspect', 'web-byte-preserving-write', 'byte-compare', 'freecad-open', 'freecad-resave', 'web-inspect-resaved'] const scenario = (id: string, direction: 'freecad-web-freecad' | 'web-freecad-web', domains: string[], evidence: Record, stages?: string[]) => ({ id, direction, stages: stages ?? (direction === 'freecad-web-freecad' ? sourceStages : webStages), domains, freecadVersion: '1.1.1', status: 'pass', differences: [], evidence, }) const roundTripScenarios = [ scenario('proxy-byte-preservation', 'freecad-web-freecad', ['objectTree', 'properties', 'gui', 'resources', 'proxy'], { sourceArchiveBytes: sourceBytes.byteLength, preservedArchiveBytes: proxyPreservedBytes.byteLength, sourceSha256, preservedSha256: proxyPreservedSha256, byteIdentical: true, proxyObjectCount: sourceInspection.compatibility.proxyObjects, blockedObjectCount: sourceInspection.compatibility.blockedObjects, unknownTypeIds: sourceInspection.compatibility.unknownTypeIds, resavedObjectCount: proxyResavedInspection.objects.length, resavedProxyObjectCount: proxyResavedInspection.compatibility.proxyObjects, resavedShapeValid: proxyResult.shapeValid, }, proxyStages), scenario('locked-partdesign-shape', 'freecad-web-freecad', ['shape', 'topology', 'resources'], { solidCount: result.solidCount, faceCount: result.faceCount, volume: result.volume, elementMapPostfixCount: resavedMap.postfixCount, elementMapCount: resavedMap.mapCount, stringHasherBytes: resavedHasher.byteLength, }), ...primitiveCases.map((oracleCase) => { const primitiveResult = primitiveResults[oracleCase.objectName] return scenario(`part-${oracleCase.objectName.toLowerCase()}`, 'web-freecad-web', ['objectTree', 'properties', 'shape', 'resources'], { typeId: `Part::${oracleCase.objectName}`, solidCount: primitiveResult.solidCount, faceCount: primitiveResult.faceCount, volume: primitiveResult.volume, boundingBox: primitiveResult.boundingBox, propertyCount: Object.keys(primitiveResult.properties).length, ...(oracleCase.objectName === 'Prism' || oracleCase.objectName === 'Wedge' || oracleCase.objectName === 'Ellipsoid' ? { properties: primitiveResult.properties } : {}), refine: primitiveResult.properties.Refine, resavedShapeAvailable: primitiveResaved.shapeResources.some((resource) => resource.path === `${oracleCase.objectName}.Shape.brp` && resource.status === 'available'), }) }), scenario('part-cut-through-hole', 'web-freecad-web', ['objectTree', 'properties', 'shape', 'resources'], { typeId: throughHoleObject.typeId, solidCount: throughHoleResult.solidCount, faceCount: throughHoleResult.faceCount, volume: throughHoleResult.volume, boundingBox: throughHoleResult.boundingBox, propertyCount: Object.keys(throughHoleResult.properties).length, properties: throughHoleResult.properties, toolPlacement: decodeFcstdPropertyValue(throughHolePlacement).value, refine: throughHoleResult.properties.Refine, resavedShapeAvailable: true, }), scenario('part-sphere-trim', 'web-freecad-web', ['properties', 'shape', 'resources'], { solidCount: sphereTrimResult.solidCount, faceCount: sphereTrimResult.faceCount, volume: sphereTrimResult.volume, boundingBox: sphereTrimResult.boundingBox, properties: sphereTrimResult.properties, resavedShapeAvailable: sphereTrimResaved.shapeResources.some((resource) => resource.path === 'SphereTrim.Shape.brp' && resource.status === 'available'), }), scenario('part-extrusion', 'web-freecad-web', ['objectTree', 'properties', 'sketch', 'shape', 'resources'], { typeId: partFeatureResaved.objects.find((object) => object.name === 'Extrusion')?.typeId, solidCount: extrusionResult.solidCount, faceCount: extrusionResult.faceCount, volume: extrusionResult.volume, properties: extrusionResult.properties, resavedShapeAvailable: partFeatureResaved.shapeResources.some((resource) => resource.path === 'Extrusion.Shape.brp' && resource.status === 'available'), }), scenario('part-revolution', 'web-freecad-web', ['objectTree', 'properties', 'sketch', 'shape', 'resources'], { typeId: partFeatureResaved.objects.find((object) => object.name === 'Revolution')?.typeId, solidCount: revolutionResult.solidCount, faceCount: revolutionResult.faceCount, volume: revolutionResult.volume, properties: revolutionResult.properties, resavedShapeAvailable: partFeatureResaved.shapeResources.some((resource) => resource.path === 'Revolution.Shape.brp' && resource.status === 'available'), }), scenario('sketch-core-external-attachment', 'web-freecad-web', ['objectTree', 'properties', 'sketch', 'topology'], { geometryCount: sketchResult.geometryCount, constraintCount: sketchResult.constraintCount, externalTypes: sketchResult.externalTypes, mapMode: sketchResult.mapMode, roundTripGeometryIds: resavedSketch.geometry.length, roundTripConstraintIds: resavedSketch.constraints.length, }), scenario('sketch-face-projection', 'web-freecad-web', ['sketch', 'topology'], { externalTypes: faceResult.externalTypes, externalGeoCount: faceResult.externalGeoCount, roundTripIds: resavedFace.externalGeometry.length, }), scenario('sketch-external-modes', 'web-freecad-web', ['sketch', 'topology', 'properties'], { externalTypes: modeResult.externalTypes, externalGeoCount: modeResult.externalGeoCount, nativeFlags: modeResult.externalStates?.map((state) => state.flags), roundTripModes: resavedModes.externalGeometry.map((external) => external.mode ?? 'projection'), }), scenario('sketch-snells-law', 'web-freecad-web', ['sketch', 'properties'], { geometryCount: snellResult.geometryCount, constraintTypes: snellResult.constraintTypes, roundTripConstraints: resavedSnell.constraints.length, }), scenario('sketch-bspline-weight', 'web-freecad-web', ['sketch', 'properties'], { geometryCount: weightResult.geometryCount, constraintTypes: weightResult.constraintTypes, roundTripConstraints: resavedWeight.constraints.length, }), scenario('sketch-internal-alignment', 'web-freecad-web', ['sketch', 'properties'], { geometryCount: internalResult.geometryCount, constraintTypes: internalResult.constraintTypes, roundTripConstraints: resavedInternal.constraints.length, }), ] const roundTripReport = { schemaVersion: 1, baselineId: 'freecad-1.1.1', freecadCommit: '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d', bitbybitVersion: '1.1.1', unknownDifferencesFail: true, directions: ['freecad-web-freecad', 'web-freecad-web'], scenarioCount: roundTripScenarios.length, scenarios: roundTripScenarios, status: 'verified', } const reportPath = resolve(root, 'config/freecad-fcstd-roundtrip-verification.json') await writeFile(reportPath, `${JSON.stringify(roundTripReport, null, 2)}\n`) console.log(JSON.stringify({ ...summary, roundTripReport: { path: 'config/freecad-fcstd-roundtrip-verification.json', scenarioCount: roundTripScenarios.length, directions: roundTripReport.directions } }, null, 2)) } finally { if (process.env.FREECAD_FCSTD_KEEP_TEMP === '1') console.error(`FREECAD_FCSTD_TEMP=${temporaryDirectory}`) else await rm(temporaryDirectory, { recursive: true, force: true }) }