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

@@ -19,6 +19,7 @@ import type {
SetPropertyInput,
SetExpressionInput,
RecomputeResult,
ResolveTopologyReferenceInput,
ShapeHandle,
TaskSnapshot,
Unsubscribe,
@@ -32,7 +33,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 { cloneObjectTopologySnapshot, migrateDocumentTopologyReferences, parseTopoRef } from './topologyReferences'
import { cloneObjectTopologySnapshot, migrateDocumentTopologyReferences, parseTopoRef, resolveDocumentTopologyReference } from './topologyReferences'
const initialTree: ModelTreeItem[] = [
{ id: 'origin', label: 'Origin', type: 'folder', children: ['XY_Plane', 'XZ_Plane', 'YZ_Plane'] },
@@ -464,6 +465,18 @@ export function createMockFacade(): BitBybitWebCadFacade {
commit({ ...state, document })
notify(`${sourceProperty.label} updated`)
}
const resolveTopologyReference = (input: ResolveTopologyReferenceInput) => {
const document = cloneDocumentSnapshot(state.document)
const resolved = resolveDocumentTopologyReference(document, input)
document.dependencies = collectDependencyEdges(document)
markDocumentTouched(document, [input.ownerObjectId])
document.version += 1
document.dirty = true
const diagnostics = state.diagnostics.filter((diagnostic) => diagnostic.topologyRepair?.ownerObjectId !== input.ownerObjectId || diagnostic.topologyRepair.referenceName !== input.referenceName)
commit({ ...state, document, diagnostics })
notify(`${input.referenceName} topology reference replaced`)
return { ...resolved, candidates: resolved.candidates ? [...resolved.candidates] : undefined }
}
const setExpression = ({ objectId, propertyName, expression }: SetExpressionInput) => {
const objectIndex = state.document.objects.findIndex((object) => object.id === objectId)
if (objectIndex < 0) throw new Error(`Document object does not exist: ${objectId}`)
@@ -566,7 +579,12 @@ export function createMockFacade(): BitBybitWebCadFacade {
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 topologyDiagnostics: Diagnostic[] = topologyMigration.issues.map((issue, index) => ({
const topologyDiagnostics: Diagnostic[] = topologyMigration.issues.map((issue, index) => {
const sourceTopology = document.objects.find((object) => object.id === issue.sourceObjectId)?.topology
const candidates = issue.candidates?.length
? issue.candidates
: sourceTopology?.entries.filter((entry) => entry.ref.kind === issue.kind && entry.ref.status !== 'deleted').map((entry) => entry.ref.persistentId) ?? []
return {
id: `topology:${document.id}:${result.generation}:${issue.ownerObjectId}:${issue.referenceName}:${index}`,
source: 'geometry',
severity: issue.status === 'deleted' ? 'error' : 'warning',
@@ -580,11 +598,12 @@ export function createMockFacade(): BitBybitWebCadFacade {
generation: result.generation,
rootCauseObjectId: issue.sourceObjectId,
dependencyPath: [issue.ownerObjectId, issue.sourceObjectId],
topologyRepair: candidates.length > 0 ? { ownerObjectId: issue.ownerObjectId, referenceName: issue.referenceName, candidates: [...new Set(candidates)] } : undefined,
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]
@@ -738,7 +757,7 @@ export function createMockFacade(): BitBybitWebCadFacade {
}
const facade: BitBybitWebCadFacade = {
app: { document: { getActive: () => getState().document, getObject: (objectId) => { const object = state.document.objects.find((candidate) => candidate.id === objectId); return object ? { ...object, properties: object.properties.map((property) => ({ ...property, options: property.options ? [...property.options] : undefined })), sketch: object.sketch ? cloneSketch(object.sketch) : undefined } : null }, create: (label) => { recomputeCoordinator.cancel(); clearFeatureShapes(); commit({ ...state, document: createDocument(label), selectedObjectId: '' }); return getState().document }, load: loadDocument, markDirty: () => { commit({ ...state, document: { ...state.document, dirty: true } }) }, setProperty, setExpression, recompute: recomputeDocument, recomputeAsync: recomputeDocumentAsync, cancelRecompute: () => recomputeCoordinator.cancel(), getDependencies: () => (state.document.dependencies ?? []).map((edge) => ({ ...edge })) }, expression: { evaluate: (expression, variables = {}) => evaluateQuantityExpression(expression, new Map(Object.entries(variables))), dimensionForUnit: quantityDimensionForUnit }, sketcher: { get: getSketch, addGeometry: addSketchGeometry, addExternalGeometry: addSketchExternalGeometry, addConstraint: addSketchConstraint, solve: solveSketchObject } },
app: { document: { getActive: () => getState().document, getObject: (objectId) => { const object = state.document.objects.find((candidate) => candidate.id === objectId); return object ? { ...object, properties: object.properties.map((property) => ({ ...property, options: property.options ? [...property.options] : undefined })), sketch: object.sketch ? cloneSketch(object.sketch) : undefined, topology: object.topology ? cloneObjectTopologySnapshot(object.topology) : undefined } : null }, create: (label) => { recomputeCoordinator.cancel(); clearFeatureShapes(); commit({ ...state, document: createDocument(label), selectedObjectId: '' }); return getState().document }, load: loadDocument, markDirty: () => { commit({ ...state, document: { ...state.document, dirty: true } }) }, setProperty, resolveTopologyReference, setExpression, recompute: recomputeDocument, recomputeAsync: recomputeDocumentAsync, cancelRecompute: () => recomputeCoordinator.cancel(), getDependencies: () => (state.document.dependencies ?? []).map((edge) => ({ ...edge })) }, expression: { evaluate: (expression, variables = {}) => evaluateQuantityExpression(expression, new Map(Object.entries(variables))), dimensionForUnit: quantityDimensionForUnit }, sketcher: { get: getSketch, addGeometry: addSketchGeometry, addExternalGeometry: addSketchExternalGeometry, addConstraint: addSketchConstraint, solve: solveSketchObject } },
history: { canUndo: () => undoStack.length > 0, canRedo: () => redoStack.length > 0, undo: () => { const previous = undoStack.pop(); if (!previous) return; clearFeatureShapes(); redoStack.push(getState()); state = previous; emitState(); notify('Undo applied') }, redo: () => { const next = redoStack.pop(); if (!next) return; clearFeatureShapes(); undoStack.push(getState()); state = next; emitState(); notify('Redo applied') } },
gui: { workbench: { list: () => Object.keys(workbenchDefinitions) as WorkbenchId[], getActive: () => state.activeWorkbench, setActive }, command: { getState: (commandId) => commandState(commandId, state.activeWorkbench, state.selectedObjectId, state.document.objects.find((object) => object.id === state.selectedObjectId)?.typeId), list: (workbench) => workbenchDefinitions[workbench].groups.flatMap((group) => group.commands), execute } },
selection: { getObjectId: () => state.selectedObjectId, select, clear: () => select('') },