feat: advance FreeCAD exact parity evidence
This commit is contained in:
@@ -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' },
|
||||
|
||||
Reference in New Issue
Block a user