|
|
|
|
@@ -1,6 +1,7 @@
|
|
|
|
|
import { workbenchDefinitions, type WorkbenchId } from '../freecadManifest'
|
|
|
|
|
import type {
|
|
|
|
|
BitBybitWebCadFacade,
|
|
|
|
|
AddDynamicPropertyInput,
|
|
|
|
|
CommandState,
|
|
|
|
|
DocumentSnapshot,
|
|
|
|
|
Diagnostic,
|
|
|
|
|
@@ -20,6 +21,8 @@ import type {
|
|
|
|
|
PlacementValue,
|
|
|
|
|
ReorderBodyFeatureInput,
|
|
|
|
|
RemoveObjectInput,
|
|
|
|
|
RemoveDynamicPropertyInput,
|
|
|
|
|
RenameDynamicPropertyInput,
|
|
|
|
|
SetPropertyInput,
|
|
|
|
|
SetExpressionInput,
|
|
|
|
|
RecomputeResult,
|
|
|
|
|
@@ -47,7 +50,7 @@ import { cloneObjectTopologySnapshot, migrateDocumentTopologyReferences, parseTo
|
|
|
|
|
import { createCamJob } from './cam'
|
|
|
|
|
import { PUMP_HOUSING_DEMO_TEMPLATE, type DocumentTemplate } from './documentTemplates'
|
|
|
|
|
import { assertPartDesignParameterSet, parameterValuesForObject, validatePartDesignParameterSet } from './partDesignParameters'
|
|
|
|
|
import { freecadPropertyRecomputeEffect, isFreecadPropertyDocumentModifiedSuppressed, isFreecadPropertyReadOnly } from './propertyStatus'
|
|
|
|
|
import { freecadPropertyRecomputeEffect, hasFreecadPropertyStatus, isFreecadPropertyDocumentModifiedSuppressed, isFreecadPropertyDynamicMutationLocked, isFreecadPropertyPartialDocumentTrigger, isFreecadPropertyReadOnly, withFreecadPropertyStatus } from './propertyStatus'
|
|
|
|
|
|
|
|
|
|
const typeIdForItem = (item: ModelTreeItem) => item.type === 'body' ? 'PartDesign::Body' : item.type === 'sketch' ? 'Sketcher::SketchObject' : item.id.startsWith('box') ? 'Part::Box' : item.id.startsWith('cylinder') ? 'Part::Cylinder' : item.id.startsWith('sphere') ? 'Part::Sphere' : item.id.startsWith('ellipsoid') ? 'Part::Ellipsoid' : item.id.startsWith('cone') ? 'Part::Cone' : item.id.startsWith('torus') ? 'Part::Torus' : item.id.startsWith('helix') ? 'Part::Helix' : item.id.startsWith('prism') ? 'Part::Prism' : item.id.startsWith('wedge') ? 'Part::Wedge' : item.id.startsWith('union') ? 'Part::Fuse' : item.id.startsWith('cut') ? 'Part::Cut' : item.id.startsWith('intersection') ? 'Part::Common' : item.id.startsWith('pad') ? 'PartDesign::Pad' : item.id.startsWith('pocket') ? 'PartDesign::Pocket' : item.id.startsWith('revolution') ? 'PartDesign::Revolution' : item.id.startsWith('groove') ? 'PartDesign::Groove' : item.id.startsWith('fillet') ? 'PartDesign::Fillet' : item.id.startsWith('chamfer') ? 'PartDesign::Chamfer' : item.id.startsWith('mirrored') ? 'PartDesign::Mirrored' : item.id.startsWith('multi-transform') ? 'PartDesign::MultiTransform' : item.id.startsWith('linear-pattern') ? 'PartDesign::LinearPattern' : item.id.startsWith('polar-pattern') ? 'PartDesign::PolarPattern' : item.id.startsWith('hole') ? 'PartDesign::Hole' : item.type === 'feature' ? 'PartDesign::Feature' : 'App::DocumentObjectGroup'
|
|
|
|
|
|
|
|
|
|
@@ -536,7 +539,7 @@ const validatePropertyValue = (document: DocumentSnapshot, property: ObjectPrope
|
|
|
|
|
if (property.readOnly || isFreecadPropertyReadOnly(property.nativeStatus)) throw new Error(`${property.label} is read-only.`)
|
|
|
|
|
if (property.type === 'App::PropertyBool' && typeof value !== 'boolean') throw new TypeError(`${property.label} requires a boolean value.`)
|
|
|
|
|
if ((property.type === 'App::PropertyString' || property.type === 'App::PropertyEnumeration' || property.type === 'App::PropertyColor') && typeof value !== 'string') throw new TypeError(`${property.label} requires a string value.`)
|
|
|
|
|
if ((property.type === 'App::PropertyLength' || property.type === 'App::PropertyDistance' || property.type === 'App::PropertyAngle' || property.type === 'App::PropertyQuantityConstraint' || property.type === 'App::PropertyPercent' || property.type === 'App::PropertyFloat' || property.type === 'App::PropertyInteger' || property.type === 'App::PropertyIntegerConstraint') && (typeof value !== 'number' || !Number.isFinite(value))) throw new TypeError(`${property.label} requires a finite numeric value.`)
|
|
|
|
|
if ((property.type === 'App::PropertyLength' || property.type === 'App::PropertyDistance' || property.type === 'App::PropertyAngle' || property.type === 'App::PropertyQuantityConstraint' || property.type === 'App::PropertyPercent' || property.type === 'App::PropertyFloat' || property.type === 'App::PropertyFloatConstraint' || property.type === 'App::PropertyPrecision' || property.type === 'App::PropertyInteger' || property.type === 'App::PropertyIntegerConstraint') && (typeof value !== 'number' || !Number.isFinite(value))) throw new TypeError(`${property.label} requires a finite numeric value.`)
|
|
|
|
|
if ((property.type === 'App::PropertyInteger' || property.type === 'App::PropertyIntegerConstraint') && !Number.isSafeInteger(value)) throw new TypeError(`${property.label} requires an integer value.`)
|
|
|
|
|
if (property.name === 'Occurrences' && ((value as number) < 2 || (value as number) > 100)) throw new RangeError('Occurrences must be between 2 and 100.')
|
|
|
|
|
if ((property.type === 'App::PropertyLength' || property.type === 'App::PropertyQuantityConstraint') && (value as number) < 0) throw new RangeError(`${property.label} cannot be negative.`)
|
|
|
|
|
@@ -576,7 +579,7 @@ const validatePropertyValue = (document: DocumentSnapshot, property: ObjectPrope
|
|
|
|
|
if (!/\.Map\.txt$/i.test(value.elementMapResource)) throw new TypeError(`${property.label} requires an ElementMap2 .Map.txt resource.`)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (property.type === 'App::PropertyLink') {
|
|
|
|
|
if (property.type === 'App::PropertyLink' || property.type === 'App::PropertyLinkHidden') {
|
|
|
|
|
if (property.name === 'Support' && value && typeof value === 'object' && !Array.isArray(value)) {
|
|
|
|
|
validateAttachmentSupport(value)
|
|
|
|
|
const knownIds = new Set(document.tree.flatMap((item) => [item.id, ...(item.children ?? [])]))
|
|
|
|
|
@@ -740,13 +743,14 @@ export function createWebCadFacade(options: WebCadFacadeOptions = {}): BitBybitW
|
|
|
|
|
const listeners = new Set<FacadeListener>()
|
|
|
|
|
const undoStack: FacadeState[] = []
|
|
|
|
|
const redoStack: FacadeState[] = []
|
|
|
|
|
const partialMutationWarnings = new Set<string>()
|
|
|
|
|
let requestSequence = 0
|
|
|
|
|
|
|
|
|
|
const emit = (event: FacadeEvent) => listeners.forEach((listener) => listener(event))
|
|
|
|
|
const emitState = () => emit({ type: 'state.changed', state: getState() })
|
|
|
|
|
const cloneSelection = (selection: SubshapeSelection | null | undefined): SubshapeSelection | null | undefined => selection ? { objectId: selection.objectId, ref: { ...selection.ref, candidates: selection.ref.candidates ? [...selection.ref.candidates] : undefined } } : selection
|
|
|
|
|
const getState = () => ({ ...state, selectedObjectIds: [...(state.selectedObjectIds ?? (state.selectedObjectId ? [state.selectedObjectId] : []))], selectedSubshape: cloneSelection(state.selectedSubshape), preselectedSubshape: cloneSelection(state.preselectedSubshape), diagnostics: state.diagnostics.map(cloneDiagnostic), document: cloneDocumentSnapshot(state.document), task: state.task ? { ...state.task, draft: Object.fromEntries(Object.entries(state.task.draft).map(([key, value]) => [key, key === 'transformations' ? clonePropertyValue(value as MultiTransformValue) : Array.isArray(value) ? [...value] : value])) } : null })
|
|
|
|
|
const commit = (next: FacadeState) => { undoStack.push(getState()); redoStack.length = 0; state = next; if (next.document.dirty) autosave.schedule(next.document); emitState() }
|
|
|
|
|
const commit = (next: FacadeState) => { undoStack.push(getState()); redoStack.length = 0; state = next; if (next.document.dirty && !next.document.partial) autosave.schedule(next.document); emitState() }
|
|
|
|
|
const notify = (message: string) => { state = { ...state, lastNotice: message }; emit({ type: 'notice', message }); emitState() }
|
|
|
|
|
const setActive = (id: WorkbenchId) => { state = { ...state, activeWorkbench: id }; emitState(); notify(`${id} workbench loaded`) }
|
|
|
|
|
const selectObjects = (objectIds: string[]) => {
|
|
|
|
|
@@ -1067,6 +1071,12 @@ export function createWebCadFacade(options: WebCadFacadeOptions = {}): BitBybitW
|
|
|
|
|
validatePropertyValue(state.document, sourceProperty, value)
|
|
|
|
|
if (Object.is(sourceProperty.value, value)) return
|
|
|
|
|
|
|
|
|
|
const beforeVersion = state.document.version
|
|
|
|
|
const transactionId = `txn-${++requestSequence}`
|
|
|
|
|
const partialDocument = state.document.partial === true
|
|
|
|
|
const partialTrigger = isFreecadPropertyPartialDocumentTrigger(sourceProperty.nativeStatus)
|
|
|
|
|
emit({ type: 'property.before-change', transactionId, documentId: state.document.id, documentVersion: beforeVersion, objectId, propertyName, partialDocument, partialTrigger })
|
|
|
|
|
|
|
|
|
|
const document = cloneDocumentSnapshot(state.document)
|
|
|
|
|
const object = document.objects[objectIndex]
|
|
|
|
|
object.properties[propertyIndex] = { ...object.properties[propertyIndex], value: clonePropertyValue(value), expression: undefined, expressionError: undefined }
|
|
|
|
|
@@ -1080,8 +1090,103 @@ export function createWebCadFacade(options: WebCadFacadeOptions = {}): BitBybitW
|
|
|
|
|
document.version += 1
|
|
|
|
|
document.dirty = isFreecadPropertyDocumentModifiedSuppressed(sourceProperty.nativeStatus) ? state.document.dirty : true
|
|
|
|
|
commit({ ...state, document })
|
|
|
|
|
emit({ type: 'property.changed', transactionId, documentId: document.id, documentVersion: document.version, objectId, propertyName, partialDocument, partialTrigger })
|
|
|
|
|
emit({ type: 'transaction.committed', transactionId, operation: 'property.set', documentId: document.id, beforeVersion, afterVersion: document.version })
|
|
|
|
|
if (partialDocument && !partialTrigger && !partialMutationWarnings.has(document.id)) {
|
|
|
|
|
partialMutationWarnings.add(document.id)
|
|
|
|
|
const diagnostic: Diagnostic = {
|
|
|
|
|
id: `diag-${++requestSequence}`,
|
|
|
|
|
source: 'command',
|
|
|
|
|
severity: 'warning',
|
|
|
|
|
code: 'PARTIAL_DOCUMENT_MUTATION_NOT_PERSISTED',
|
|
|
|
|
message: `Changes to partial loaded document will not be saved: ${objectId}.${propertyName}`,
|
|
|
|
|
objectId,
|
|
|
|
|
documentId: document.id,
|
|
|
|
|
documentVersion: document.version,
|
|
|
|
|
}
|
|
|
|
|
state = { ...state, diagnostics: [...state.diagnostics, diagnostic] }
|
|
|
|
|
emit({ type: 'diagnostic.added', diagnostic, context: { apiVersion: state.apiVersion, requestId: transactionId, documentId: document.id, documentVersion: document.version, workbench: state.activeWorkbench } })
|
|
|
|
|
}
|
|
|
|
|
notify(`${sourceProperty.label} updated`)
|
|
|
|
|
}
|
|
|
|
|
const addDynamicProperty = ({ objectId, property: inputProperty }: AddDynamicPropertyInput): ObjectPropertySnapshot => {
|
|
|
|
|
const objectIndex = state.document.objects.findIndex((object) => object.id === objectId)
|
|
|
|
|
if (objectIndex < 0) throw new Error(`Document object does not exist: ${objectId}`)
|
|
|
|
|
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(inputProperty.name)) throw new RangeError(`Invalid property name '${inputProperty.name}'.`)
|
|
|
|
|
if (state.document.objects[objectIndex].properties.some((property) => property.name === inputProperty.name)) throw new RangeError(`Property ${objectId}.${inputProperty.name} already exists.`)
|
|
|
|
|
|
|
|
|
|
const property: ObjectPropertySnapshot = {
|
|
|
|
|
...inputProperty,
|
|
|
|
|
value: clonePropertyValue(inputProperty.value),
|
|
|
|
|
options: inputProperty.options ? [...inputProperty.options] : undefined,
|
|
|
|
|
nativeStatus: withFreecadPropertyStatus(inputProperty.nativeStatus, 'PropDynamic'),
|
|
|
|
|
}
|
|
|
|
|
validatePropertyValue(state.document, { ...property, readOnly: false, nativeStatus: undefined }, property.value)
|
|
|
|
|
const beforeVersion = state.document.version
|
|
|
|
|
const transactionId = `txn-${++requestSequence}`
|
|
|
|
|
const document = cloneDocumentSnapshot(state.document)
|
|
|
|
|
document.objects[objectIndex].properties.push(property)
|
|
|
|
|
document.dependencies = collectDependencyEdges(document)
|
|
|
|
|
document.version += 1
|
|
|
|
|
document.dirty = true
|
|
|
|
|
commit({ ...state, document })
|
|
|
|
|
const partialDocument = document.partial === true
|
|
|
|
|
const partialTrigger = isFreecadPropertyPartialDocumentTrigger(property.nativeStatus)
|
|
|
|
|
emit({ type: 'property.added', transactionId, documentId: document.id, documentVersion: document.version, objectId, propertyName: property.name, partialDocument, partialTrigger })
|
|
|
|
|
emit({ type: 'transaction.committed', transactionId, operation: 'property.add', documentId: document.id, beforeVersion, afterVersion: document.version })
|
|
|
|
|
notify(`${property.label} property added`)
|
|
|
|
|
return { ...property, value: clonePropertyValue(property.value), options: property.options ? [...property.options] : undefined }
|
|
|
|
|
}
|
|
|
|
|
const removeDynamicProperty = ({ objectId, propertyName }: RemoveDynamicPropertyInput): boolean => {
|
|
|
|
|
const objectIndex = state.document.objects.findIndex((object) => object.id === objectId)
|
|
|
|
|
if (objectIndex < 0) throw new Error(`Document object does not exist: ${objectId}`)
|
|
|
|
|
const propertyIndex = state.document.objects[objectIndex].properties.findIndex((property) => property.name === propertyName)
|
|
|
|
|
if (propertyIndex < 0) return false
|
|
|
|
|
const property = state.document.objects[objectIndex].properties[propertyIndex]
|
|
|
|
|
if (!hasFreecadPropertyStatus(property.nativeStatus, 'PropDynamic') || isFreecadPropertyDynamicMutationLocked(property.nativeStatus)) return false
|
|
|
|
|
|
|
|
|
|
const beforeVersion = state.document.version
|
|
|
|
|
const transactionId = `txn-${++requestSequence}`
|
|
|
|
|
const document = cloneDocumentSnapshot(state.document)
|
|
|
|
|
document.objects[objectIndex].properties.splice(propertyIndex, 1)
|
|
|
|
|
document.dependencies = collectDependencyEdges(document)
|
|
|
|
|
document.version += 1
|
|
|
|
|
document.dirty = true
|
|
|
|
|
commit({ ...state, document })
|
|
|
|
|
const partialDocument = document.partial === true
|
|
|
|
|
const partialTrigger = isFreecadPropertyPartialDocumentTrigger(property.nativeStatus)
|
|
|
|
|
emit({ type: 'property.removed', transactionId, documentId: document.id, documentVersion: document.version, objectId, propertyName, partialDocument, partialTrigger })
|
|
|
|
|
emit({ type: 'transaction.committed', transactionId, operation: 'property.remove', documentId: document.id, beforeVersion, afterVersion: document.version })
|
|
|
|
|
notify(`${property.label} property removed`)
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
const renameDynamicProperty = ({ objectId, propertyName, newPropertyName }: RenameDynamicPropertyInput): boolean => {
|
|
|
|
|
const objectIndex = state.document.objects.findIndex((object) => object.id === objectId)
|
|
|
|
|
if (objectIndex < 0) throw new Error(`Document object does not exist: ${objectId}`)
|
|
|
|
|
const sourceObject = state.document.objects[objectIndex]
|
|
|
|
|
const propertyIndex = sourceObject.properties.findIndex((property) => property.name === propertyName)
|
|
|
|
|
if (propertyIndex < 0 || !hasFreecadPropertyStatus(sourceObject.properties[propertyIndex].nativeStatus, 'PropDynamic')) throw new Error(`Property container has no dynamic property '${propertyName}'.`)
|
|
|
|
|
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(newPropertyName)) throw new RangeError(`Invalid property name '${newPropertyName}'.`)
|
|
|
|
|
if (sourceObject.properties.some((property) => property.name === newPropertyName)) throw new RangeError(`Property ${objectId}.${newPropertyName} already exists.`)
|
|
|
|
|
const sourceProperty = sourceObject.properties[propertyIndex]
|
|
|
|
|
if (isFreecadPropertyDynamicMutationLocked(sourceProperty.nativeStatus)) throw new Error(`Property ${propertyName} is locked`)
|
|
|
|
|
if (propertyName === newPropertyName) return true
|
|
|
|
|
|
|
|
|
|
const beforeVersion = state.document.version
|
|
|
|
|
const transactionId = `txn-${++requestSequence}`
|
|
|
|
|
const document = cloneDocumentSnapshot(state.document)
|
|
|
|
|
document.objects[objectIndex].properties[propertyIndex].name = newPropertyName
|
|
|
|
|
document.dependencies = collectDependencyEdges(document)
|
|
|
|
|
document.version += 1
|
|
|
|
|
document.dirty = true
|
|
|
|
|
commit({ ...state, document })
|
|
|
|
|
const partialDocument = document.partial === true
|
|
|
|
|
const partialTrigger = isFreecadPropertyPartialDocumentTrigger(sourceProperty.nativeStatus)
|
|
|
|
|
emit({ type: 'property.renamed', transactionId, documentId: document.id, documentVersion: document.version, objectId, propertyName: newPropertyName, previousPropertyName: propertyName, partialDocument, partialTrigger })
|
|
|
|
|
emit({ type: 'transaction.committed', transactionId, operation: 'property.rename', documentId: document.id, beforeVersion, afterVersion: document.version })
|
|
|
|
|
notify(`${sourceProperty.label} property renamed`)
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
const reorderBodyFeature = ({ bodyId, objectId, beforeObjectId }: ReorderBodyFeatureInput) => {
|
|
|
|
|
const document = cloneDocumentSnapshot(state.document)
|
|
|
|
|
const body = document.tree.find((item) => item.id === bodyId && item.type === 'body')
|
|
|
|
|
@@ -1184,6 +1289,10 @@ export function createWebCadFacade(options: WebCadFacadeOptions = {}): BitBybitW
|
|
|
|
|
notify(`${sourceProperty.label} expression updated`)
|
|
|
|
|
}
|
|
|
|
|
const recomputeDocument = (): RecomputeResult => {
|
|
|
|
|
if (state.document.partial) {
|
|
|
|
|
notify(`Partial document ${state.document.label} must be fully loaded before recomputation`)
|
|
|
|
|
return { affected: [], order: [], levels: [], cycles: [], generation: state.document.recompute?.generation ?? 0, status: 'completed', errors: [] }
|
|
|
|
|
}
|
|
|
|
|
const document = cloneDocumentSnapshot(state.document)
|
|
|
|
|
const graph = new DependencyGraph(document.dependencies ?? [], document.objects.map((object) => object.id))
|
|
|
|
|
const recompute = document.recompute ?? createRecomputeSnapshot(document.objects.map((object) => object.id))
|
|
|
|
|
@@ -1221,12 +1330,23 @@ export function createWebCadFacade(options: WebCadFacadeOptions = {}): BitBybitW
|
|
|
|
|
state = { ...state, document, diagnostics }
|
|
|
|
|
const context: FacadeRequestContext = { apiVersion: state.apiVersion, requestId: `recompute-${generation}`, documentId: document.id, documentVersion: document.version, workbench: state.activeWorkbench }
|
|
|
|
|
for (const diagnostic of nextDiagnostics) emit({ type: 'diagnostic.added', diagnostic, context })
|
|
|
|
|
if (document.dirty) autosave.schedule(document)
|
|
|
|
|
if (document.dirty && !document.partial) autosave.schedule(document)
|
|
|
|
|
emitState()
|
|
|
|
|
return { ...plan, generation, status: nextRecompute.status, errors }
|
|
|
|
|
}
|
|
|
|
|
const recomputeDocumentAsync = async (options: RecomputeExecutionOptions = {}) => {
|
|
|
|
|
const source = cloneDocumentSnapshot(state.document)
|
|
|
|
|
if (source.partial) {
|
|
|
|
|
notify(`Partial document ${source.label} must be fully loaded before recomputation`)
|
|
|
|
|
const recompute = source.recompute ?? createRecomputeSnapshot(source.objects.map((object) => object.id))
|
|
|
|
|
return {
|
|
|
|
|
generation: recompute.generation,
|
|
|
|
|
documentVersion: source.version,
|
|
|
|
|
status: 'completed' as const,
|
|
|
|
|
affected: [], order: [], levels: [], completed: [], suppressed: [], failed: [], skipped: [],
|
|
|
|
|
dirtyObjects: [...recompute.dirtyObjects], objectStates: { ...recompute.objectStates }, objectUpdates: [], errors: [],
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const result = await recomputeCoordinator.run(source, options)
|
|
|
|
|
if ((result.status !== 'completed' && result.status !== 'failed') || state.document.id !== source.id || state.document.version !== source.version) return result
|
|
|
|
|
|
|
|
|
|
@@ -1293,7 +1413,7 @@ export function createWebCadFacade(options: WebCadFacadeOptions = {}): BitBybitW
|
|
|
|
|
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 })
|
|
|
|
|
if (document.dirty) autosave.schedule(document)
|
|
|
|
|
if (document.dirty && !document.partial) autosave.schedule(document)
|
|
|
|
|
emitState()
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
@@ -1492,7 +1612,7 @@ export function createWebCadFacade(options: WebCadFacadeOptions = {}): BitBybitW
|
|
|
|
|
|
|
|
|
|
const facade: BitBybitWebCadFacade = {
|
|
|
|
|
runtime,
|
|
|
|
|
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: nextBlankDocument(label), selectedObjectId: '', selectedObjectIds: [] }); return getState().document }, load: loadDocument, markDirty: () => { commit({ ...state, document: { ...state.document, dirty: true } }) }, setProperty, reorderBodyFeature, removeObject, 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, projectGeometry: projectSketch, carbonCopy: carbonCopySketch, 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, value: clonePropertyValue(property.value), 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: nextBlankDocument(label), selectedObjectId: '', selectedObjectIds: [] }); return getState().document }, load: loadDocument, markDirty: () => { commit({ ...state, document: { ...state.document, dirty: true } }) }, setProperty, addDynamicProperty, removeDynamicProperty, renameDynamicProperty, reorderBodyFeature, removeObject, 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, projectGeometry: projectSketch, carbonCopy: carbonCopySketch, 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), state.selectedSubshape), list: (workbench) => workbenchDefinitions[workbench].groups.flatMap((group) => group.commands), execute } },
|
|
|
|
|
selection: { getObjectId: () => state.selectedObjectId, getObjectIds: () => [...(state.selectedObjectIds ?? (state.selectedObjectId ? [state.selectedObjectId] : []))], getSubshape: () => cloneSelection(state.selectedSubshape) ?? null, getPreselection: () => cloneSelection(state.preselectedSubshape) ?? null, select, selectObjects, selectSubshape, preselectSubshape, clear: () => select('') },
|
|
|
|
|
|