feat: persist generation topology history

This commit is contained in:
2026-08-03 01:12:25 -04:00
parent 0fc2cfe41b
commit ffbe2c69dc
16 changed files with 404 additions and 63 deletions

View File

@@ -32,7 +32,7 @@ import { cloneSketch, cloneSketchConstraint, cloneSketchGeometry, createSketch,
import { createFacadeGeometryRecomputeExecutor, RecomputeCoordinator, type RecomputeExecutionOptions } from './recomputeEngine'
import { inspectFcstdArchive } from './fcstd'
import { buildDiagnosticTree, buildRecomputeDiagnostics, cloneDiagnostic, replaceRecomputeDiagnostics } from './diagnostics'
import { parseTopoRef } from './topologyReferences'
import { cloneObjectTopologySnapshot, migrateDocumentTopologyReferences, parseTopoRef } from './topologyReferences'
const initialTree: ModelTreeItem[] = [
{ id: 'origin', label: 'Origin', type: 'folder', children: ['XY_Plane', 'XZ_Plane', 'YZ_Plane'] },
@@ -156,7 +156,7 @@ const clonePropertyValue = (value: PropertyValue): PropertyValue => {
const cloneDocumentSnapshot = (document: DocumentSnapshot): DocumentSnapshot => ({
...document,
tree: document.tree.map((item) => ({ ...item, children: item.children ? [...item.children] : undefined })),
objects: document.objects.map((object) => ({ ...object, properties: object.properties.map((property) => ({ ...property, value: clonePropertyValue(property.value), options: property.options ? [...property.options] : undefined })), sketch: object.sketch ? cloneSketch(object.sketch) : undefined })),
objects: document.objects.map((object) => ({ ...object, properties: object.properties.map((property) => ({ ...property, value: clonePropertyValue(property.value), options: property.options ? [...property.options] : undefined })), sketch: object.sketch ? cloneSketch(object.sketch) : undefined, topology: object.topology ? cloneObjectTopologySnapshot(object.topology) : undefined })),
dependencies: document.dependencies?.map((edge) => ({ ...edge })),
recompute: document.recompute ? { ...document.recompute, dirtyObjects: [...document.recompute.dirtyObjects], order: [...document.recompute.order], objectStates: { ...document.recompute.objectStates }, errors: document.recompute.errors.map((error) => ({ ...error })) } : undefined,
})
@@ -538,6 +538,8 @@ export function createMockFacade(): BitBybitWebCadFacade {
const document = cloneDocumentSnapshot(state.document)
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)
const topologyMigration = migrateDocumentTopologyReferences(document, result.objectUpdates.map((object) => object.id))
document.dependencies = collectDependencyEdges(document)
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)
@@ -555,8 +557,37 @@ export function createMockFacade(): BitBybitWebCadFacade {
order: result.order,
errors: result.errors,
}
for (const objectId of topologyMigration.changedOwnerIds) {
document.recompute.objectStates[objectId] = 'touched'
if (!document.recompute.dirtyObjects.includes(objectId)) document.recompute.dirtyObjects.push(objectId)
const item = document.tree.find((candidate) => candidate.id === objectId)
if (item && item.state !== 'readonly') item.state = topologyMigration.issues.some((issue) => issue.ownerObjectId === objectId) ? 'warning' : 'dirty'
const status = document.objects.find((candidate) => candidate.id === objectId)?.properties.find((property) => property.name === 'Status')
if (status) status.value = topologyMigration.issues.some((issue) => issue.ownerObjectId === objectId) ? 'Topology reference requires repair' : 'Touched'
}
const nextDiagnostics = buildRecomputeDiagnostics({ document, generation: result.generation, affected: result.affected, objectStates: result.objectStates, errors: result.errors })
const diagnostics = replaceRecomputeDiagnostics(state.diagnostics, document.id, result.affected, nextDiagnostics)
const topologyDiagnostics: Diagnostic[] = topologyMigration.issues.map((issue, index) => ({
id: `topology:${document.id}:${result.generation}:${issue.ownerObjectId}:${issue.referenceName}:${index}`,
source: 'geometry',
severity: issue.status === 'deleted' ? 'error' : 'warning',
code: issue.status === 'deleted' ? 'TOPOLOGY_REFERENCE_DELETED' : 'TOPOLOGY_REFERENCE_AMBIGUOUS',
message: issue.status === 'deleted'
? `${issue.referenceName} no longer resolves on ${issue.sourceObjectId}; select a replacement subshape.`
: `${issue.referenceName} resolves to multiple subshapes on ${issue.sourceObjectId}: ${(issue.candidates ?? []).join(', ')}.`,
objectId: issue.ownerObjectId,
documentId: document.id,
documentVersion: document.version,
generation: result.generation,
rootCauseObjectId: issue.sourceObjectId,
dependencyPath: [issue.ownerObjectId, issue.sourceObjectId],
repairActions: [
{ id: 'select-object', label: 'Select reference owner', targetObjectId: issue.ownerObjectId, enabled: true },
{ id: 'recompute-root', label: 'Recompute after replacing reference', targetObjectId: issue.ownerObjectId, enabled: false, reason: 'Select one current subshape and replace the ambiguous or deleted reference first.' },
],
}))
const replaced = replaceRecomputeDiagnostics(state.diagnostics, document.id, result.affected, nextDiagnostics)
const topologyOwners = new Set(topologyMigration.issues.map((issue) => issue.ownerObjectId))
const diagnostics = [...replaced.filter((diagnostic) => !diagnostic.code.startsWith('TOPOLOGY_REFERENCE_') || diagnostic.documentId !== document.id || !diagnostic.objectId || !topologyOwners.has(diagnostic.objectId)), ...topologyDiagnostics]
state = { ...state, document, diagnostics }
const context: FacadeRequestContext = { apiVersion: state.apiVersion, requestId: `recompute-${result.generation}`, documentId: document.id, documentVersion: document.version, workbench: state.activeWorkbench }
for (const diagnostic of nextDiagnostics) emit({ type: 'diagnostic.added', diagnostic, context })