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

@@ -18,7 +18,7 @@ export type RecomputeNodeContext = {
}
export type RecomputeNodeResult = {
status: 'success' | 'failed'
status: 'success' | 'suppressed' | 'failed'
errors?: RecomputeExecutionError[]
updatedObject?: DocumentObjectSnapshot
}
@@ -68,6 +68,7 @@ export type RecomputeExecutionResult = {
order: string[]
levels: string[][]
completed: string[]
suppressed: string[]
failed: string[]
skipped: string[]
dirtyObjects: string[]
@@ -108,6 +109,7 @@ export class RecomputeCoordinator {
for (const objectId of plan.affected) objectStates[objectId] = 'recomputing'
const completed: string[] = []
const suppressed: string[] = []
const failed: string[] = []
const skipped: string[] = []
const errors: RecomputeExecutionError[] = []
@@ -125,7 +127,9 @@ export class RecomputeCoordinator {
const terminalResult = (status: RecomputeExecutionStatus): RecomputeExecutionResult => {
if (status === 'cancelled' || status === 'stale') {
for (const objectId of plan.affected) if (!completed.includes(objectId)) objectStates[objectId] = 'touched'
for (const objectId of plan.affected) {
if (objectStates[objectId] !== 'up-to-date' && objectStates[objectId] !== 'suppressed' && objectStates[objectId] !== 'upstream-suppressed') objectStates[objectId] = 'touched'
}
}
if (this.active?.generation === generation) this.active = null
return {
@@ -136,9 +140,10 @@ export class RecomputeCoordinator {
order: plan.order,
levels: plan.levels,
completed,
suppressed,
failed,
skipped,
dirtyObjects: plan.affected.filter((objectId) => objectStates[objectId] !== 'up-to-date'),
dirtyObjects: plan.affected.filter((objectId) => objectStates[objectId] === 'touched' || objectStates[objectId] === 'recomputing' || objectStates[objectId] === 'error' || objectStates[objectId] === 'upstream-failed'),
objectStates,
objectUpdates,
errors,
@@ -153,8 +158,11 @@ export class RecomputeCoordinator {
// Nodes in a level have no dependencies on each other. Execute them together,
// then merge outcomes in plan order so persistence and UI events stay deterministic.
const outcomes = await Promise.all(level.map(async (objectId) => {
const failedDependency = graph.dependenciesOf(objectId).find((dependencyId) => objectStates[dependencyId] === 'error' || objectStates[dependencyId] === 'upstream-failed')
const dependencies = graph.dependenciesOf(objectId)
const failedDependency = dependencies.find((dependencyId) => objectStates[dependencyId] === 'error' || objectStates[dependencyId] === 'upstream-failed')
if (failedDependency) return { objectId, state: 'upstream-failed' as const, error: { objectId, code: 'UPSTREAM_FAILED', message: `Dependency ${failedDependency} did not recompute successfully.` } }
const suppressedDependency = dependencies.find((dependencyId) => objectStates[dependencyId] === 'suppressed' || objectStates[dependencyId] === 'upstream-suppressed')
if (suppressedDependency) return { objectId, state: 'upstream-suppressed' as const }
const object = objectById.get(objectId)
if (!object) return { objectId, state: 'error' as const, error: { objectId, code: 'OBJECT_NOT_FOUND', message: `Document object does not exist: ${objectId}` } }
@@ -173,6 +181,7 @@ export class RecomputeCoordinator {
errors: result.errors?.length ? result.errors : [{ objectId, code: 'RECOMPUTE_FAILED', message: `${objectId} failed to recompute.` }],
}
}
if (result.status === 'suppressed') return { objectId, state: 'suppressed' as const }
return { objectId, state: 'up-to-date' as const, updatedObject: result.updatedObject }
} catch (error) {
if (controller.signal.aborted || isAbortError(error)) return { objectId, state: 'cancelled' as const }
@@ -190,6 +199,11 @@ export class RecomputeCoordinator {
if (outcome.state === 'up-to-date') {
completed.push(outcome.objectId)
if (outcome.updatedObject) objectUpdates.push(outcome.updatedObject)
} else if (outcome.state === 'suppressed') {
suppressed.push(outcome.objectId)
skipped.push(outcome.objectId)
} else if (outcome.state === 'upstream-suppressed') {
skipped.push(outcome.objectId)
} else if (outcome.state === 'upstream-failed') {
skipped.push(outcome.objectId)
if (outcome.error) errors.push(outcome.error)
@@ -209,6 +223,7 @@ export class RecomputeCoordinator {
export const executeFacadeRecomputeNode: RecomputeNodeExecutor = async (object, _document, context) => {
if (context.signal.aborted) throw new DOMException('Recompute cancelled.', 'AbortError')
if (object.properties.some((property) => property.name === 'Suppressed' && property.value === true)) return { status: 'suppressed' }
const expressionError = object.properties.find((property) => property.expressionError)
if (expressionError) {
return {
@@ -279,6 +294,12 @@ export const createFacadeGeometryRecomputeExecutor = (
shapes: Map<string, ShapeHandle> = new Map(),
): RecomputeNodeExecutor => async (object, document, context) => {
const base = await executeFacadeRecomputeNode(object, document, context)
if (base.status === 'suppressed') {
const previous = shapes.get(object.id)
shapes.delete(object.id)
if (previous) await geometry.release(previous)
return base
}
if (base.status === 'failed' || object.sketch || geometry.capabilities().status !== 'ready') return base
if (!['Part::Box', 'Part::Cylinder', 'Part::Sphere', 'Part::Cone', 'Part::Fuse', 'Part::Cut', 'Part::Common', 'PartDesign::Pad', 'PartDesign::Pocket', 'PartDesign::Revolution', 'PartDesign::Fillet', 'PartDesign::Chamfer'].includes(object.typeId)) return base