Files
Web_FreeCAD_Bitbybit/scripts/check-freecad-fcstd-roundtrip-verification.mjs

78 lines
10 KiB
JavaScript

import { readFile } from 'node:fs/promises'
import { resolve } from 'node:path'
const root = resolve(new URL('..', import.meta.url).pathname)
const report = JSON.parse(await readFile(resolve(root, 'config/freecad-fcstd-roundtrip-verification.json'), 'utf8'))
const fail = (message) => { throw new Error(`FCStd round-trip verification: ${message}`) }
if (report.schemaVersion !== 1 || report.baselineId !== 'freecad-1.1.1' || report.freecadCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.bitbybitVersion !== '1.1.1') fail('baseline is not locked to FreeCAD/Bitbybit 1.1.1.')
if (report.unknownDifferencesFail !== true || report.status !== 'verified') fail('unknown differences are not release-blocking.')
if (JSON.stringify(report.directions) !== '["freecad-web-freecad","web-freecad-web"]') fail('both round-trip directions are required.')
if (!Array.isArray(report.scenarios) || report.scenarioCount !== 23 || report.scenarios.length !== report.scenarioCount) fail('exactly 23 native scenarios are required.')
const expectedIds = new Set([
'proxy-byte-preservation',
'locked-partdesign-shape',
'part-box', 'part-cylinder', 'part-sphere', 'part-ellipsoid', 'part-cone', 'part-torus', 'part-prism', 'part-wedge', 'part-fuse', 'part-cut', 'part-cut-through-hole', 'part-common',
'part-sphere-trim',
'part-extrusion', 'part-revolution',
'sketch-core-external-attachment', 'sketch-face-projection', 'sketch-external-modes',
'sketch-snells-law', 'sketch-bspline-weight', 'sketch-internal-alignment',
])
const seen = new Set()
for (const scenario of report.scenarios) {
if (!expectedIds.has(scenario.id) || seen.has(scenario.id)) fail(`unexpected or duplicate scenario '${scenario.id}'.`)
seen.add(scenario.id)
const expectedStages = scenario.id === 'proxy-byte-preservation'
? ['freecad-source', 'web-inspect', 'web-byte-preserving-write', 'byte-compare', 'freecad-open', 'freecad-resave', 'web-inspect-resaved']
: scenario.direction === 'freecad-web-freecad'
? ['freecad-source', 'web-inspect', 'web-write', 'freecad-open', 'freecad-resave', 'web-inspect-resaved']
: scenario.direction === 'web-freecad-web'
? ['web-model', 'web-write', 'freecad-open', 'freecad-recompute', 'freecad-resave', 'web-inspect']
: undefined
if (!expectedStages || JSON.stringify(scenario.stages) !== JSON.stringify(expectedStages)) fail(`${scenario.id} has an incomplete execution path.`)
if (scenario.freecadVersion !== '1.1.1' || scenario.status !== 'pass' || !Array.isArray(scenario.differences) || scenario.differences.length !== 0) fail(`${scenario.id} has an unclassified difference.`)
if (!Array.isArray(scenario.domains) || scenario.domains.length === 0 || !scenario.evidence || typeof scenario.evidence !== 'object') fail(`${scenario.id} has no semantic evidence.`)
}
if (seen.size !== expectedIds.size) fail('one or more required scenarios are missing.')
const byId = Object.fromEntries(report.scenarios.map((scenario) => [scenario.id, scenario]))
const proxy = byId['proxy-byte-preservation'].evidence
if (proxy.sourceArchiveBytes <= 0 || proxy.sourceArchiveBytes !== proxy.preservedArchiveBytes || proxy.sourceSha256 !== proxy.preservedSha256 || !/^[a-f0-9]{64}$/.test(proxy.sourceSha256) || proxy.byteIdentical !== true || proxy.proxyObjectCount !== 8 || proxy.blockedObjectCount !== 0 || proxy.resavedObjectCount !== 17 || proxy.resavedProxyObjectCount !== 8 || proxy.resavedShapeValid !== true || JSON.stringify(proxy.unknownTypeIds) !== '["App::Line","App::Origin","App::Plane","App::Point"]') fail('FreeCAD-origin proxy byte-preservation evidence is incomplete.')
const sourceShape = byId['locked-partdesign-shape'].evidence
if (sourceShape.solidCount !== 1 || sourceShape.faceCount !== 23 || sourceShape.elementMapPostfixCount !== 63 || sourceShape.elementMapCount !== 2 || sourceShape.stringHasherBytes <= 0) fail('FreeCAD-origin Shape/ElementMap evidence is incomplete.')
for (const id of ['part-box', 'part-cylinder', 'part-sphere', 'part-ellipsoid', 'part-cone', 'part-torus', 'part-prism', 'part-wedge', 'part-fuse', 'part-cut', 'part-cut-through-hole', 'part-common']) {
const evidence = byId[id].evidence
if (evidence.solidCount !== 1 || !Number.isFinite(evidence.volume) || evidence.volume <= 0 || evidence.propertyCount <= 0 || evidence.resavedShapeAvailable !== true) fail(`${id} did not preserve parameters and a valid Shape resource.`)
}
if (byId['part-fuse'].evidence.refine !== false || byId['part-cut'].evidence.refine !== false || byId['part-cut-through-hole'].evidence.refine !== false || byId['part-common'].evidence.refine !== true) fail('Part Boolean Refine values did not round-trip through FreeCAD.')
const cutThroughHole = byId['part-cut-through-hole'].evidence
if (cutThroughHole.typeId !== 'Part::Cut' || cutThroughHole.faceCount !== 7 || !Number.isFinite(cutThroughHole.volume) || Math.abs(cutThroughHole.volume - (4000 - 90 * Math.PI)) > 1e-8 || !cutThroughHole.boundingBox || JSON.stringify(cutThroughHole.boundingBox.min) !== '[0,0,0]' || JSON.stringify(cutThroughHole.boundingBox.max) !== '[20,20,10]' || cutThroughHole.properties?.Base !== 'Base' || cutThroughHole.properties?.Tool !== 'HoleTool' || cutThroughHole.properties?.Refine !== false || JSON.stringify(cutThroughHole.toolPlacement) !== '{"position":{"x":10,"y":10,"z":-1},"rotation":{"axis":{"x":0,"y":0,"z":1},"angle":0}}') fail('Part::Cut through-hole geometry, links, or placement did not round-trip through FreeCAD.')
const torus = byId['part-torus'].evidence
const torusBound = 12.988706403508727
if (!Number.isFinite(torus.volume) || Math.abs(torus.volume - 60 * Math.PI ** 2) > 1e-8 || torus.propertyCount !== 5 || !torus.boundingBox || torus.boundingBox.min.some((value, index) => Math.abs(value - [-torusBound, -torusBound, -2][index]) > 1e-8) || torus.boundingBox.max.some((value, index) => Math.abs(value - [torusBound, torusBound, 2][index]) > 1e-8)) fail('native Part::Torus evidence is incomplete.')
const prism = byId['part-prism'].evidence
if (prism.typeId !== 'Part::Prism' || prism.solidCount !== 1 || prism.faceCount !== 8 || !Number.isFinite(prism.volume) || Math.abs(prism.volume - 60 * Math.sqrt(3)) > 1e-8 || prism.propertyCount !== 5 || JSON.stringify(prism.properties) !== '{"Circumradius":2,"FirstAngle":10,"Height":10,"Polygon":6,"SecondAngle":-5}' || !prism.boundingBox || prism.boundingBox.min.some((value, index) => Math.abs(value - [-2, -2.6069374428281185, 0][index]) > 1e-8) || prism.boundingBox.max.some((value, index) => Math.abs(value - [3.76326980708465, 1.7320508075688776, 10][index]) > 1e-8)) fail('native Part::Prism evidence is incomplete.')
const wedge = byId['part-wedge'].evidence
if (wedge.typeId !== 'Part::Wedge' || wedge.solidCount !== 1 || wedge.faceCount !== 6 || !Number.isFinite(wedge.volume) || Math.abs(wedge.volume - 2440 / 3) > 1e-8 || wedge.propertyCount !== 10 || JSON.stringify(wedge.properties) !== '{"X2max":8,"X2min":0,"Xmax":10,"Xmin":0,"Ymax":10,"Ymin":0,"Z2max":8,"Z2min":0,"Zmax":10,"Zmin":0}' || wedge.resavedShapeAvailable !== true) fail('native Part::Wedge evidence is incomplete.')
const ellipsoid = byId['part-ellipsoid'].evidence
if (ellipsoid.typeId !== 'Part::Ellipsoid' || ellipsoid.solidCount !== 1 || ellipsoid.faceCount !== 1 || !Number.isFinite(ellipsoid.volume) || Math.abs(ellipsoid.volume - 133.9826640573845) > 1e-8 || ellipsoid.propertyCount !== 6 || JSON.stringify(ellipsoid.properties) !== '{"Angle1":-90,"Angle2":90,"Angle3":360,"Radius1":2,"Radius2":4,"Radius3":0}' || ellipsoid.resavedShapeAvailable !== true || !ellipsoid.boundingBox || ellipsoid.boundingBox.min.some((value, index) => Math.abs(value - [-8, -6.92820323027551, -2][index]) > 1e-8) || ellipsoid.boundingBox.max.some((value, index) => Math.abs(value - [4, 6.928203230275507, 2][index]) > 1e-8)) fail('native Part::Ellipsoid evidence is incomplete.')
const sphereTrim = byId['part-sphere-trim'].evidence
if (sphereTrim.solidCount !== 1 || !Number.isFinite(sphereTrim.volume) || sphereTrim.volume <= 0 || JSON.stringify(sphereTrim.properties) !== '{"Angle1":-45,"Angle2":45,"Angle3":120,"Radius":5}' || sphereTrim.resavedShapeAvailable !== true || !sphereTrim.boundingBox || sphereTrim.boundingBox.min.length !== 3 || sphereTrim.boundingBox.max.length !== 3) fail('native Part::Sphere trim evidence is incomplete.')
const extrusion = byId['part-extrusion'].evidence
if (extrusion.typeId !== 'Part::Extrusion' || extrusion.solidCount !== 1 || !Number.isFinite(extrusion.volume) || Math.abs(extrusion.volume - 30) > 1e-8 || JSON.stringify(extrusion.properties) !== '{"Base":"FeatureProfile","DirMode":"Custom","LengthFwd":5,"LengthRev":0,"Reversed":false,"Solid":true,"Symmetric":false,"TaperAngle":0,"TaperAngleRev":0}' || extrusion.resavedShapeAvailable !== true) fail('native Part::Extrusion evidence is incomplete.')
const revolution = byId['part-revolution'].evidence
if (revolution.typeId !== 'Part::Revolution' || revolution.solidCount !== 1 || !Number.isFinite(revolution.volume) || Math.abs(revolution.volume - 36 * Math.PI) > 1e-8 || JSON.stringify(revolution.properties) !== '{"Angle":360,"Solid":true,"Source":"FeatureProfile","Symmetric":false}' || revolution.resavedShapeAvailable !== true) fail('native Part::Revolution evidence is incomplete.')
const coreSketch = byId['sketch-core-external-attachment'].evidence
if (coreSketch.geometryCount !== 10 || coreSketch.constraintCount !== 11 || JSON.stringify(coreSketch.externalTypes) !== '[0]' || coreSketch.mapMode !== 'FlatFace' || coreSketch.roundTripGeometryIds !== 10 || coreSketch.roundTripConstraintIds !== 11) fail('core Sketch round-trip evidence is incomplete.')
const modeSketch = byId['sketch-external-modes'].evidence
if (JSON.stringify(modeSketch.externalTypes) !== '[1,2,0]' || modeSketch.externalGeoCount !== 8 || JSON.stringify(modeSketch.nativeFlags) !== '[1,1,1,1,1,2]' || JSON.stringify(modeSketch.roundTripModes) !== '["intersection","both","both","both","both","projection"]') fail('external geometry mode/flag evidence is incomplete.')
if (byId['sketch-face-projection'].evidence.roundTripIds !== 4 || byId['sketch-snells-law'].evidence.roundTripConstraints !== 1 || byId['sketch-bspline-weight'].evidence.roundTripConstraints !== 1 || byId['sketch-internal-alignment'].evidence.roundTripConstraints !== 5) fail('advanced Sketcher round-trip evidence is incomplete.')
console.log(JSON.stringify({
status: 'freecad-fcstd-roundtrip-verification-pass',
scenarioCount: report.scenarioCount,
directions: report.directions,
unknownDifferences: 0,
}, null, 2))