feat: align FreeCAD property status semantics
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { test } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { strToU8, unzipSync, zipSync } from 'fflate'
|
||||
import { createMockFacade } from '../src/facade/mockFacade'
|
||||
import { createMockFacade, createWebCadFacade } from '../src/facade/mockFacade'
|
||||
import { createProductionFacade } from '../src/facade/productionFacade'
|
||||
import { PROJECT_SCHEMA_MIGRATIONS, PROJECT_SCHEMA_SQL, PROJECT_SCHEMA_VERSION, runProjectSchemaMigrations } from '../src/facade/projectSchema'
|
||||
import { createSqliteProjectPersistence, PersistenceWriteQueue, ProjectAutosaveScheduler } from '../src/facade/projectStore'
|
||||
@@ -30,6 +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 { 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'
|
||||
@@ -4496,6 +4497,83 @@ test('FCStd metadata writer round-trips Document and GuiDocument while preservin
|
||||
assert.throws(() => serializeFcstdMetadataArchive(document, { opaqueEntries: { 'Document.xml': new Uint8Array() } }), /reserved path/)
|
||||
})
|
||||
|
||||
test('FreeCAD Property status masks preserve all source bits and enforce native touch behavior', () => {
|
||||
const mask = encodeFreecadPropertyStatus(['Immutable', 'Hidden', 'Output', 'NoModify', 'NoRecompute', 'Ordered', 'PropNoPersist', 'PropNoRecompute', 'PropReadOnly', 'PropHidden', 'User4'])
|
||||
assert.deepEqual(decodeFreecadPropertyStatus(mask), {
|
||||
mask,
|
||||
names: ['Immutable', 'Hidden', 'Output', 'NoModify', 'NoRecompute', 'Ordered', 'PropNoPersist', 'PropNoRecompute', 'PropReadOnly', 'PropHidden', 'User4'],
|
||||
unknownBits: [],
|
||||
})
|
||||
assert.equal(isFreecadPropertyReadOnly(mask), true)
|
||||
assert.equal(isFreecadPropertyHidden(mask), true)
|
||||
assert.equal(isFreecadPropertyRecomputeSuppressed(mask), true)
|
||||
assert.equal(isFreecadPropertyPersistenceSuppressed(mask), true)
|
||||
assert.equal(isFreecadPropertyDocumentModifiedSuppressed(mask), true)
|
||||
assert.equal(freecadPropertyRecomputeEffect(encodeFreecadPropertyStatus(['NoRecompute'])), 'owner')
|
||||
assert.equal(freecadPropertyRecomputeEffect(encodeFreecadPropertyStatus(['PropNoRecompute'])), 'dependents')
|
||||
assert.equal(freecadPropertyRecomputeEffect(encodeFreecadPropertyStatus(['Output'])), 'none')
|
||||
assert.throws(() => normalizeFreecadPropertyStatusMask(-1), /unsigned 32-bit/)
|
||||
assert.throws(() => normalizeFreecadPropertyStatusMask(0x1_0000_0000), /unsigned 32-bit/)
|
||||
|
||||
const document = recomputeDocumentFixture()
|
||||
document.dirty = false
|
||||
document.tree = [{ id: 'Box', label: 'Box', type: 'feature', state: 'up-to-date' }]
|
||||
document.objects = [{ id: 'Box', typeId: 'Part::Box', properties: [
|
||||
{ name: 'Length', label: 'Length', group: 'Box', scope: 'data', type: 'App::PropertyLength', value: 10, recompute: true, nativeStatus: encodeFreecadPropertyStatus(['NoModify', 'Output']) },
|
||||
{ name: 'Width', label: 'Width', group: 'Box', scope: 'data', type: 'App::PropertyLength', value: 10, recompute: true, nativeStatus: encodeFreecadPropertyStatus(['PropReadOnly']) },
|
||||
{ name: 'Height', label: 'Height', group: 'Box', scope: 'data', type: 'App::PropertyLength', value: 10, recompute: true, nativeStatus: encodeFreecadPropertyStatus(['PropNoRecompute']) },
|
||||
] }, { id: 'Follower', typeId: 'Part::Feature', properties: [
|
||||
{ name: 'Base', label: 'Base', group: 'Base', scope: 'data', type: 'App::PropertyLink', value: 'Box', recompute: true },
|
||||
] }]
|
||||
document.tree.push({ id: 'Follower', label: 'Follower', type: 'feature', state: 'up-to-date' })
|
||||
document.dependencies = [{ sourceId: 'Follower', targetId: 'Box', relation: 'link', propertyName: 'Base' }]
|
||||
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 })
|
||||
facade.app.document.setProperty({ objectId: 'Box', propertyName: 'Length', value: 12 })
|
||||
assert.equal(facade.app.document.getObject('Box')?.properties[0].value, 12)
|
||||
assert.equal(facade.app.document.getActive().dirty, false)
|
||||
assert.deepEqual(facade.app.document.getActive().recompute?.dirtyObjects, [])
|
||||
assert.throws(() => facade.app.document.setProperty({ objectId: 'Box', propertyName: 'Width', value: 12 }), /read-only/)
|
||||
facade.app.document.setProperty({ objectId: 'Box', propertyName: 'Height', value: 12 })
|
||||
assert.deepEqual(facade.app.document.getActive().recompute?.dirtyObjects, ['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()
|
||||
document.tree = [{ id: 'Box', label: 'Box', type: 'feature' }]
|
||||
document.objects = [{ id: 'Box', typeId: 'Part::Box', properties: [
|
||||
{ name: 'Length', label: 'Length', group: 'Box', scope: 'data', type: 'App::PropertyLength', value: 12.5, nativeStatus },
|
||||
{ name: 'Width', label: 'Width', group: 'Box', scope: 'data', type: 'App::PropertyLength', value: 3, nativeStatus: encodeFreecadPropertyStatus(['PropNoPersist']) },
|
||||
{ name: 'Height', label: 'Height', group: 'Box', scope: 'data', type: 'App::PropertyLength', value: 4, nativeStatus: encodeFreecadPropertyStatus(['Transient']) },
|
||||
{ name: 'WebScratch', label: 'Web scratch', group: 'Web', scope: 'data', type: 'App::PropertyInteger', value: 9, nativeStatus: encodeFreecadPropertyStatus(['PropDynamic', 'PropTransient']) },
|
||||
] }]
|
||||
document.dependencies = []
|
||||
const archive = serializeFcstdMetadataArchive(document)
|
||||
const documentXml = new TextDecoder().decode(unzipSync(archive)['Document.xml'])
|
||||
assert.match(documentXml, new RegExp(`<Property name="Length" type="App::PropertyLength" status="${nativeStatus}">`))
|
||||
assert.doesNotMatch(documentXml, /<Property name="Width"/)
|
||||
assert.match(documentXml, new RegExp(`<_Property name="Height" type="App::PropertyLength" status="${encodeFreecadPropertyStatus(['Transient'])}"\/>`))
|
||||
assert.match(documentXml, new RegExp(`<Property name="WebScratch" type="App::PropertyInteger"[^>]+status="${encodeFreecadPropertyStatus(['PropDynamic', 'PropTransient'])}"><\/Property>`))
|
||||
assert.doesNotMatch(documentXml, /<Integer value="9"/)
|
||||
const properties = inspectFcstdArchive(archive).objects[0].properties
|
||||
assert.deepEqual(properties.find((property) => property.name === 'Length'), {
|
||||
name: 'Length', typeId: 'App::PropertyLength', element: 'Float', value: '12.5', nativeStatus,
|
||||
statusNames: ['Ordered', 'PropNoRecompute', 'PropReadOnly', 'PropHidden', 'PropOutput'],
|
||||
})
|
||||
assert.equal(properties.some((property) => property.name === 'Width'), false)
|
||||
assert.deepEqual(properties.find((property) => property.name === 'Height'), {
|
||||
name: 'Height', typeId: 'App::PropertyLength', element: '', value: '', nativeStatus: encodeFreecadPropertyStatus(['Transient']), statusNames: ['Transient'], transientMetadata: true,
|
||||
})
|
||||
assert.deepEqual(properties.find((property) => property.name === 'WebScratch'), {
|
||||
name: 'WebScratch', typeId: 'App::PropertyInteger', element: '', value: '', nativeStatus: encodeFreecadPropertyStatus(['PropDynamic', 'PropTransient']), statusNames: ['PropDynamic', 'PropTransient'],
|
||||
})
|
||||
|
||||
const invalidStatus = '<Document><Objects Count="1"><Object name="Box" type="Part::Box"/></Objects><ObjectData Count="1"><Object name="Box"><Properties Count="1"><Property name="Length" type="App::PropertyLength" status="4294967296"><Float value="1"/></Property></Properties></Object></ObjectData></Document>'
|
||||
assert.throws(() => inspectFcstdArchive(zipSync({ 'Document.xml': strToU8(invalidStatus) })), /unsigned 32-bit/)
|
||||
})
|
||||
|
||||
test('FCStd typed property codec decodes numeric, boolean and LinkSub values without executing expressions', () => {
|
||||
assert.deepEqual(decodeFcstdPropertyValue({ name: 'Length', typeId: 'App::PropertyLength', element: 'Length', value: '12.5' }), { value: 12.5, decoded: true })
|
||||
assert.deepEqual(decodeFcstdPropertyValue({ name: 'Polygon', typeId: 'App::PropertyIntegerConstraint', element: 'Integer', value: '6' }), { value: 6, decoded: true })
|
||||
|
||||
Reference in New Issue
Block a user