feat: advance FreeCAD exact parity evidence
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

This commit is contained in:
2026-08-14 22:39:16 -04:00
parent e3373c9d6c
commit 5bbd7b9d4f
64 changed files with 113069 additions and 21074 deletions

View File

@@ -30,7 +30,7 @@ import { resolveMeshSubshape, resolveScreenBoxSelection } from '../src/facade/th
import { createFacadeGeometryRecomputeExecutor, executeFacadeRecomputeNode, RecomputeCoordinator, type RecomputeGeometryRuntime } from '../src/facade/recomputeEngine'
import { decodeFcstdPropertyValue, extractFcstdShapeResources, inspectFcstdArchive, instantiateFcstdShapeResource, rewriteFcstdMetadataArchive, serializeFcstdMetadataArchive, serializeStringHasherTableResource, storeFcstdShapeResources } from '../src/facade/fcstd'
import { ATTACHMENT_MAP_MODES, composeAttachmentPlacement, identityAttachmentOffset, validateAttachmentMapMode, validateAttachmentOffset, validateAttachmentSupport } from '../src/facade/attachment'
import { decodeFreecadPropertyStatus, encodeFreecadPropertyStatus, freecadPropertyRecomputeEffect, isFreecadPropertyDocumentModifiedSuppressed, isFreecadPropertyHidden, isFreecadPropertyPersistenceSuppressed, isFreecadPropertyReadOnly, isFreecadPropertyRecomputeSuppressed, normalizeFreecadPropertyStatusMask } from '../src/facade/propertyStatus'
import { decodeFreecadPropertyStatus, encodeFreecadPropertyStatus, freecadPropertyRecomputeEffect, isFreecadPropertyDocumentModifiedSuppressed, isFreecadPropertyHidden, isFreecadPropertyPartialDocumentTrigger, isFreecadPropertyPersistenceSuppressed, isFreecadPropertyReadOnly, isFreecadPropertyRecomputeSuppressed, normalizeFreecadPropertyStatusMask } from '../src/facade/propertyStatus'
import { redirectBodyTips, resolveBodyTip } from '../src/facade/bodyRules'
import { assertShapeHandleIntegrity, BitbybitGeometryRuntime, classifyPlanarProfile, collectGeometryImportText, MAX_GEOMETRY_IMPORT_TEXT_BYTES, normalizeBitbybitMesh, validateBooleanUnionInput, validateBoxInput, validateChamferInput, validateConeInput, validateCylinderInput, validateFilletInput, validateGeometryFileImport, validateGrooveInput, validateLoftInput, validateMirrorInput, validatePadInput, validatePipeInput, validatePlacementInput, validatePlanarProfile, validatePrismInput, validateRevolutionInput, validateSphereInput, validateTorusInput } from '../src/facade/geometryRuntime'
import type { DocumentObjectSnapshot, DocumentSnapshot, MultiTransformValue, NativeTopologyHistoryInput, ObjectTopologySnapshot, PlanarProfile, Point3, ShapeHandle, SubshapeTopology, TopoRefValue } from '../src/facade/types'
@@ -4509,6 +4509,7 @@ test('FreeCAD Property status masks preserve all source bits and enforce native
assert.equal(isFreecadPropertyRecomputeSuppressed(mask), true)
assert.equal(isFreecadPropertyPersistenceSuppressed(mask), true)
assert.equal(isFreecadPropertyDocumentModifiedSuppressed(mask), true)
assert.equal(isFreecadPropertyPartialDocumentTrigger(encodeFreecadPropertyStatus(['PartialTrigger'])), true)
assert.equal(freecadPropertyRecomputeEffect(encodeFreecadPropertyStatus(['NoRecompute'])), 'owner')
assert.equal(freecadPropertyRecomputeEffect(encodeFreecadPropertyStatus(['PropNoRecompute'])), 'dependents')
assert.equal(freecadPropertyRecomputeEffect(encodeFreecadPropertyStatus(['Output'])), 'none')
@@ -4539,6 +4540,79 @@ test('FreeCAD Property status masks preserve all source bits and enforce native
facade.geometry.dispose()
})
test('dynamic Property mutations are transactional, observable, and enforce LockDynamic', () => {
const document = recomputeDocumentFixture([{ sourceId: 'Follower', targetId: 'Box', relation: 'link', propertyName: 'Base' }])
document.tree = [{ id: 'Box', label: 'Box', type: 'feature' }, { id: 'Follower', label: 'Follower', type: 'feature' }]
document.objects = [
{ id: 'Box', typeId: 'App::FeaturePython', properties: [] },
{ id: 'Follower', typeId: 'Part::Feature', properties: [{ name: 'Base', label: 'Base', group: 'Base', scope: 'data', type: 'App::PropertyLink', value: 'Box', recompute: true }] },
]
document.recompute = { generation: 0, status: 'idle', objectStates: { Box: 'up-to-date', Follower: 'up-to-date' }, dirtyObjects: [], order: [], errors: [] }
const facade = createWebCadFacade({ runtimeMode: 'mock', initialDocument: document })
const observed: string[] = []
facade.subscribe((event) => {
if (event.type.startsWith('property.') || event.type === 'transaction.committed') observed.push(event.type)
})
const locked = facade.app.document.addDynamicProperty({ objectId: 'Box', property: {
name: 'LockedValue', label: 'Locked value', group: 'Oracle', scope: 'data', type: 'App::PropertyInteger', value: 1, recompute: true,
nativeStatus: encodeFreecadPropertyStatus(['LockDynamic']),
} })
assert.deepEqual(decodeFreecadPropertyStatus(locked.nativeStatus as number).names, ['LockDynamic', 'PropDynamic'])
facade.app.document.setProperty({ objectId: 'Box', propertyName: 'LockedValue', value: 2 })
assert.deepEqual(facade.app.document.getActive().recompute?.dirtyObjects, ['Box', 'Follower'])
facade.history.undo()
assert.equal(facade.app.document.getObject('Box')?.properties.find((property) => property.name === 'LockedValue')?.value, 1)
facade.history.redo()
assert.equal(facade.app.document.removeDynamicProperty({ objectId: 'Box', propertyName: 'LockedValue' }), false)
assert.throws(() => facade.app.document.renameDynamicProperty({ objectId: 'Box', propertyName: 'LockedValue', newPropertyName: 'RenamedLockedValue' }), /Property LockedValue is locked/)
facade.app.document.addDynamicProperty({ objectId: 'Box', property: { name: 'MutableValue', label: 'Mutable value', group: 'Oracle', scope: 'data', type: 'App::PropertyFloatConstraint', value: 3 } })
assert.equal(facade.app.document.renameDynamicProperty({ objectId: 'Box', propertyName: 'MutableValue', newPropertyName: 'RenamedValue' }), true)
assert.equal(facade.app.document.removeDynamicProperty({ objectId: 'Box', propertyName: 'RenamedValue' }), true)
assert.equal(facade.app.document.getObject('Box')?.properties.some((property) => property.name === 'RenamedValue'), false)
assert.deepEqual(observed, [
'property.added', 'transaction.committed',
'property.before-change', 'property.changed', 'transaction.committed',
'property.added', 'transaction.committed',
'property.renamed', 'transaction.committed',
'property.removed', 'transaction.committed',
])
facade.geometry.dispose()
})
test('PartialTrigger suppresses partial-document mutation warnings while recompute waits for a full load', async () => {
const document = recomputeDocumentFixture([{ sourceId: 'Follower', targetId: 'Binder', relation: 'link', propertyName: 'Base' }])
document.partial = true
document.dirty = false
document.tree = [{ id: 'Binder', label: 'Binder', type: 'feature' }, { id: 'Follower', label: 'Follower', type: 'feature' }]
document.objects = [
{ id: 'Binder', typeId: 'PartDesign::SubShapeBinder', properties: [
{ name: 'PartialLoad', label: 'Partial load', group: 'Base', scope: 'data', type: 'App::PropertyBool', value: false, recompute: true, nativeStatus: encodeFreecadPropertyStatus(['PartialTrigger']) },
{ name: 'Ordinary', label: 'Ordinary', group: 'Base', scope: 'data', type: 'App::PropertyInteger', value: 1, recompute: true },
] },
{ id: 'Follower', typeId: 'Part::Feature', properties: [{ name: 'Base', label: 'Base', group: 'Base', scope: 'data', type: 'App::PropertyLink', value: 'Binder', recompute: true }] },
]
document.recompute = { generation: 4, status: 'idle', objectStates: { Binder: 'up-to-date', Follower: 'up-to-date' }, dirtyObjects: [], order: [], errors: [] }
const facade = createWebCadFacade({ runtimeMode: 'mock', initialDocument: document })
const changes: Array<{ propertyName: string; partialTrigger: boolean }> = []
facade.subscribe((event) => { if (event.type === 'property.changed') changes.push({ propertyName: event.propertyName, partialTrigger: event.partialTrigger }) })
facade.app.document.setProperty({ objectId: 'Binder', propertyName: 'PartialLoad', value: true })
assert.equal(facade.diagnostics.list().some((diagnostic) => diagnostic.code === 'PARTIAL_DOCUMENT_MUTATION_NOT_PERSISTED'), false)
facade.app.document.setProperty({ objectId: 'Binder', propertyName: 'Ordinary', value: 2 })
assert.equal(facade.diagnostics.list().filter((diagnostic) => diagnostic.code === 'PARTIAL_DOCUMENT_MUTATION_NOT_PERSISTED').length, 1)
assert.deepEqual(changes, [{ propertyName: 'PartialLoad', partialTrigger: true }, { propertyName: 'Ordinary', partialTrigger: false }])
const sync = facade.app.document.recompute()
assert.deepEqual(sync.affected, [])
assert.equal(sync.generation, 4)
const asyncResult = await facade.app.document.recomputeAsync()
assert.equal(asyncResult.status, 'completed')
assert.deepEqual(asyncResult.affected, [])
assert.deepEqual(asyncResult.dirtyObjects, ['Binder', 'Follower'])
facade.geometry.dispose()
})
test('FCStd Property status preserves transient definitions while NoPersist stays out of the archive', () => {
const nativeStatus = encodeFreecadPropertyStatus(['Ordered', 'PropNoRecompute', 'PropReadOnly', 'PropHidden', 'PropOutput'])
const document = recomputeDocumentFixture()
@@ -4585,6 +4659,33 @@ test('FCStd typed property codec decodes numeric, boolean and LinkSub values wit
assert.deepEqual(decodeFcstdPropertyValue({ name: 'OptionalBase', typeId: 'App::PropertyLink', element: 'Link', value: '' }), { value: null, decoded: true })
})
test('FCStd high-frequency App Property codecs preserve constraint numerics and hidden links natively', () => {
const dynamicStatus = encodeFreecadPropertyStatus(['PropDynamic'])
const document = recomputeDocumentFixture()
document.tree = [{ id: 'Holder', label: 'Holder', type: 'feature' }, { id: 'Target', label: 'Target', type: 'feature' }]
document.objects = [
{ id: 'Holder', typeId: 'App::FeaturePython', properties: [
{ name: 'FloatConstraint', label: 'Float constraint', group: 'Oracle', scope: 'data', type: 'App::PropertyFloatConstraint', value: 0.25, nativeStatus: dynamicStatus },
{ name: 'QuantityConstraint', label: 'Quantity constraint', group: 'Oracle', scope: 'data', type: 'App::PropertyQuantityConstraint', value: 12.5, nativeStatus: dynamicStatus },
{ name: 'Precision', label: 'Precision', group: 'Oracle', scope: 'data', type: 'App::PropertyPrecision', value: 0.001, nativeStatus: dynamicStatus },
{ name: 'HiddenLink', label: 'Hidden link', group: 'Oracle', scope: 'data', type: 'App::PropertyLinkHidden', value: 'Target', nativeStatus: dynamicStatus },
] },
{ id: 'Target', typeId: 'App::FeaturePython', properties: [] },
]
document.dependencies = []
const archive = serializeFcstdMetadataArchive(document)
const xml = new TextDecoder().decode(unzipSync(archive)['Document.xml'])
assert.match(xml, /type="App::PropertyFloatConstraint"[^>]*><Float value="0\.25"\/><\/Property>/)
assert.match(xml, /type="App::PropertyQuantityConstraint"[^>]*><Float value="12\.5"\/><\/Property>/)
assert.match(xml, /type="App::PropertyPrecision"[^>]*><Float value="0\.001"\/><\/Property>/)
assert.match(xml, /type="App::PropertyLinkHidden"[^>]*><Link value="Target"\/><\/Property>/)
const properties = inspectFcstdArchive(archive).objects.find((object) => object.name === 'Holder')?.properties ?? []
assert.deepEqual(decodeFcstdPropertyValue(properties.find((property) => property.name === 'FloatConstraint')!), { value: 0.25, decoded: true })
assert.deepEqual(decodeFcstdPropertyValue(properties.find((property) => property.name === 'QuantityConstraint')!), { value: 12.5, decoded: true })
assert.deepEqual(decodeFcstdPropertyValue(properties.find((property) => property.name === 'Precision')!), { value: 0.001, decoded: true })
assert.deepEqual(decodeFcstdPropertyValue(properties.find((property) => property.name === 'HiddenLink')!), { value: 'Target', decoded: true })
})
test('FCStd native Part primitive properties omit dynamic metadata and use Float payloads', () => {
const document = recomputeDocumentFixture([
{ sourceId: 'Extrusion', targetId: 'Profile', relation: 'link', propertyName: 'Base' },