P3/P4: execute OCCT features and export shapes
This commit is contained in:
@@ -16,6 +16,7 @@ import type {
|
||||
SetPropertyInput,
|
||||
SetExpressionInput,
|
||||
RecomputeResult,
|
||||
ShapeHandle,
|
||||
TaskSnapshot,
|
||||
Unsubscribe,
|
||||
} from './types'
|
||||
@@ -25,7 +26,7 @@ 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 { executeFacadeRecomputeNode, RecomputeCoordinator, type RecomputeExecutionOptions } from './recomputeEngine'
|
||||
import { createFacadeGeometryRecomputeExecutor, RecomputeCoordinator, type RecomputeExecutionOptions } from './recomputeEngine'
|
||||
import { inspectFcstdArchive } from './fcstd'
|
||||
|
||||
const initialTree: ModelTreeItem[] = [
|
||||
@@ -38,7 +39,7 @@ const initialTree: ModelTreeItem[] = [
|
||||
{ id: 'reference', label: 'Reference geometry', type: 'folder', children: ['DatumPlane', 'DatumAxis'] },
|
||||
]
|
||||
|
||||
const typeIdForItem = (item: ModelTreeItem) => item.type === 'body' ? 'PartDesign::Body' : item.type === 'sketch' ? 'Sketcher::SketchObject' : item.id.startsWith('pad') ? 'PartDesign::Pad' : item.id.startsWith('pocket') ? 'PartDesign::Pocket' : item.id.startsWith('fillet') ? 'PartDesign::Fillet' : item.type === 'feature' ? 'PartDesign::Feature' : 'App::DocumentObjectGroup'
|
||||
const typeIdForItem = (item: ModelTreeItem) => item.type === 'body' ? 'PartDesign::Body' : item.type === 'sketch' ? 'Sketcher::SketchObject' : item.id.startsWith('pad') ? 'PartDesign::Pad' : item.id.startsWith('pocket') ? 'PartDesign::Pocket' : item.id.startsWith('fillet') ? 'PartDesign::Fillet' : item.id.startsWith('chamfer') ? 'PartDesign::Chamfer' : item.type === 'feature' ? 'PartDesign::Feature' : 'App::DocumentObjectGroup'
|
||||
|
||||
const commonProperties = (item: ModelTreeItem): ObjectPropertySnapshot[] => [
|
||||
{ name: 'Label', label: 'Label', group: 'Identity', scope: 'data', type: 'App::PropertyString', value: item.label },
|
||||
@@ -74,6 +75,10 @@ const featureProperties = (item: ModelTreeItem): ObjectPropertySnapshot[] => {
|
||||
{ name: 'Radius', label: 'Radius', group: 'Parameters', scope: 'data', type: 'App::PropertyLength', value: 3, unit: 'mm', recompute: true },
|
||||
{ name: 'Base', label: 'Base', group: 'Dependencies', scope: 'data', type: 'App::PropertyLink', value: 'pocket', recompute: true },
|
||||
]
|
||||
if (item.id.startsWith('chamfer')) return [
|
||||
{ name: 'Distance', label: 'Distance', group: 'Parameters', scope: 'data', type: 'App::PropertyLength', value: 2, unit: 'mm', recompute: true },
|
||||
{ name: 'Base', label: 'Base', group: 'Dependencies', scope: 'data', type: 'App::PropertyLink', value: 'pocket', recompute: true },
|
||||
]
|
||||
if (item.type === 'sketch') return [
|
||||
{ name: 'Support', label: 'Support', group: 'Attachment', scope: 'data', type: 'App::PropertyLink', value: 'XY_Plane', recompute: true },
|
||||
{ name: 'ConstraintStatus', label: 'Solver state', group: 'Constraints', scope: 'data', type: 'App::PropertyString', value: 'Fully constrained', readOnly: true },
|
||||
@@ -216,12 +221,18 @@ const markDocumentTouched = (document: DocumentSnapshot, objectIds: Iterable<str
|
||||
export function createMockFacade(): BitBybitWebCadFacade {
|
||||
const projectPersistence = createSqliteProjectPersistence()
|
||||
const geometryRuntime = new BitbybitGeometryRuntime()
|
||||
const featureShapes = new Map<string, ShapeHandle>()
|
||||
const autosave = new ProjectAutosaveScheduler((document) => projectPersistence.save(document))
|
||||
let state: FacadeState = { apiVersion: '0.1', activeWorkbench: 'Part Design', selectedObjectId: 'pad', document: createDocument(), persistence: projectPersistence.capabilities(), task: null, lastNotice: '', diagnostics: [] }
|
||||
const recomputeCoordinator = new RecomputeCoordinator(
|
||||
executeFacadeRecomputeNode,
|
||||
createFacadeGeometryRecomputeExecutor(geometryRuntime, featureShapes),
|
||||
(documentId) => state.document.id === documentId ? state.document.version : null,
|
||||
)
|
||||
const clearFeatureShapes = () => {
|
||||
const retained = [...featureShapes.values()]
|
||||
featureShapes.clear()
|
||||
void Promise.all(retained.map((shape) => geometryRuntime.release(shape)))
|
||||
}
|
||||
const listeners = new Set<FacadeListener>()
|
||||
const undoStack: FacadeState[] = []
|
||||
const redoStack: FacadeState[] = []
|
||||
@@ -440,7 +451,7 @@ export function createMockFacade(): BitBybitWebCadFacade {
|
||||
emit({ type: 'diagnostic.added', diagnostic, context }); emit({ type: 'command.failed', commandId, context, message: status.reason }); notify(status.reason || 'Command is disabled'); return requestId
|
||||
}
|
||||
emit({ type: 'command.started', commandId, context })
|
||||
if (commandId === 'new-document') commit({ ...state, document: createDocument('Untitled document'), selectedObjectId: '' })
|
||||
if (commandId === 'new-document') { clearFeatureShapes(); commit({ ...state, document: createDocument('Untitled document'), selectedObjectId: '' }) }
|
||||
else if (commandId === 'save') {
|
||||
const savedDocument = { ...state.document, dirty: false, version: state.document.version + 1 }
|
||||
commit({ ...state, document: savedDocument })
|
||||
@@ -458,13 +469,13 @@ 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(); commit({ ...state, document: createDocument(label), selectedObjectId: '' }); return getState().document }, 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 } },
|
||||
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') } },
|
||||
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 }, 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 } },
|
||||
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), list: (workbench) => workbenchDefinitions[workbench].groups.flatMap((group) => group.commands), execute } },
|
||||
selection: { getObjectId: () => state.selectedObjectId, select, clear: () => select('') },
|
||||
task: { getActive: () => getState().task, begin: beginTask, update: (draft) => { if (state.task) state = { ...state, task: { ...state.task, draft: { ...state.task.draft, ...draft } } }; emitState() }, apply: applyTask, cancel: () => { if (state.task) state = { ...state, task: { ...state.task, status: 'cancelled' } }; emitState() } },
|
||||
project: { capabilities: () => projectPersistence.capabilities(), subscribeExternalChanges: (listener) => projectPersistence.subscribeExternalChanges(listener), save: (document = getState().document) => projectPersistence.save(document), load: (documentId) => projectPersistence.load(documentId), recovery: (documentId) => projectPersistence.recovery(documentId), fcstd: { inspect: (bytes, limits) => inspectFcstdArchive(bytes, limits) }, resource: projectPersistence.resource },
|
||||
geometry: { capabilities: () => geometryRuntime.capabilities(), initialize: () => geometryRuntime.initialize(), createBox: (input) => geometryRuntime.createBox(input), createCylinder: (input) => geometryRuntime.createCylinder(input), createSphere: (input) => geometryRuntime.createSphere(input), createCone: (input) => geometryRuntime.createCone(input), applyPlacement: (input) => geometryRuntime.applyPlacement(input), union: (input) => geometryRuntime.union(input), cut: (input) => geometryRuntime.cut(input), intersection: (input) => geometryRuntime.intersection(input), fillet: (input) => geometryRuntime.fillet(input), chamfer: (input) => geometryRuntime.chamfer(input), exportStep: (shape, fileName) => geometryRuntime.exportStep(shape, fileName), exportStl: (shape, fileName, precision) => geometryRuntime.exportStl(shape, fileName, precision), pad: (input) => geometryRuntime.pad(input), pocket: (input) => geometryRuntime.pocket(input), revolution: (input) => geometryRuntime.revolution(input), mesh: (shape, precision) => geometryRuntime.mesh(shape, precision), subshapes: (shape, precision) => geometryRuntime.subshapes(shape, precision), topology: (shape, precision) => geometryRuntime.topology(shape, precision), release: (shape) => geometryRuntime.release(shape), dispose: () => geometryRuntime.dispose() },
|
||||
geometry: { capabilities: () => geometryRuntime.capabilities(), initialize: () => geometryRuntime.initialize(), createBox: (input) => geometryRuntime.createBox(input), createCylinder: (input) => geometryRuntime.createCylinder(input), createSphere: (input) => geometryRuntime.createSphere(input), createCone: (input) => geometryRuntime.createCone(input), applyPlacement: (input) => geometryRuntime.applyPlacement(input), union: (input) => geometryRuntime.union(input), cut: (input) => geometryRuntime.cut(input), intersection: (input) => geometryRuntime.intersection(input), fillet: (input) => geometryRuntime.fillet(input), chamfer: (input) => geometryRuntime.chamfer(input), exportStep: (shape, fileName) => geometryRuntime.exportStep(shape, fileName), exportStl: (shape, fileName, precision) => geometryRuntime.exportStl(shape, fileName, precision), pad: (input) => geometryRuntime.pad(input), pocket: (input) => geometryRuntime.pocket(input), revolution: (input) => geometryRuntime.revolution(input), mesh: (shape, precision) => geometryRuntime.mesh(shape, precision), subshapes: (shape, precision) => geometryRuntime.subshapes(shape, precision), topology: (shape, precision) => geometryRuntime.topology(shape, precision), getObjectShape: (objectId) => { const shape = featureShapes.get(objectId); return shape ? { ...shape } : null }, release: (shape) => geometryRuntime.release(shape), dispose: () => { clearFeatureShapes(); geometryRuntime.dispose() } },
|
||||
viewport: { createAdapter: () => new ThreeViewportAdapter() },
|
||||
getState, subscribe: (listener) => { listeners.add(listener); return () => { listeners.delete(listener) } }, notify,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user