feat: propagate feature suppression in recompute
This commit is contained in:
@@ -15,7 +15,7 @@ export type RecomputePlan = {
|
||||
cycles: string[][]
|
||||
}
|
||||
|
||||
export type RecomputeState = 'up-to-date' | 'touched' | 'recomputing' | 'error' | 'upstream-failed'
|
||||
export type RecomputeState = 'up-to-date' | 'touched' | 'recomputing' | 'suppressed' | 'upstream-suppressed' | 'error' | 'upstream-failed'
|
||||
|
||||
export type RecomputeSnapshot = {
|
||||
generation: number
|
||||
|
||||
@@ -45,6 +45,7 @@ const commonProperties = (item: ModelTreeItem): ObjectPropertySnapshot[] => [
|
||||
{ name: 'Label', label: 'Label', group: 'Identity', scope: 'data', type: 'App::PropertyString', value: item.label },
|
||||
{ name: 'TypeId', label: 'Type', group: 'Identity', scope: 'data', type: 'App::PropertyString', value: typeIdForItem(item), readOnly: true },
|
||||
{ name: 'Status', label: 'Status', group: 'Identity', scope: 'data', type: 'App::PropertyString', value: item.state === 'warning' ? 'Warning' : 'Valid', readOnly: true },
|
||||
...(item.type === 'feature' ? [{ name: 'Suppressed', label: 'Suppressed', group: 'Feature state', scope: 'data' as const, type: 'App::PropertyBool' as const, value: false, recompute: true }] : []),
|
||||
]
|
||||
|
||||
const viewProperties = (): ObjectPropertySnapshot[] => [
|
||||
@@ -286,6 +287,11 @@ export function createMockFacade(): BitBybitWebCadFacade {
|
||||
createFacadeGeometryRecomputeExecutor(geometryRuntime, featureShapes),
|
||||
(documentId) => state.document.id === documentId ? state.document.version : null,
|
||||
)
|
||||
const releaseFeatureShape = (objectId: string) => {
|
||||
const shape = featureShapes.get(objectId)
|
||||
featureShapes.delete(objectId)
|
||||
if (shape) void geometryRuntime.release(shape)
|
||||
}
|
||||
const clearFeatureShapes = () => {
|
||||
const retained = [...featureShapes.values()]
|
||||
featureShapes.clear()
|
||||
@@ -414,20 +420,21 @@ export function createMockFacade(): BitBybitWebCadFacade {
|
||||
const message = `Dependency cycle: ${cycle.join(' -> ')}`
|
||||
for (const objectId of cycle) { objectStates[objectId] = 'error'; errors.push({ objectId, code: 'DEPENDENCY_CYCLE', message }) }
|
||||
}
|
||||
if (errors.length === 0) {
|
||||
for (const objectId of plan.order) {
|
||||
objectStates[objectId] = 'up-to-date'
|
||||
const item = document.tree.find((candidate) => candidate.id === objectId)
|
||||
if (item?.state === 'dirty') item.state = item.type === 'body' ? 'active' : 'valid'
|
||||
const status = document.objects.find((candidate) => candidate.id === objectId)?.properties.find((property) => property.name === 'Status')
|
||||
if (status?.value === 'Touched') status.value = 'Valid'
|
||||
}
|
||||
} else {
|
||||
for (const objectId of plan.affected) if (objectStates[objectId] !== 'error') objectStates[objectId] = 'upstream-failed'
|
||||
for (const objectId of plan.affected) {
|
||||
const item = document.tree.find((candidate) => candidate.id === objectId)
|
||||
if (item && item.state !== 'active') item.state = 'warning'
|
||||
for (const objectId of plan.order) {
|
||||
const dependencies = graph.dependenciesOf(objectId)
|
||||
if (dependencies.some((dependencyId) => objectStates[dependencyId] === 'error' || objectStates[dependencyId] === 'upstream-failed')) {
|
||||
objectStates[objectId] = 'upstream-failed'
|
||||
} else if (dependencies.some((dependencyId) => objectStates[dependencyId] === 'suppressed' || objectStates[dependencyId] === 'upstream-suppressed')) {
|
||||
objectStates[objectId] = 'upstream-suppressed'
|
||||
} else {
|
||||
const object = document.objects.find((candidate) => candidate.id === objectId)
|
||||
objectStates[objectId] = object?.properties.some((property) => property.name === 'Suppressed' && property.value === true) ? 'suppressed' : 'up-to-date'
|
||||
}
|
||||
if (objectStates[objectId] === 'suppressed' || objectStates[objectId] === 'upstream-suppressed') releaseFeatureShape(objectId)
|
||||
const item = document.tree.find((candidate) => candidate.id === objectId)
|
||||
if (item && item.state !== 'readonly') item.state = objectStates[objectId] === 'upstream-failed' ? 'warning' : item.type === 'body' ? 'active' : 'valid'
|
||||
const status = document.objects.find((candidate) => candidate.id === objectId)?.properties.find((property) => property.name === 'Status')
|
||||
if (status) status.value = objectStates[objectId] === 'suppressed' ? 'Suppressed' : objectStates[objectId] === 'upstream-suppressed' ? 'Upstream suppressed' : objectStates[objectId] === 'up-to-date' ? 'Valid' : 'Warning'
|
||||
}
|
||||
const nextRecompute = { generation, status: errors.length === 0 ? 'completed' as const : 'failed' as const, objectStates, dirtyObjects: errors.length === 0 ? [] : plan.affected, order: plan.order, errors }
|
||||
document.recompute = nextRecompute
|
||||
@@ -445,12 +452,13 @@ export function createMockFacade(): BitBybitWebCadFacade {
|
||||
const updates = new Map(result.objectUpdates.map((object) => [object.id, object]))
|
||||
document.objects = document.objects.map((object) => updates.has(object.id) ? updates.get(object.id) as DocumentObjectSnapshot : object)
|
||||
for (const objectId of result.affected) {
|
||||
if (result.objectStates[objectId] === 'suppressed' || result.objectStates[objectId] === 'upstream-suppressed') releaseFeatureShape(objectId)
|
||||
const item = document.tree.find((candidate) => candidate.id === objectId)
|
||||
if (!item || item.state === 'readonly') continue
|
||||
if (result.objectStates[objectId] === 'up-to-date') item.state = item.type === 'body' ? 'active' : 'valid'
|
||||
if (result.objectStates[objectId] === 'up-to-date' || result.objectStates[objectId] === 'suppressed' || result.objectStates[objectId] === 'upstream-suppressed') item.state = item.type === 'body' ? 'active' : 'valid'
|
||||
else if (result.objectStates[objectId] === 'error' || result.objectStates[objectId] === 'upstream-failed') item.state = 'warning'
|
||||
const status = document.objects.find((candidate) => candidate.id === objectId)?.properties.find((property) => property.name === 'Status')
|
||||
if (status) status.value = result.objectStates[objectId] === 'up-to-date' ? 'Valid' : 'Warning'
|
||||
if (status) status.value = result.objectStates[objectId] === 'suppressed' ? 'Suppressed' : result.objectStates[objectId] === 'upstream-suppressed' ? 'Upstream suppressed' : result.objectStates[objectId] === 'up-to-date' ? 'Valid' : 'Warning'
|
||||
}
|
||||
document.recompute = {
|
||||
generation: result.generation,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user