feat: resolve topology reference candidates

This commit is contained in:
2026-08-03 01:28:16 -04:00
parent ffbe2c69dc
commit 56903dbeb2
12 changed files with 160 additions and 22 deletions

View File

@@ -7,7 +7,7 @@ import { createSqliteProjectPersistence, PersistenceWriteQueue, ProjectAutosaveS
import { DependencyGraph } from '../src/facade/dependencyGraph'
import { evaluateQuantityExpression, quantityFromNumber } from '../src/facade/units'
import { createEdgeSubshapeRefs, createSubshapeRefs, createVertexSubshapeRefs, matchSubshapes, signatureForEdge, signatureForFace, signatureForVertex } from '../src/facade/topologyNaming'
import { createPersistedTopoRef, migrateDocumentTopologyReferences, migrateTopoRefs, parseTopoRef, resolveTopoRef, serializeTopoRef } from '../src/facade/topologyReferences'
import { createPersistedTopoRef, migrateDocumentTopologyReferences, migrateTopoRefs, parseTopoRef, resolveDocumentTopologyReference, resolveTopoRef, serializeTopoRef } from '../src/facade/topologyReferences'
import { captureSignatureTopologyHistory } from '../src/facade/topologyHistory'
import { assessResourceQuota, planResourceSweep } from '../src/facade/resourcePolicy'
import { cloneSketch, createSketch, solveSketch } from '../src/facade/sketcher'
@@ -887,6 +887,13 @@ test('generation topology migration updates LinkSub and external geometry withou
assert.equal(external?.candidates?.length, 2)
assert.deepEqual(ambiguous.changedOwnerIds, ['owner'])
assert.deepEqual(ambiguous.issues.map((issue) => issue.referenceName), ['SupportFace', 'ExternalGeometry:external'])
const propertyCandidate = external?.candidates?.[0] as string
const resolvedProperty = resolveDocumentTopologyReference(document, { ownerObjectId: 'owner', referenceName: 'SupportFace', candidatePersistentId: propertyCandidate })
assert.equal(resolvedProperty.status, 'stable')
assert.equal(resolvedProperty.generation, 2)
const externalCandidate = external?.candidates?.[1] as string
assert.equal(resolveDocumentTopologyReference(document, { ownerObjectId: 'owner', referenceName: 'ExternalGeometry:external', candidatePersistentId: externalCandidate }).persistentId, externalCandidate)
assert.throws(() => resolveDocumentTopologyReference(document, { ownerObjectId: 'owner', referenceName: 'SupportFace', candidatePersistentId: 'missing' }), /not available/)
const deletedMigration = migrateTopoRefs('source', previousEntries, [], 3)
document.objects[0].topology = {
@@ -916,6 +923,44 @@ test('generation topology migration updates LinkSub and external geometry withou
assert.equal(resolvedAcrossGap.issues.length, 1)
})
test('Facade topology replacement is versioned, rebuilds dependencies and is undoable', async () => {
const face = { vertexCoord: [0, 0, 0, 2, 0, 0, 2, 2, 0, 0, 2, 0], normalCoord: [], triIndexes: [0, 1, 2, 0, 2, 3] }
const current = createSubshapeRefs('shape-current', 5, [face, face])
const entries = current.refs.map((ref, index) => ({ ref, signature: current.signatures[index] }))
const migration = migrateTopoRefs('source', [], entries, 5)
const candidateIds = migration.matches.map((match) => match.current.persistentId)
const reference = createPersistedTopoRef('source', { ...current.refs[0], status: 'ambiguous', candidates: candidateIds }, 4)
const topology: ObjectTopologySnapshot = {
shapeId: 'shape-current', documentVersion: 5, generation: 5, entries,
migration: { previousGeneration: 4, matches: migration.matches },
history: captureSignatureTopologyHistory('source:generation:5', [], entries),
}
const document: DocumentSnapshot = {
...recomputeDocumentFixture(), id: 'doc-topology-repair', version: 5,
tree: [{ id: 'source', label: 'Source', type: 'feature' }, { id: 'owner', label: 'Owner', type: 'feature' }],
objects: [
{ id: 'source', typeId: 'Part::Box', properties: [], topology },
{ id: 'owner', typeId: 'PartDesign::Feature', properties: [{ name: 'SupportFace', label: 'Support face', group: 'Attachment', scope: 'data', type: 'App::PropertyLinkSub', value: reference, recompute: true }] },
], dependencies: [{ sourceId: 'owner', targetId: 'source', relation: 'topo-ref', propertyName: 'SupportFace', reference: reference.persistentId }],
recompute: { generation: 5, status: 'idle', objectStates: { source: 'up-to-date', owner: 'touched' }, dirtyObjects: ['owner'], order: [], errors: [] },
}
const facade = createMockFacade()
await facade.project.save(document)
await facade.app.document.load(document.id)
const versionBefore = facade.app.document.getActive().version
const resolved = facade.app.document.resolveTopologyReference({ ownerObjectId: 'owner', referenceName: 'SupportFace', candidatePersistentId: candidateIds[1] })
assert.equal(resolved.status, 'stable')
assert.equal(resolved.persistentId, candidateIds[1])
assert.equal(facade.app.document.getActive().version, versionBefore + 1)
assert.equal(facade.app.document.getActive().dirty, true)
assert.equal(facade.app.document.getDependencies().find((edge) => edge.propertyName === 'SupportFace')?.reference, candidateIds[1])
facade.history.undo()
assert.equal((facade.app.document.getObject('owner')?.properties[0].value as typeof reference).status, 'ambiguous')
facade.history.redo()
assert.equal((facade.app.document.getObject('owner')?.properties[0].value as typeof reference).persistentId, candidateIds[1])
facade.geometry.dispose()
})
test('facade async recompute commits only an accepted generation', async () => {
const facade = createMockFacade()
facade.app.document.setProperty({ objectId: 'pad', propertyName: 'Length', value: 51 })