P5: add facade sketch model solver and persistence
This commit is contained in:
@@ -24,6 +24,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'
|
||||
|
||||
const initialTree: ModelTreeItem[] = [
|
||||
{ id: 'origin', label: 'Origin', type: 'folder', children: ['XY_Plane', 'XZ_Plane', 'YZ_Plane'] },
|
||||
@@ -81,12 +82,12 @@ const featureProperties = (item: ModelTreeItem): ObjectPropertySnapshot[] => {
|
||||
return []
|
||||
}
|
||||
|
||||
const createObjectSnapshot = (item: ModelTreeItem): DocumentObjectSnapshot => ({ id: item.id, typeId: typeIdForItem(item), properties: [...commonProperties(item), ...featureProperties(item), ...viewProperties()] })
|
||||
const createObjectSnapshot = (item: ModelTreeItem): DocumentObjectSnapshot => ({ id: item.id, typeId: typeIdForItem(item), properties: [...commonProperties(item), ...featureProperties(item), ...viewProperties()], sketch: item.type === 'sketch' ? createSketch(item.id) : undefined })
|
||||
|
||||
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, options: property.options ? [...property.options] : undefined })) })),
|
||||
objects: document.objects.map((object) => ({ ...object, properties: object.properties.map((property) => ({ ...property, options: property.options ? [...property.options] : undefined })), sketch: object.sketch ? cloneSketch(object.sketch) : 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,
|
||||
})
|
||||
@@ -242,7 +243,7 @@ export function createMockFacade(): BitBybitWebCadFacade {
|
||||
if (body) body.children = [...(body.children || []), objectId]
|
||||
tree.push(item)
|
||||
}
|
||||
const objects = [...document.objects.map((object) => ({ ...object, properties: object.properties.map((property) => ({ ...property })) })), createObjectSnapshot(item)]
|
||||
const objects = [...document.objects.map((object) => ({ ...object, properties: object.properties.map((property) => ({ ...property })), sketch: object.sketch ? cloneSketch(object.sketch) : undefined })), createObjectSnapshot(item)]
|
||||
const nextDocument: DocumentSnapshot = { ...document, version: document.version + 1, dirty: true, tree, objects }
|
||||
nextDocument.dependencies = collectDependencyEdges(nextDocument)
|
||||
nextDocument.recompute = createRecomputeSnapshot(objects.map((object) => object.id), document.recompute?.generation ?? 0)
|
||||
@@ -331,6 +332,52 @@ export function createMockFacade(): BitBybitWebCadFacade {
|
||||
emitState()
|
||||
return { ...plan, generation, status: nextRecompute.status, errors }
|
||||
}
|
||||
const getSketch = (objectId: string) => {
|
||||
const object = state.document.objects.find((candidate) => candidate.id === objectId)
|
||||
return object?.sketch ? cloneSketch(object.sketch) : null
|
||||
}
|
||||
const updateSketch = (objectId: string, update: (sketch: SketchSnapshot) => void) => {
|
||||
const objectIndex = state.document.objects.findIndex((object) => object.id === objectId)
|
||||
if (objectIndex < 0) throw new Error(`Document object does not exist: ${objectId}`)
|
||||
const source = state.document.objects[objectIndex]
|
||||
if (!source.sketch) throw new TypeError(`${objectId} is not a Sketcher object.`)
|
||||
const document = cloneDocumentSnapshot(state.document)
|
||||
const sketch = document.objects[objectIndex].sketch as SketchSnapshot
|
||||
update(sketch)
|
||||
const solved = solveSketch(sketch)
|
||||
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'
|
||||
markDocumentTouched(document, [objectId])
|
||||
document.version += 1
|
||||
document.dirty = true
|
||||
commit({ ...state, document })
|
||||
return cloneSketch(solved.snapshot)
|
||||
}
|
||||
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)
|
||||
})
|
||||
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)
|
||||
})
|
||||
const solveSketchObject = (objectId: string) => {
|
||||
const objectIndex = state.document.objects.findIndex((object) => object.id === objectId)
|
||||
if (objectIndex < 0) throw new Error(`Document object does not exist: ${objectId}`)
|
||||
const source = state.document.objects[objectIndex]
|
||||
if (!source.sketch) throw new TypeError(`${objectId} is not a Sketcher object.`)
|
||||
const result = solveSketch(source.sketch)
|
||||
const document = cloneDocumentSnapshot(state.document)
|
||||
document.objects[objectIndex].sketch = result.snapshot
|
||||
const status = document.objects[objectIndex].properties.find((property) => property.name === 'ConstraintStatus')
|
||||
if (status) status.value = result.status === 'solved' ? 'Fully constrained' : result.status === 'under-constrained' ? `Under-constrained (${result.degreesOfFreedom} DOF)` : result.status === 'conflicting' ? 'Conflicting constraints' : 'Invalid constraints'
|
||||
markDocumentTouched(document, [objectId])
|
||||
document.version += 1
|
||||
document.dirty = true
|
||||
commit({ ...state, document })
|
||||
return result
|
||||
}
|
||||
const applyTask = () => {
|
||||
const task = state.task
|
||||
if (!task || task.status !== 'preview') return
|
||||
@@ -369,7 +416,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 })) } : null }, create: (label) => { commit({ ...state, document: createDocument(label), selectedObjectId: '' }); return getState().document }, markDirty: () => { commit({ ...state, document: { ...state.document, dirty: true } }) }, setProperty, setExpression, recompute: recomputeDocument, getDependencies: () => (state.document.dependencies ?? []).map((edge) => ({ ...edge })) }, expression: { evaluate: (expression, variables = {}) => evaluateQuantityExpression(expression, new Map(Object.entries(variables))), dimensionForUnit: quantityDimensionForUnit } },
|
||||
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) => { commit({ ...state, document: createDocument(label), selectedObjectId: '' }); return getState().document }, markDirty: () => { commit({ ...state, document: { ...state.document, dirty: true } }) }, setProperty, setExpression, recompute: recomputeDocument, 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 } },
|
||||
history: { canUndo: () => undoStack.length > 0, canRedo: () => redoStack.length > 0, undo: () => { const previous = undoStack.pop(); if (!previous) return; redoStack.push(getState()); state = previous; emitState(); notify('Undo applied') }, redo: () => { const next = redoStack.pop(); if (!next) return; 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), list: (workbench) => workbenchDefinitions[workbench].groups.flatMap((group) => group.commands), execute } },
|
||||
selection: { getObjectId: () => state.selectedObjectId, select, clear: () => select('') },
|
||||
|
||||
Reference in New Issue
Block a user