feat: persist stable sketch external geometry

This commit is contained in:
2026-08-02 23:54:56 -04:00
parent 6786261f16
commit a8f902ceb5
7 changed files with 61 additions and 15 deletions

View File

@@ -27,7 +27,7 @@ import { BitbybitGeometryRuntime } from './geometryRuntime'
import { ThreeViewportAdapter } from './threeViewport'
import { DependencyGraph, createRecomputeSnapshot, type DependencyEdge } from './dependencyGraph'
import { convertQuantity, evaluateQuantityExpression, getUnit, quantityDimensionForUnit, quantityFromNumber, quantityFromUnit, type Quantity } from './units'
import { cloneSketch, createSketch, solveSketch, type SketchConstraint, type SketchGeometry, type SketchSnapshot } from './sketcher'
import { cloneSketch, cloneSketchConstraint, cloneSketchGeometry, createSketch, solveSketch, type SketchConstraint, type SketchExternalGeometry, type SketchGeometry, type SketchSnapshot } from './sketcher'
import { createFacadeGeometryRecomputeExecutor, RecomputeCoordinator, type RecomputeExecutionOptions } from './recomputeEngine'
import { inspectFcstdArchive } from './fcstd'
import { buildDiagnosticTree, buildRecomputeDiagnostics, cloneDiagnostic, replaceRecomputeDiagnostics } from './diagnostics'
@@ -168,6 +168,9 @@ const collectDependencyEdges = (document: Pick<DocumentSnapshot, 'objects'>): De
}
}
}
for (const external of object.sketch?.externalGeometry ?? []) {
if (objectIds.has(external.source.objectId)) edges.push({ sourceId: object.id, targetId: external.source.objectId, relation: 'topo-ref', propertyName: `ExternalGeometry:${external.id}`, reference: external.source.persistentId })
}
}
return edges
}
@@ -572,6 +575,7 @@ export function createMockFacade(): BitBybitWebCadFacade {
document.objects[objectIndex].sketch = solved.snapshot
const status = document.objects[objectIndex].properties.find((property) => property.name === 'ConstraintStatus')
if (status) status.value = solved.status === 'solved' ? 'Fully constrained' : solved.status === 'under-constrained' ? `Under-constrained (${solved.degreesOfFreedom} DOF)` : solved.status === 'conflicting' ? 'Conflicting constraints' : 'Invalid constraints'
document.dependencies = collectDependencyEdges(document)
markDocumentTouched(document, [objectId])
document.version += 1
document.dirty = true
@@ -580,11 +584,18 @@ export function createMockFacade(): BitBybitWebCadFacade {
}
const addSketchGeometry = (objectId: string, geometry: SketchGeometry) => updateSketch(objectId, (sketch) => {
if (sketch.geometry.some((candidate) => candidate.id === geometry.id)) throw new RangeError(`Sketch geometry already exists: ${geometry.id}`)
sketch.geometry.push({ ...geometry } as SketchGeometry)
sketch.geometry.push(cloneSketchGeometry(geometry))
})
const addSketchExternalGeometry = (objectId: string, external: SketchExternalGeometry) => updateSketch(objectId, (sketch) => {
if (!external.id.trim()) throw new RangeError('External geometry id is required.')
if (sketch.externalGeometry.some((candidate) => candidate.id === external.id)) throw new RangeError(`External geometry already exists: ${external.id}`)
if (external.source.objectId === objectId) throw new RangeError('A sketch cannot import external geometry from itself.')
if (external.source.status === 'deleted') throw new RangeError('Deleted topology cannot be imported as external geometry.')
sketch.externalGeometry.push({ ...external, source: { ...external.source, candidates: external.source.candidates ? [...external.source.candidates] : undefined }, projection: cloneSketchGeometry(external.projection), construction: true })
})
const addSketchConstraint = (objectId: string, constraint: SketchConstraint) => updateSketch(objectId, (sketch) => {
if (sketch.constraints.some((candidate) => candidate.id === constraint.id)) throw new RangeError(`Sketch constraint already exists: ${constraint.id}`)
sketch.constraints.push({ ...constraint } as SketchConstraint)
sketch.constraints.push(cloneSketchConstraint(constraint))
})
const solveSketchObject = (objectId: string) => {
const objectIndex = state.document.objects.findIndex((object) => object.id === objectId)
@@ -664,7 +675,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, 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 } : 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 } },
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('') },