feat: propagate feature suppression in recompute

This commit is contained in:
2026-08-02 22:37:41 -04:00
parent fca408784d
commit bc7adb500b
6 changed files with 102 additions and 26 deletions

View File

@@ -8,7 +8,7 @@ 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 { createSketch, solveSketch } from '../src/facade/sketcher'
import { createFacadeGeometryRecomputeExecutor, RecomputeCoordinator, type RecomputeGeometryRuntime } from '../src/facade/recomputeEngine'
import { createFacadeGeometryRecomputeExecutor, executeFacadeRecomputeNode, RecomputeCoordinator, type RecomputeGeometryRuntime } from '../src/facade/recomputeEngine'
import { inspectFcstdArchive } from '../src/facade/fcstd'
import { assertShapeHandleIntegrity, normalizeBitbybitMesh, validateBooleanUnionInput, validateBoxInput, validateChamferInput, validateConeInput, validateCylinderInput, validateFilletInput, validatePadInput, validatePlacementInput, validatePlanarProfile, validateRevolutionInput, validateSphereInput } from '../src/facade/geometryRuntime'
import type { DocumentSnapshot, ShapeHandle } from '../src/facade/types'
@@ -348,6 +348,25 @@ test('recompute failure marks dependent objects as upstream-failed', async () =>
assert.ok(result.errors.some((error) => error.code === 'UPSTREAM_FAILED'))
})
test('recompute suppression skips dependent nodes without reporting a failure', async () => {
const document = recomputeDocumentFixture([{ sourceId: 'child', targetId: 'root', relation: 'link' }])
document.objects[0].properties.push({ name: 'Suppressed', label: 'Suppressed', group: 'Feature state', scope: 'data', type: 'App::PropertyBool', value: true, recompute: true })
const executed: string[] = []
const coordinator = new RecomputeCoordinator(async (object, source, context) => {
executed.push(object.id)
return executeFacadeRecomputeNode(object, source, context)
}, () => 1)
const result = await coordinator.run(document)
assert.equal(result.status, 'completed')
assert.deepEqual(executed, ['root'])
assert.deepEqual(result.suppressed, ['root'])
assert.deepEqual(result.skipped, ['root', 'child'])
assert.equal(result.objectStates.root, 'suppressed')
assert.equal(result.objectStates.child, 'upstream-suppressed')
assert.deepEqual(result.dirtyObjects, [])
assert.deepEqual(result.errors, [])
})
test('OCCT feature executor builds a Sketch to Pad to Pocket chain and retains the last valid Shape', async () => {
const shape = (id: string): ShapeHandle => ({ id, kernel: 'bitbybit-occt', kind: 'solid', documentId: 'doc-feature', documentVersion: 2 })
const calls: string[] = []
@@ -417,6 +436,12 @@ test('OCCT feature executor builds a Sketch to Pad to Pocket chain and retains t
assert.equal(revolutionResult.status, 'success')
assert.equal(shapes.get('revolution')?.id, 'revolution-shape')
assert.ok(calls.includes('revolution:270:-1'))
const suppressedPad = { ...pad, properties: [...pad.properties, { name: 'Suppressed', label: 'Suppressed', group: 'Feature state', scope: 'data' as const, type: 'App::PropertyBool' as const, value: true }] }
const suppressedResult = await executor(suppressedPad, document, context)
assert.equal(suppressedResult.status, 'suppressed')
assert.equal(shapes.has('pad'), false)
assert.ok(calls.includes('release:pad-shape'))
})
test('Part primitive and boolean nodes execute through the same Shape cache', async () => {
@@ -475,6 +500,28 @@ test('facade async recompute commits only an accepted generation', async () => {
assert.equal(facade.app.document.getActive().recompute?.dirtyObjects.length, 0)
})
test('feature suppression persists and unsuppress recomputes only its dependent closure', async () => {
const facade = createMockFacade()
facade.app.document.setProperty({ objectId: 'pocket', propertyName: 'Suppressed', value: true })
const suppressed = facade.app.document.recompute()
assert.equal(suppressed.status, 'completed')
assert.deepEqual(suppressed.order, ['pocket', 'fillet', 'body'])
assert.deepEqual(new Set(suppressed.affected), new Set(['pocket', 'fillet', 'body']))
assert.equal(facade.app.document.getActive().recompute?.objectStates.pocket, 'suppressed')
assert.equal(facade.app.document.getActive().recompute?.objectStates.fillet, 'upstream-suppressed')
await facade.project.save()
assert.equal((await facade.project.load('doc-pump-housing'))?.objects.find((object) => object.id === 'pocket')?.properties.find((property) => property.name === 'Suppressed')?.value, true)
facade.app.document.setProperty({ objectId: 'pocket', propertyName: 'Suppressed', value: false })
const restored = await facade.app.document.recomputeAsync()
assert.equal(restored.status, 'completed')
assert.deepEqual(restored.order, ['pocket', 'fillet', 'body'])
assert.deepEqual(new Set(restored.affected), new Set(['pocket', 'fillet', 'body']))
assert.equal(facade.app.document.getActive().recompute?.objectStates.pocket, 'up-to-date')
assert.equal(facade.app.document.getActive().recompute?.objectStates.fillet, 'up-to-date')
})
test('persistence writes are serialized and continue after a failed write', async () => {
const queue = new PersistenceWriteQueue()
let inFlight = 0