Files
modeling-app/src/machines/modelingMachine.ts
Andrew Varga 9248b2e42d #5905 Cleanup console warnings (#5908)
* Fix ToolBar WebkitAppRegion warning

* make intersectionPlane non-nullable, avoid trying to create it multiple times to get rid of warning

* remove derived scene from sceneEntities

* intersectionPlane is now always non-null, make it readonly too

* sceneInfra small cleanups

* A snapshot a day keeps the bugs away! 📷🐛

* A snapshot a day keeps the bugs away! 📷🐛

* Clean up snapshots

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Pierre Jacquier <pierre@zoo.dev>
2025-03-21 00:55:20 +00:00

4210 lines
138 KiB
TypeScript

import {
Expr,
PathToNode,
VariableDeclaration,
VariableDeclarator,
parse,
recast,
resultIsOk,
sketchFromKclValue,
} from 'lang/wasm'
import {
Axis,
DefaultPlaneSelection,
Selections,
Selection,
updateSelections,
} from 'lib/selections'
import { assign, fromPromise, setup } from 'xstate'
import { SidebarType } from 'components/ModelingSidebar/ModelingPanes'
import { isNodeSafeToReplacePath, stringifyPathToNode } from 'lang/queryAst'
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
import {
kclManager,
sceneInfra,
sceneEntitiesManager,
engineCommandManager,
editorManager,
codeManager,
} from 'lib/singletons'
import {
horzVertInfo,
applyConstraintHorzVert,
} from 'components/Toolbar/HorzVert'
import {
applyConstraintHorzVertAlign,
horzVertDistanceInfo,
} from 'components/Toolbar/SetHorzVertDistance'
import { angleBetweenInfo } from 'components/Toolbar/SetAngleBetween'
import { angleLengthInfo } from 'components/Toolbar/setAngleLength'
import {
applyConstraintEqualLength,
setEqualLengthInfo,
} from 'components/Toolbar/EqualLength'
import { revolveSketch } from 'lang/modifyAst/addRevolve'
import {
addHelix,
addOffsetPlane,
addShell,
addSweep,
createIdentifier,
createLiteral,
extrudeSketch,
insertNamedConstant,
loftSketches,
} from 'lang/modifyAst'
import {
applyEdgeTreatmentToSelection,
ChamferParameters,
EdgeTreatmentType,
FilletParameters,
getPathToExtrudeForSegmentSelection,
mutateAstWithTagForSketchSegment,
} from 'lang/modifyAst/addEdgeTreatment'
import { getNodeFromPath } from '../lang/queryAst'
import {
applyConstraintEqualAngle,
equalAngleInfo,
} from 'components/Toolbar/EqualAngle'
import {
applyRemoveConstrainingValues,
removeConstrainingValuesInfo,
} from 'components/Toolbar/RemoveConstrainingValues'
import { intersectInfo } from 'components/Toolbar/Intersect'
import {
absDistanceInfo,
applyConstraintAxisAlign,
} from 'components/Toolbar/SetAbsDistance'
import { ModelingCommandSchema } from 'lib/commandBarConfigs/modelingCommandConfig'
import { err, reportRejection, trap } from 'lib/trap'
import { DefaultPlaneStr } from 'lib/planes'
import { isArray, uuidv4 } from 'lib/utils'
import { Coords2d } from 'lang/std/sketch'
import { deleteSegment } from 'clientSideScene/ClientSideSceneComp'
import toast from 'react-hot-toast'
import { ToolbarModeName } from 'lib/toolbar'
import { orthoScale, quaternionFromUpNForward } from 'clientSideScene/helpers'
import { Mesh, Vector2, Vector3 } from 'three'
import { MachineManager } from 'components/MachineManagerProvider'
import { KclCommandValue } from 'lib/commandTypes'
import { ModelingMachineContext } from 'components/ModelingMachineProvider'
import {
deleteSelectionPromise,
deletionErrorMessage,
} from 'lang/modifyAst/deleteSelection'
import { getPathsFromPlaneArtifact } from 'lang/std/artifactGraph'
import { createProfileStartHandle } from 'clientSideScene/segments'
import { DRAFT_POINT } from 'clientSideScene/sceneInfra'
import { setAppearance } from 'lang/modifyAst/setAppearance'
import { DRAFT_DASHED_LINE } from 'clientSideScene/sceneEntities'
import { Node } from '@rust/kcl-lib/bindings/Node'
import { updateModelingState } from 'lang/modelingWorkflows'
export const MODELING_PERSIST_KEY = 'MODELING_PERSIST_KEY'
export type SetSelections =
| {
selectionType: 'singleCodeCursor'
selection?: Selection
scrollIntoView?: boolean
}
| {
selectionType: 'axisSelection'
selection: Axis
}
| {
selectionType: 'defaultPlaneSelection'
selection: DefaultPlaneSelection
}
| {
selectionType: 'completeSelection'
selection: Selections
updatedSketchEntryNodePath?: PathToNode
updatedSketchNodePaths?: PathToNode[]
updatedPlaneNodePath?: PathToNode
}
| {
selectionType: 'mirrorCodeMirrorSelections'
selection: Selections
}
export type MouseState =
| {
type: 'idle'
}
| {
type: 'isHovering'
on: any
}
| {
type: 'isDragging'
on: any
}
| {
type: 'timeoutEnd'
pathToNodeString: string
}
export interface SketchDetails {
sketchEntryNodePath: PathToNode
sketchNodePaths: PathToNode[]
planeNodePath: PathToNode
zAxis: [number, number, number]
yAxis: [number, number, number]
origin: [number, number, number]
// face id or plane id, both are strings
animateTargetId?: string
// this is the expression that was added when as sketch tool was used but not completed
// i.e first click for the center of the circle, but not the second click for the radius
// we added a circle to editor, but they bailed out early so we should remove it, set to -1 to ignore
expressionIndexToDelete?: number
}
export interface SketchDetailsUpdate {
updatedEntryNodePath: PathToNode
updatedSketchNodePaths: PathToNode[]
updatedPlaneNodePath?: PathToNode
// see comment in SketchDetails
expressionIndexToDelete: number
}
export interface SegmentOverlay {
windowCoords: Coords2d
angle: number
group: any
pathToNode: PathToNode
visible: boolean
hasThreeDotMenu: boolean
filterValue?: string
}
export interface SegmentOverlays {
[pathToNodeString: string]: SegmentOverlay[]
}
export interface EdgeCutInfo {
type: 'edgeCut'
tagName: string
subType: 'base' | 'opposite' | 'adjacent'
}
export interface CapInfo {
type: 'cap'
subType: 'start' | 'end'
}
export type ExtrudeFacePlane = {
type: 'extrudeFace'
position: [number, number, number]
sketchPathToNode: PathToNode
extrudePathToNode: PathToNode
faceInfo:
| {
type: 'wall'
}
| CapInfo
| EdgeCutInfo
faceId: string
zAxis: [number, number, number]
yAxis: [number, number, number]
}
export type DefaultPlane = {
type: 'defaultPlane'
plane: DefaultPlaneStr
planeId: string
zAxis: [number, number, number]
yAxis: [number, number, number]
}
export type OffsetPlane = {
type: 'offsetPlane'
position: [number, number, number]
planeId: string
pathToNode: PathToNode
zAxis: [number, number, number]
yAxis: [number, number, number]
}
export type SegmentOverlayPayload =
| {
type: 'set-one'
pathToNodeString: string
seg: SegmentOverlay[]
}
| {
type: 'delete-one'
pathToNodeString: string
}
| { type: 'clear' }
| {
type: 'add-many'
overlays: SegmentOverlays
}
export interface Store {
videoElement?: HTMLVideoElement
openPanes: SidebarType[]
}
export type SketchTool =
| 'line'
| 'tangentialArc'
| 'rectangle'
| 'center rectangle'
| 'circle'
| 'circleThreePoint'
| 'arc'
| 'arcThreePoint'
| 'none'
export type ModelingMachineEvent =
| {
type: 'Enter sketch'
data?: {
forceNewSketch?: boolean
}
}
| { type: 'Sketch On Face' }
| {
type: 'Select default plane'
data: DefaultPlane | ExtrudeFacePlane | OffsetPlane
}
| {
type: 'Set selection'
data: SetSelections
}
| {
type: 'Delete selection'
}
| { type: 'Sketch no face' }
| { type: 'Toggle gui mode' }
| { type: 'Cancel'; cleanup?: () => void }
| { type: 'CancelSketch' }
| {
type: 'Add start point' | 'Continue existing profile'
data: {
sketchNodePaths: PathToNode[]
sketchEntryNodePath: PathToNode
}
}
| { type: 'Close sketch' }
| { type: 'Make segment horizontal' }
| { type: 'Make segment vertical' }
| { type: 'Constrain horizontal distance' }
| { type: 'Constrain ABS X' }
| { type: 'Constrain ABS Y' }
| { type: 'Constrain vertical distance' }
| { type: 'Constrain angle' }
| { type: 'Constrain perpendicular distance' }
| { type: 'Constrain horizontally align' }
| { type: 'Constrain vertically align' }
| { type: 'Constrain snap to X' }
| { type: 'Constrain snap to Y' }
| {
type: 'Constrain length'
data: ModelingCommandSchema['Constrain length']
}
| { type: 'Constrain equal length' }
| { type: 'Constrain parallel' }
| { type: 'Constrain remove constraints'; data?: PathToNode }
| { type: 'Re-execute' }
| {
type: 'event.parameter.create'
data: ModelingCommandSchema['event.parameter.create']
}
| {
type: 'event.parameter.edit'
data: ModelingCommandSchema['event.parameter.edit']
}
| { type: 'Export'; data: ModelingCommandSchema['Export'] }
| { type: 'Make'; data: ModelingCommandSchema['Make'] }
| { type: 'Extrude'; data?: ModelingCommandSchema['Extrude'] }
| { type: 'Sweep'; data?: ModelingCommandSchema['Sweep'] }
| { type: 'Loft'; data?: ModelingCommandSchema['Loft'] }
| { type: 'Shell'; data?: ModelingCommandSchema['Shell'] }
| { type: 'Revolve'; data?: ModelingCommandSchema['Revolve'] }
| { type: 'Fillet'; data?: ModelingCommandSchema['Fillet'] }
| { type: 'Chamfer'; data?: ModelingCommandSchema['Chamfer'] }
| { type: 'Offset plane'; data: ModelingCommandSchema['Offset plane'] }
| { type: 'Helix'; data: ModelingCommandSchema['Helix'] }
| { type: 'Text-to-CAD'; data: ModelingCommandSchema['Text-to-CAD'] }
| { type: 'Prompt-to-edit'; data: ModelingCommandSchema['Prompt-to-edit'] }
| {
type: 'Delete selection'
data: ModelingCommandSchema['Delete selection']
}
| { type: 'Appearance'; data: ModelingCommandSchema['Appearance'] }
| {
type:
| 'Add circle origin'
| 'Add circle center'
| 'Add center rectangle origin'
| 'click in scene'
| 'Add first point'
data: [x: number, y: number]
}
| {
type: 'Add second point'
data: {
p1: [x: number, y: number]
p2: [x: number, y: number]
}
}
| {
type: 'xstate.done.actor.animate-to-face'
output: SketchDetails
}
| { type: 'xstate.done.actor.animate-to-sketch'; output: SketchDetails }
| { type: `xstate.done.actor.do-constrain${string}`; output: SetSelections }
| {
type:
| 'xstate.done.actor.set-up-draft-circle'
| 'xstate.done.actor.set-up-draft-rectangle'
| 'xstate.done.actor.set-up-draft-center-rectangle'
| 'xstate.done.actor.set-up-draft-circle-three-point'
| 'xstate.done.actor.set-up-draft-arc'
| 'xstate.done.actor.set-up-draft-arc-three-point'
| 'xstate.done.actor.split-sketch-pipe-if-needed'
| 'xstate.done.actor.actor-circle-three-point'
output: SketchDetailsUpdate
}
| {
type: 'xstate.done.actor.setup-client-side-sketch-segments9'
}
| { type: 'Set mouse state'; data: MouseState }
| { type: 'Set context'; data: Partial<Store> }
| {
type: 'Set Segment Overlays'
data: SegmentOverlayPayload
}
| {
type: 'Center camera on selection'
}
| {
type: 'Delete segment'
data: PathToNode
}
| {
type: 'code edit during sketch'
}
| {
type: 'Constrain with named value'
data: ModelingCommandSchema['Constrain with named value']
}
| {
type: 'change tool'
data: {
tool: SketchTool
}
}
| { type: 'Finish rectangle' }
| { type: 'Finish center rectangle' }
| { type: 'Finish circle' }
| { type: 'Finish circle three point' }
| { type: 'Finish arc' }
| { type: 'Artifact graph populated' }
| { type: 'Artifact graph emptied' }
export type MoveDesc = { line: number; snippet: string }
export const PERSIST_MODELING_CONTEXT = 'persistModelingContext'
interface PersistedModelingContext {
openPanes: Store['openPanes']
}
type PersistedKeys = keyof PersistedModelingContext
export const PersistedValues: PersistedKeys[] = ['openPanes']
export const getPersistedContext = (): Partial<PersistedModelingContext> => {
const c = (typeof window !== 'undefined' &&
JSON.parse(localStorage.getItem(PERSIST_MODELING_CONTEXT) || '{}')) || {
openPanes: ['code'],
}
return c
}
export interface ModelingMachineContext {
currentMode: ToolbarModeName
currentTool: SketchTool
machineManager: MachineManager
selection: string[]
selectionRanges: Selections
sketchDetails: SketchDetails | null
sketchPlaneId: string
sketchEnginePathId: string
moveDescs: MoveDesc[]
mouseState: MouseState
segmentOverlays: SegmentOverlays
segmentHoverMap: { [pathToNodeString: string]: number }
store: Store
}
export const modelingMachineDefaultContext: ModelingMachineContext = {
currentMode: 'modeling',
currentTool: 'none',
machineManager: {
machines: [],
machineApiIp: null,
currentMachine: null,
setCurrentMachine: () => {},
noMachinesReason: () => undefined,
},
selection: [],
selectionRanges: {
otherSelections: [],
graphSelections: [],
},
sketchDetails: {
sketchEntryNodePath: [],
planeNodePath: [],
sketchNodePaths: [],
zAxis: [0, 0, 1],
yAxis: [0, 1, 0],
origin: [0, 0, 0],
},
sketchPlaneId: '',
sketchEnginePathId: '',
moveDescs: [],
mouseState: { type: 'idle' },
segmentOverlays: {},
segmentHoverMap: {},
store: {
openPanes: getPersistedContext().openPanes || ['code'],
},
}
export const modelingMachine = setup({
types: {
context: {} as ModelingMachineContext,
events: {} as ModelingMachineEvent,
input: {} as ModelingMachineContext,
},
guards: {
'Selection is on face': () => false,
'Has exportable geometry': () => false,
'has valid selection for deletion': () => false,
'is editing existing sketch': ({ context: { sketchDetails } }) =>
isEditingExistingSketch({ sketchDetails }),
'Can make selection horizontal': ({ context: { selectionRanges } }) => {
const info = horzVertInfo(selectionRanges, 'horizontal')
if (trap(info)) return false
return info.enabled
},
'Can make selection vertical': ({ context: { selectionRanges } }) => {
const info = horzVertInfo(selectionRanges, 'vertical')
if (trap(info)) return false
return info.enabled
},
'Can constrain horizontal distance': ({ context: { selectionRanges } }) => {
const info = horzVertDistanceInfo({
selectionRanges: selectionRanges,
constraint: 'setHorzDistance',
})
if (trap(info)) return false
return info.enabled
},
'Can constrain vertical distance': ({ context: { selectionRanges } }) => {
const info = horzVertDistanceInfo({
selectionRanges: selectionRanges,
constraint: 'setVertDistance',
})
if (trap(info)) return false
return info.enabled
},
'Can constrain ABS X': ({ context: { selectionRanges } }) => {
const info = absDistanceInfo({
selectionRanges,
constraint: 'xAbs',
})
if (trap(info)) return false
return info.enabled
},
'Can constrain ABS Y': ({ context: { selectionRanges } }) => {
const info = absDistanceInfo({
selectionRanges,
constraint: 'yAbs',
})
if (trap(info)) return false
return info.enabled
},
'Can constrain angle': ({ context: { selectionRanges } }) => {
const angleBetween = angleBetweenInfo({
selectionRanges,
})
if (trap(angleBetween)) return false
const angleLength = angleLengthInfo({
selectionRanges,
angleOrLength: 'setAngle',
})
if (trap(angleLength)) return false
return angleBetween.enabled || angleLength.enabled
},
'Can constrain length': ({ context: { selectionRanges } }) => {
const angleLength = angleLengthInfo({
selectionRanges,
})
if (trap(angleLength)) return false
return angleLength.enabled
},
'Can constrain perpendicular distance': ({
context: { selectionRanges },
}) => {
const info = intersectInfo({ selectionRanges })
if (trap(info)) return false
return info.enabled
},
'Can constrain horizontally align': ({ context: { selectionRanges } }) => {
const info = horzVertDistanceInfo({
selectionRanges: selectionRanges,
constraint: 'setHorzDistance',
})
if (trap(info)) return false
return info.enabled
},
'Can constrain vertically align': ({ context: { selectionRanges } }) => {
const info = horzVertDistanceInfo({
selectionRanges: selectionRanges,
constraint: 'setHorzDistance',
})
if (trap(info)) return false
return info.enabled
},
'Can constrain snap to X': ({ context: { selectionRanges } }) => {
const info = absDistanceInfo({
selectionRanges,
constraint: 'snapToXAxis',
})
if (trap(info)) return false
return info.enabled
},
'Can constrain snap to Y': ({ context: { selectionRanges } }) => {
const info = absDistanceInfo({
selectionRanges,
constraint: 'snapToYAxis',
})
if (trap(info)) return false
return info.enabled
},
'Can constrain equal length': ({ context: { selectionRanges } }) => {
const info = setEqualLengthInfo({
selectionRanges,
})
if (trap(info)) return false
return info.enabled
},
'Can canstrain parallel': ({ context: { selectionRanges } }) => {
const info = equalAngleInfo({
selectionRanges,
})
if (err(info)) return false
return info.enabled
},
'Can constrain remove constraints': ({
context: { selectionRanges },
event,
}) => {
if (event.type !== 'Constrain remove constraints') return false
const info = removeConstrainingValuesInfo({
selectionRanges,
pathToNodes: event.data && [event.data],
})
if (trap(info)) return false
return info.enabled
},
'Can convert to named value': ({ event }) => {
if (event.type !== 'Constrain with named value') return false
if (!event.data) return false
const ast = parse(recast(kclManager.ast))
if (err(ast) || !ast.program || ast.errors.length > 0) return false
const isSafeRetVal = isNodeSafeToReplacePath(
ast.program,
event.data.currentValue.pathToNode
)
if (err(isSafeRetVal)) return false
return isSafeRetVal.isSafe
},
'next is tangential arc': ({ context: { sketchDetails, currentTool } }) =>
currentTool === 'tangentialArc' &&
isEditingExistingSketch({ sketchDetails }),
'next is rectangle': ({ context: { currentTool } }) =>
currentTool === 'rectangle',
'next is center rectangle': ({ context: { currentTool } }) =>
currentTool === 'center rectangle',
'next is circle': ({ context: { currentTool } }) =>
currentTool === 'circle',
'next is circle three point': ({ context: { currentTool } }) =>
currentTool === 'circleThreePoint',
'next is circle three point neo': ({ context: { currentTool } }) =>
currentTool === 'circleThreePoint',
'next is line': ({ context }) => context.currentTool === 'line',
'next is none': ({ context }) => context.currentTool === 'none',
'next is arc': ({ context }) => context.currentTool === 'arc',
'next is arc three point': ({ context }) =>
context.currentTool === 'arcThreePoint',
},
// end guards
actions: {
toastError: ({ event }) => {
if ('output' in event && event.output instanceof Error) {
toast.error(event.output.message)
} else if ('data' in event && event.data instanceof Error) {
toast.error(event.data.message)
}
},
'assign tool in context': assign({
currentTool: ({ event }) =>
'data' in event && event.data && 'tool' in event.data
? event.data.tool
: 'none',
}),
'reset selections': assign({
selectionRanges: { graphSelections: [], otherSelections: [] },
}),
'enter sketching mode': assign({ currentMode: 'sketching' }),
'enter modeling mode': assign({ currentMode: 'modeling' }),
'set sketchMetadata from pathToNode': assign(
({ context: { sketchDetails } }) => {
if (!sketchDetails?.sketchEntryNodePath || !sketchDetails) return {}
return {
sketchDetails: {
...sketchDetails,
sketchEntryNodePath: sketchDetails.sketchEntryNodePath,
},
}
}
),
'hide default planes': () => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
kclManager.hidePlanes()
},
'reset sketch metadata': assign({
sketchDetails: null,
sketchEnginePathId: '',
sketchPlaneId: '',
}),
'reset camera position': () => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
center: { x: 0, y: 0, z: 0 },
vantage: { x: 0, y: -1250, z: 580 },
up: { x: 0, y: 0, z: 1 },
},
})
},
'set new sketch metadata': assign(({ event }) => {
if (
event.type !== 'xstate.done.actor.animate-to-sketch' &&
event.type !== 'xstate.done.actor.animate-to-face'
)
return {}
return {
sketchDetails: event.output,
}
}),
'AST revolve': ({ context: { store }, event }) => {
if (event.type !== 'Revolve') return
;(async () => {
if (!event.data) return
const { selection, angle, axis, edge, axisOrEdge } = event.data
let ast = kclManager.ast
if (
'variableName' in angle &&
angle.variableName &&
angle.insertIndex !== undefined
) {
const newBody = [...ast.body]
newBody.splice(angle.insertIndex, 0, angle.variableDeclarationAst)
ast.body = newBody
}
// This is the selection of the sketch that will be revolved
const pathToNode = getNodePathFromSourceRange(
ast,
selection.graphSelections[0]?.codeRef.range
)
const revolveSketchRes = revolveSketch(
ast,
pathToNode,
'variableName' in angle
? angle.variableIdentifierAst
: angle.valueAst,
axisOrEdge,
axis,
edge,
engineCommandManager.artifactGraph,
selection.graphSelections[0]?.artifact
)
if (trap(revolveSketchRes)) return
const { modifiedAst, pathToRevolveArg } = revolveSketchRes
const updatedAst = await kclManager.updateAst(modifiedAst, true, {
focusPath: [pathToRevolveArg],
zoomToFit: true,
zoomOnRangeAndType: {
range: selection.graphSelections[0]?.codeRef.range,
type: 'path',
},
})
await codeManager.updateEditorWithAstAndWriteToFile(updatedAst.newAst)
if (updatedAst?.selections) {
editorManager.selectRange(updatedAst?.selections)
}
})().catch(reportRejection)
},
'set selection filter to curves only': () => {
;(async () => {
await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'set_selection_filter',
filter: ['curve'],
},
})
})().catch(reportRejection)
},
'setup client side sketch segments': ({
context: { sketchDetails, selectionRanges },
}) => {
if (!sketchDetails) return
;(async () => {
if (Object.keys(sceneEntitiesManager.activeSegments).length > 0) {
sceneEntitiesManager.tearDownSketch({ removeAxis: false })
}
sceneInfra.resetMouseListeners()
if (!sketchDetails?.sketchEntryNodePath) return
await sceneEntitiesManager.setupSketch({
sketchEntryNodePath: sketchDetails?.sketchEntryNodePath || [],
sketchNodePaths: sketchDetails.sketchNodePaths,
forward: sketchDetails.zAxis,
up: sketchDetails.yAxis,
position: sketchDetails.origin,
maybeModdedAst: kclManager.ast,
selectionRanges,
})
sceneInfra.resetMouseListeners()
sceneEntitiesManager.setupSketchIdleCallbacks({
sketchEntryNodePath: sketchDetails?.sketchEntryNodePath || [],
forward: sketchDetails.zAxis,
up: sketchDetails.yAxis,
position: sketchDetails.origin,
sketchNodePaths: sketchDetails.sketchNodePaths,
planeNodePath: sketchDetails.planeNodePath,
})
})().catch(reportRejection)
},
'tear down client sketch': () => {
if (sceneEntitiesManager.activeSegments) {
sceneEntitiesManager.tearDownSketch({ removeAxis: false })
}
},
'remove sketch grid': () => sceneEntitiesManager.removeSketchGrid(),
'set up draft line': assign(({ context: { sketchDetails }, event }) => {
if (!sketchDetails) return {}
if (event.type !== 'Add start point') return {}
// eslint-disable-next-line @typescript-eslint/no-floating-promises
sceneEntitiesManager
.setupDraftSegment(
event.data.sketchEntryNodePath || sketchDetails.sketchEntryNodePath,
event.data.sketchNodePaths || sketchDetails.sketchNodePaths,
sketchDetails.planeNodePath,
sketchDetails.zAxis,
sketchDetails.yAxis,
sketchDetails.origin,
'line'
)
.then(() => {
return codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
})
return {
sketchDetails: {
...sketchDetails,
sketchEntryNodePath: event.data.sketchEntryNodePath,
sketchNodePaths: event.data.sketchNodePaths,
},
}
}),
'set up draft arc': assign(({ context: { sketchDetails }, event }) => {
if (!sketchDetails) return {}
if (event.type !== 'Continue existing profile') return {}
// eslint-disable-next-line @typescript-eslint/no-floating-promises
sceneEntitiesManager
.setupDraftSegment(
event.data.sketchEntryNodePath || sketchDetails.sketchEntryNodePath,
event.data.sketchNodePaths || sketchDetails.sketchNodePaths,
sketchDetails.planeNodePath,
sketchDetails.zAxis,
sketchDetails.yAxis,
sketchDetails.origin,
'tangentialArcTo'
)
.then(() => {
return codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
})
return {
sketchDetails: {
...sketchDetails,
sketchEntryNodePath: event.data.sketchEntryNodePath,
sketchNodePaths: event.data.sketchNodePaths,
},
}
}),
'listen for rectangle origin': ({ context: { sketchDetails } }) => {
if (!sketchDetails) return
const quaternion = quaternionFromUpNForward(
new Vector3(...sketchDetails.yAxis),
new Vector3(...sketchDetails.zAxis)
)
// Position the click raycast plane
sceneEntitiesManager.intersectionPlane.setRotationFromQuaternion(
quaternion
)
sceneEntitiesManager.intersectionPlane.position.copy(
new Vector3(...(sketchDetails?.origin || [0, 0, 0]))
)
sceneInfra.setCallbacks({
onClick: (args) => {
if (!args) return
if (args.mouseEvent.which !== 1) return
const twoD = args.intersectionPoint?.twoD
if (twoD) {
sceneInfra.modelingSend({
type: 'click in scene',
data: sceneEntitiesManager.getSnappedDragPoint({
intersection2d: twoD,
intersects: args.intersects,
}).snappedPoint,
})
} else {
console.error('No intersection point found')
}
},
})
},
'listen for center rectangle origin': ({ context: { sketchDetails } }) => {
if (!sketchDetails) return
const quaternion = quaternionFromUpNForward(
new Vector3(...sketchDetails.yAxis),
new Vector3(...sketchDetails.zAxis)
)
// Position the click raycast plane
sceneEntitiesManager.intersectionPlane.setRotationFromQuaternion(
quaternion
)
sceneEntitiesManager.intersectionPlane.position.copy(
new Vector3(...(sketchDetails?.origin || [0, 0, 0]))
)
sceneInfra.setCallbacks({
onClick: (args) => {
if (!args) return
if (args.mouseEvent.which !== 1) return
const twoD = args.intersectionPoint?.twoD
if (twoD) {
sceneInfra.modelingSend({
type: 'Add center rectangle origin',
data: [twoD.x, twoD.y],
})
} else {
console.error('No intersection point found')
}
},
})
},
'listen for circle origin': ({ context: { sketchDetails } }) => {
if (!sketchDetails) return
const quaternion = quaternionFromUpNForward(
new Vector3(...sketchDetails.yAxis),
new Vector3(...sketchDetails.zAxis)
)
// Position the click raycast plane
sceneEntitiesManager.intersectionPlane.setRotationFromQuaternion(
quaternion
)
sceneEntitiesManager.intersectionPlane.position.copy(
new Vector3(...(sketchDetails?.origin || [0, 0, 0]))
)
sceneInfra.setCallbacks({
onClick: (args) => {
if (!args) return
if (args.mouseEvent.which !== 1) return
const { intersectionPoint } = args
if (!intersectionPoint?.twoD) return
const twoD = args.intersectionPoint?.twoD
if (twoD) {
sceneInfra.modelingSend({
type: 'Add circle origin',
data: [twoD.x, twoD.y],
})
} else {
console.error('No intersection point found')
}
},
})
},
'listen for circle first point': ({ context: { sketchDetails } }) => {
if (!sketchDetails) return
const quaternion = quaternionFromUpNForward(
new Vector3(...sketchDetails.yAxis),
new Vector3(...sketchDetails.zAxis)
)
// Position the click raycast plane
sceneEntitiesManager.intersectionPlane.setRotationFromQuaternion(
quaternion
)
sceneEntitiesManager.intersectionPlane.position.copy(
new Vector3(...(sketchDetails?.origin || [0, 0, 0]))
)
sceneInfra.setCallbacks({
onClick: (args) => {
if (!args) return
if (args.mouseEvent.which !== 1) return
const { intersectionPoint } = args
if (!intersectionPoint?.twoD) return
const twoD = args.intersectionPoint?.twoD
if (twoD) {
sceneInfra.modelingSend({
type: 'Add first point',
data: [twoD.x, twoD.y],
})
} else {
console.error('No intersection point found')
}
},
})
},
'listen for circle second point': ({
context: { sketchDetails },
event,
}) => {
if (!sketchDetails) return
if (event.type !== 'Add first point') return
const quaternion = quaternionFromUpNForward(
new Vector3(...sketchDetails.yAxis),
new Vector3(...sketchDetails.zAxis)
)
// Position the click raycast plane
sceneEntitiesManager.intersectionPlane.setRotationFromQuaternion(
quaternion
)
sceneEntitiesManager.intersectionPlane.position.copy(
new Vector3(...(sketchDetails?.origin || [0, 0, 0]))
)
const dummy = new Mesh()
dummy.position.set(0, 0, 0)
const scale = sceneInfra.getClientSceneScaleFactor(dummy)
const position = new Vector3(event.data[0], event.data[1], 0)
position.applyQuaternion(quaternion)
const draftPoint = createProfileStartHandle({
isDraft: true,
from: event.data,
scale,
theme: sceneInfra._theme,
})
draftPoint.position.copy(position)
sceneInfra.scene.add(draftPoint)
sceneInfra.setCallbacks({
onClick: (args) => {
if (!args) return
if (args.mouseEvent.which !== 1) return
const { intersectionPoint } = args
if (!intersectionPoint?.twoD) return
const twoD = args.intersectionPoint?.twoD
if (twoD) {
sceneInfra.modelingSend({
type: 'Add second point',
data: {
p1: event.data,
p2: [twoD.x, twoD.y],
},
})
} else {
console.error('No intersection point found')
}
},
})
},
'update sketchDetails': assign(({ event, context }) => {
if (
event.type !== 'xstate.done.actor.actor-circle-three-point' &&
event.type !== 'xstate.done.actor.set-up-draft-circle' &&
event.type !== 'xstate.done.actor.set-up-draft-arc' &&
event.type !== 'xstate.done.actor.set-up-draft-arc-three-point' &&
event.type !== 'xstate.done.actor.set-up-draft-circle-three-point' &&
event.type !== 'xstate.done.actor.set-up-draft-rectangle' &&
event.type !== 'xstate.done.actor.set-up-draft-center-rectangle' &&
event.type !== 'xstate.done.actor.split-sketch-pipe-if-needed'
)
return {}
if (!context.sketchDetails) return {}
return {
sketchDetails: {
...context.sketchDetails,
planeNodePath:
event.output.updatedPlaneNodePath ||
context.sketchDetails?.planeNodePath ||
[],
sketchEntryNodePath: event.output.updatedEntryNodePath,
sketchNodePaths: event.output.updatedSketchNodePaths,
expressionIndexToDelete: event.output.expressionIndexToDelete,
},
}
}),
'update sketchDetails arc': assign(({ event, context }) => {
if (event.type !== 'Add start point') return {}
if (!context.sketchDetails) return {}
return {
sketchDetails: {
...context.sketchDetails,
sketchEntryNodePath: event.data.sketchEntryNodePath,
sketchNodePaths: event.data.sketchNodePaths,
},
}
}),
're-eval nodePaths': assign(({ context: { sketchDetails } }) => {
if (!sketchDetails) return {}
const planeArtifact = [
...engineCommandManager.artifactGraph.values(),
].find(
(artifact) =>
artifact.type === 'plane' &&
stringifyPathToNode(artifact.codeRef.pathToNode) ===
stringifyPathToNode(sketchDetails.planeNodePath)
)
if (planeArtifact?.type !== 'plane') return {}
const newPaths = getPathsFromPlaneArtifact(
planeArtifact,
engineCommandManager.artifactGraph,
kclManager.ast
)
return {
sketchDetails: {
...sketchDetails,
sketchNodePaths: newPaths,
sketchEntryNodePath: newPaths[0] || [],
},
selectionRanges: {
otherSelections: [],
graphSelections: [],
},
}
}),
'show default planes': () => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
kclManager.showPlanes()
},
'setup noPoints onClick listener': ({
context: { sketchDetails, currentTool },
}) => {
if (!sketchDetails) return
sceneEntitiesManager.setupNoPointsListener({
sketchDetails,
currentTool,
afterClick: (_, data) =>
sceneInfra.modelingSend(
currentTool === 'tangentialArc'
? { type: 'Continue existing profile', data }
: currentTool === 'arc'
? { type: 'Add start point', data }
: { type: 'Add start point', data }
),
})
},
'add axis n grid': ({ context: { sketchDetails } }) => {
if (!sketchDetails) return
if (localStorage.getItem('disableAxis')) return
// eslint-disable-next-line @typescript-eslint/no-floating-promises
sceneEntitiesManager.createSketchAxis(
sketchDetails.sketchEntryNodePath || [],
sketchDetails.zAxis,
sketchDetails.yAxis,
sketchDetails.origin
)
// eslint-disable-next-line @typescript-eslint/no-floating-promises
codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
},
'reset client scene mouse handlers': () => {
// when not in sketch mode we don't need any mouse listeners
// (note the orbit controls are always active though)
sceneInfra.resetMouseListeners()
},
'clientToEngine cam sync direction': () => {
sceneInfra.camControls.syncDirection = 'clientToEngine'
},
'engineToClient cam sync direction': () => {
sceneInfra.camControls.syncDirection = 'engineToClient'
},
/** TODO: this action is hiding unawaited asynchronous code */
'set selection filter to faces only': () => {
kclManager.setSelectionFilter(['face', 'object'])
},
/** TODO: this action is hiding unawaited asynchronous code */
'set selection filter to defaults': () =>
kclManager.defaultSelectionFilter(),
'Delete segment': ({ context: { sketchDetails }, event }) => {
if (event.type !== 'Delete segment') return
if (!sketchDetails || !event.data) return
// eslint-disable-next-line @typescript-eslint/no-floating-promises
deleteSegment({
pathToNode: event.data,
sketchDetails,
}).then(() => {
return codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
})
},
'Reset Segment Overlays': () => sceneEntitiesManager.resetOverlays(),
'Set context': assign({
store: ({ context: { store }, event }) => {
if (event.type !== 'Set context') return store
if (!event.data) return store
const result = {
...store,
...event.data,
}
const persistedContext: Partial<PersistedModelingContext> = {}
for (const key of PersistedValues) {
persistedContext[key] = result[key]
}
if (typeof window !== 'undefined') {
window.localStorage.setItem(
PERSIST_MODELING_CONTEXT,
JSON.stringify(persistedContext)
)
}
return result
},
}),
'remove draft entities': () => {
const draftPoint = sceneInfra.scene.getObjectByName(DRAFT_POINT)
if (draftPoint) {
sceneInfra.scene.remove(draftPoint)
}
const draftLine = sceneInfra.scene.getObjectByName(DRAFT_DASHED_LINE)
if (draftLine) {
sceneInfra.scene.remove(draftLine)
}
},
'add draft line': ({ event, context }) => {
if (
event.type !== 'Add start point' &&
event.type !== 'xstate.done.actor.setup-client-side-sketch-segments9'
)
return
let sketchEntryNodePath: PathToNode | undefined
if (event.type === 'Add start point') {
sketchEntryNodePath = event.data?.sketchEntryNodePath
} else if (
event.type === 'xstate.done.actor.setup-client-side-sketch-segments9'
) {
sketchEntryNodePath =
context.sketchDetails?.sketchNodePaths.slice(-1)[0]
}
if (!sketchEntryNodePath) return
const varDec = getNodeFromPath<VariableDeclaration>(
kclManager.ast,
sketchEntryNodePath,
'VariableDeclaration'
)
if (err(varDec)) return
const varName = varDec.node.declaration.id.name
const sg = sketchFromKclValue(kclManager.variables[varName], varName)
if (err(sg)) return
const lastSegment = sg.paths[sg.paths.length - 1] || sg.start
const to = lastSegment.to
const { group, updater } = sceneEntitiesManager.drawDashedLine({
from: to,
to: [to[0] + 0.001, to[1] + 0.001],
})
sceneInfra.scene.add(group)
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
sceneInfra.setCallbacks({
onMove: (args) => {
const { intersectionPoint } = args
if (!intersectionPoint?.twoD) return
if (!context.sketchDetails) return
const { snappedPoint, isSnapped } =
sceneEntitiesManager.getSnappedDragPoint({
intersection2d: intersectionPoint.twoD,
intersects: args.intersects,
})
if (isSnapped) {
sceneEntitiesManager.positionDraftPoint({
snappedPoint: new Vector2(...snappedPoint),
origin: context.sketchDetails.origin,
yAxis: context.sketchDetails.yAxis,
zAxis: context.sketchDetails.zAxis,
})
} else {
sceneEntitiesManager.removeDraftPoint()
}
updater(
group,
[intersectionPoint.twoD.x, intersectionPoint.twoD.y],
orthoFactor
)
},
})
},
'reset deleteIndex': assign(({ context: { sketchDetails } }) => {
if (!sketchDetails) return {}
return {
sketchDetails: {
...sketchDetails,
expressionIndexToDelete: -1,
},
}
}),
'enable copilot': () => {},
'disable copilot': () => {},
'Set selection': () => {},
'Set mouse state': () => {},
'Set Segment Overlays': () => {},
'Center camera on selection': () => {},
'Submit to Text-to-CAD API': () => {},
'Set sketchDetails': () => {},
'sketch exit execute': () => {},
'debug-action': (data) => {
console.log('re-eval debug-action', data)
},
},
// end actions
actors: {
'do-constrain-remove-constraint': fromPromise(
async ({
input: { selectionRanges, sketchDetails, data },
}: {
input: Pick<
ModelingMachineContext,
'selectionRanges' | 'sketchDetails'
> & { data?: PathToNode }
}) => {
const constraint = applyRemoveConstrainingValues({
selectionRanges,
pathToNodes: data && [data],
})
if (trap(constraint)) return
const { pathToNodeMap } = constraint
if (!sketchDetails) return
let updatedAst = await sceneEntitiesManager.updateAstAndRejigSketch(
pathToNodeMap[0],
sketchDetails.sketchNodePaths,
sketchDetails.planeNodePath,
constraint.modifiedAst,
sketchDetails.zAxis,
sketchDetails.yAxis,
sketchDetails.origin
)
if (trap(updatedAst, { suppress: true })) return
if (!updatedAst) return
await codeManager.updateEditorWithAstAndWriteToFile(updatedAst.newAst)
return {
selectionType: 'completeSelection',
selection: updateSelections(
pathToNodeMap,
selectionRanges,
updatedAst.newAst
),
}
}
),
'do-constrain-horizontally': fromPromise(
async ({
input: { selectionRanges, sketchDetails },
}: {
input: Pick<ModelingMachineContext, 'selectionRanges' | 'sketchDetails'>
}) => {
const constraint = applyConstraintHorzVert(
selectionRanges,
'horizontal',
kclManager.ast,
kclManager.variables
)
if (trap(constraint)) return false
const { modifiedAst, pathToNodeMap } = constraint
if (!sketchDetails) return
const updatedAst = await sceneEntitiesManager.updateAstAndRejigSketch(
sketchDetails.sketchEntryNodePath,
sketchDetails.sketchNodePaths,
sketchDetails.planeNodePath,
modifiedAst,
sketchDetails.zAxis,
sketchDetails.yAxis,
sketchDetails.origin
)
if (trap(updatedAst, { suppress: true })) return
if (!updatedAst) return
await codeManager.updateEditorWithAstAndWriteToFile(updatedAst.newAst)
return {
selectionType: 'completeSelection',
selection: updateSelections(
pathToNodeMap,
selectionRanges,
updatedAst.newAst
),
}
}
),
'do-constrain-vertically': fromPromise(
async ({
input: { selectionRanges, sketchDetails },
}: {
input: Pick<ModelingMachineContext, 'selectionRanges' | 'sketchDetails'>
}) => {
const constraint = applyConstraintHorzVert(
selectionRanges,
'vertical',
kclManager.ast,
kclManager.variables
)
if (trap(constraint)) return false
const { modifiedAst, pathToNodeMap } = constraint
if (!sketchDetails) return
const updatedAst = await sceneEntitiesManager.updateAstAndRejigSketch(
sketchDetails.sketchEntryNodePath || [],
sketchDetails.sketchNodePaths,
sketchDetails.planeNodePath,
modifiedAst,
sketchDetails.zAxis,
sketchDetails.yAxis,
sketchDetails.origin
)
if (trap(updatedAst, { suppress: true })) return
if (!updatedAst) return
await codeManager.updateEditorWithAstAndWriteToFile(updatedAst.newAst)
return {
selectionType: 'completeSelection',
selection: updateSelections(
pathToNodeMap,
selectionRanges,
updatedAst.newAst
),
}
}
),
'do-constrain-horizontally-align': fromPromise(
async ({
input: { selectionRanges, sketchDetails },
}: {
input: Pick<ModelingMachineContext, 'selectionRanges' | 'sketchDetails'>
}) => {
const constraint = applyConstraintHorzVertAlign({
selectionRanges: selectionRanges,
constraint: 'setVertDistance',
})
if (trap(constraint)) return
const { modifiedAst, pathToNodeMap } = constraint
if (!sketchDetails) return
const updatedAst = await sceneEntitiesManager.updateAstAndRejigSketch(
sketchDetails?.sketchEntryNodePath || [],
sketchDetails.sketchNodePaths,
sketchDetails.planeNodePath,
modifiedAst,
sketchDetails.zAxis,
sketchDetails.yAxis,
sketchDetails.origin
)
if (trap(updatedAst, { suppress: true })) return
if (!updatedAst) return
await codeManager.updateEditorWithAstAndWriteToFile(updatedAst.newAst)
const updatedSelectionRanges = updateSelections(
pathToNodeMap,
selectionRanges,
updatedAst.newAst
)
return {
selectionType: 'completeSelection',
selection: updatedSelectionRanges,
}
}
),
'do-constrain-vertically-align': fromPromise(
async ({
input: { selectionRanges, sketchDetails },
}: {
input: Pick<ModelingMachineContext, 'selectionRanges' | 'sketchDetails'>
}) => {
const constraint = applyConstraintHorzVertAlign({
selectionRanges: selectionRanges,
constraint: 'setHorzDistance',
})
if (trap(constraint)) return
const { modifiedAst, pathToNodeMap } = constraint
if (!sketchDetails) return
const updatedAst = await sceneEntitiesManager.updateAstAndRejigSketch(
sketchDetails?.sketchEntryNodePath || [],
sketchDetails.sketchNodePaths,
sketchDetails.planeNodePath,
modifiedAst,
sketchDetails.zAxis,
sketchDetails.yAxis,
sketchDetails.origin
)
if (trap(updatedAst, { suppress: true })) return
if (!updatedAst) return
await codeManager.updateEditorWithAstAndWriteToFile(updatedAst.newAst)
const updatedSelectionRanges = updateSelections(
pathToNodeMap,
selectionRanges,
updatedAst.newAst
)
return {
selectionType: 'completeSelection',
selection: updatedSelectionRanges,
}
}
),
'do-constrain-snap-to-x': fromPromise(
async ({
input: { selectionRanges, sketchDetails },
}: {
input: Pick<ModelingMachineContext, 'selectionRanges' | 'sketchDetails'>
}) => {
const constraint = applyConstraintAxisAlign({
selectionRanges,
constraint: 'snapToXAxis',
})
if (err(constraint)) return false
const { modifiedAst, pathToNodeMap } = constraint
if (!sketchDetails) return
const updatedAst = await sceneEntitiesManager.updateAstAndRejigSketch(
sketchDetails?.sketchEntryNodePath || [],
sketchDetails.sketchNodePaths,
sketchDetails.planeNodePath,
modifiedAst,
sketchDetails.zAxis,
sketchDetails.yAxis,
sketchDetails.origin
)
if (trap(updatedAst, { suppress: true })) return
if (!updatedAst) return
await codeManager.updateEditorWithAstAndWriteToFile(updatedAst.newAst)
const updatedSelectionRanges = updateSelections(
pathToNodeMap,
selectionRanges,
updatedAst.newAst
)
return {
selectionType: 'completeSelection',
selection: updatedSelectionRanges,
}
}
),
'do-constrain-snap-to-y': fromPromise(
async ({
input: { selectionRanges, sketchDetails },
}: {
input: Pick<ModelingMachineContext, 'selectionRanges' | 'sketchDetails'>
}) => {
const constraint = applyConstraintAxisAlign({
selectionRanges,
constraint: 'snapToYAxis',
})
if (trap(constraint)) return false
const { modifiedAst, pathToNodeMap } = constraint
if (!sketchDetails) return
const updatedAst = await sceneEntitiesManager.updateAstAndRejigSketch(
sketchDetails?.sketchEntryNodePath || [],
sketchDetails.sketchNodePaths,
sketchDetails.planeNodePath,
modifiedAst,
sketchDetails.zAxis,
sketchDetails.yAxis,
sketchDetails.origin
)
if (trap(updatedAst, { suppress: true })) return
if (!updatedAst) return
await codeManager.updateEditorWithAstAndWriteToFile(updatedAst.newAst)
const updatedSelectionRanges = updateSelections(
pathToNodeMap,
selectionRanges,
updatedAst.newAst
)
return {
selectionType: 'completeSelection',
selection: updatedSelectionRanges,
}
}
),
'do-constrain-parallel': fromPromise(
async ({
input: { selectionRanges, sketchDetails },
}: {
input: Pick<ModelingMachineContext, 'selectionRanges' | 'sketchDetails'>
}) => {
const constraint = applyConstraintEqualAngle({
selectionRanges,
})
if (trap(constraint)) return false
const { modifiedAst, pathToNodeMap } = constraint
if (!sketchDetails) {
trap(new Error('No sketch details'))
return
}
const recastAst = parse(recast(modifiedAst))
if (err(recastAst) || !resultIsOk(recastAst)) return
const updatedAst = await sceneEntitiesManager.updateAstAndRejigSketch(
sketchDetails?.sketchEntryNodePath || [],
sketchDetails.sketchNodePaths,
sketchDetails.planeNodePath,
recastAst.program,
sketchDetails.zAxis,
sketchDetails.yAxis,
sketchDetails.origin
)
if (trap(updatedAst, { suppress: true })) return
if (!updatedAst) return
await codeManager.updateEditorWithAstAndWriteToFile(updatedAst.newAst)
const updatedSelectionRanges = updateSelections(
pathToNodeMap,
selectionRanges,
updatedAst.newAst
)
return {
selectionType: 'completeSelection',
selection: updatedSelectionRanges,
}
}
),
'do-constrain-equal-length': fromPromise(
async ({
input: { selectionRanges, sketchDetails },
}: {
input: Pick<ModelingMachineContext, 'selectionRanges' | 'sketchDetails'>
}) => {
const constraint = applyConstraintEqualLength({
selectionRanges,
})
if (trap(constraint)) return false
const { modifiedAst, pathToNodeMap } = constraint
if (!sketchDetails) return
const updatedAst = await sceneEntitiesManager.updateAstAndRejigSketch(
sketchDetails?.sketchEntryNodePath || [],
sketchDetails.sketchNodePaths,
sketchDetails.planeNodePath,
modifiedAst,
sketchDetails.zAxis,
sketchDetails.yAxis,
sketchDetails.origin
)
if (trap(updatedAst, { suppress: true })) return
if (!updatedAst) return
await codeManager.updateEditorWithAstAndWriteToFile(updatedAst.newAst)
const updatedSelectionRanges = updateSelections(
pathToNodeMap,
selectionRanges,
updatedAst.newAst
)
return {
selectionType: 'completeSelection',
selection: updatedSelectionRanges,
}
}
),
'Get vertical info': fromPromise(
async (_: {
input: Pick<ModelingMachineContext, 'selectionRanges' | 'sketchDetails'>
}) => {
return {} as SetSelections
}
),
'Get ABS X info': fromPromise(
async (_: {
input: Pick<ModelingMachineContext, 'selectionRanges' | 'sketchDetails'>
}) => {
return {} as SetSelections
}
),
'Get ABS Y info': fromPromise(
async (_: {
input: Pick<ModelingMachineContext, 'selectionRanges' | 'sketchDetails'>
}) => {
return {} as SetSelections
}
),
'Get angle info': fromPromise(
async (_: {
input: Pick<ModelingMachineContext, 'selectionRanges' | 'sketchDetails'>
}) => {
return {} as SetSelections
}
),
'Get perpendicular distance info': fromPromise(
async (_: {
input: Pick<ModelingMachineContext, 'selectionRanges' | 'sketchDetails'>
}) => {
return {} as SetSelections
}
),
'AST-undo-startSketchOn': fromPromise(
async (_: { input: Pick<ModelingMachineContext, 'sketchDetails'> }) => {
return undefined
}
),
'animate-to-face': fromPromise(
async (_: { input?: ExtrudeFacePlane | DefaultPlane | OffsetPlane }) => {
return {} as ModelingMachineContext['sketchDetails']
}
),
'animate-to-sketch': fromPromise(
async (_: { input: Pick<ModelingMachineContext, 'selectionRanges'> }) => {
return {} as ModelingMachineContext['sketchDetails']
}
),
'Get horizontal info': fromPromise(
async (_: {
input: Pick<ModelingMachineContext, 'sketchDetails' | 'selectionRanges'>
}) => {
return {} as SetSelections
}
),
astConstrainLength: fromPromise(
async (_: {
input: Pick<
ModelingMachineContext,
'sketchDetails' | 'selectionRanges'
> & {
lengthValue?: KclCommandValue
}
}) => {
return {} as SetSelections
}
),
'Apply named value constraint': fromPromise(
async (_: {
input: Pick<
ModelingMachineContext,
'sketchDetails' | 'selectionRanges'
> & {
data?: ModelingCommandSchema['Constrain with named value']
}
}) => {
return {} as SetSelections
}
),
extrudeAstMod: fromPromise<
unknown,
ModelingCommandSchema['Extrude'] | undefined
>(async ({ input }) => {
if (!input) return new Error('No input provided')
const { selection, distance, nodeToEdit } = input
const isEditing =
nodeToEdit !== undefined && typeof nodeToEdit[1][0] === 'number'
let ast = structuredClone(kclManager.ast)
let extrudeName: string | undefined = undefined
// If this is an edit flow, first we're going to remove the old extrusion
if (isEditing) {
// Extract the plane name from the node to edit
const extrudeNameNode = getNodeFromPath<VariableDeclaration>(
ast,
nodeToEdit,
'VariableDeclaration'
)
if (err(extrudeNameNode)) {
console.error('Error extracting plane name')
} else {
extrudeName = extrudeNameNode.node.declaration.id.name
}
// Removing the old extrusion statement
const newBody = [...ast.body]
newBody.splice(nodeToEdit[1][0] as number, 1)
ast.body = newBody
}
const pathToNode = getNodePathFromSourceRange(
ast,
selection.graphSelections[0]?.codeRef.range
)
// Add an extrude statement to the AST
const extrudeSketchRes = extrudeSketch({
node: ast,
pathToNode,
artifact: selection.graphSelections[0].artifact,
artifactGraph: engineCommandManager.artifactGraph,
distance:
'variableName' in distance
? distance.variableIdentifierAst
: distance.valueAst,
extrudeName,
})
if (err(extrudeSketchRes)) return extrudeSketchRes
const { modifiedAst, pathToExtrudeArg } = extrudeSketchRes
// Insert the distance variable if the user has provided a variable name
if (
'variableName' in distance &&
distance.variableName &&
typeof pathToExtrudeArg[1][0] === 'number'
) {
const insertIndex = Math.min(
pathToExtrudeArg[1][0],
distance.insertIndex
)
const newBody = [...modifiedAst.body]
newBody.splice(insertIndex, 0, distance.variableDeclarationAst)
modifiedAst.body = newBody
// Since we inserted a new variable, we need to update the path to the extrude argument
pathToExtrudeArg[1][0]++
}
const updatedAst = await kclManager.updateAst(modifiedAst, true, {
focusPath: [pathToExtrudeArg],
zoomToFit: true,
zoomOnRangeAndType: {
range: selection.graphSelections[0]?.codeRef.range,
type: 'path',
},
})
await codeManager.updateEditorWithAstAndWriteToFile(updatedAst.newAst)
if (updatedAst?.selections) {
editorManager.selectRange(updatedAst?.selections)
}
}),
offsetPlaneAstMod: fromPromise(
async ({
input,
}: {
input: ModelingCommandSchema['Offset plane'] | undefined
}) => {
if (!input) return new Error('No input provided')
// Extract inputs
const ast = kclManager.ast
const { plane: selection, distance, nodeToEdit } = input
let insertIndex: number | undefined = undefined
let planeName: string | undefined = undefined
// If this is an edit flow, first we're going to remove the old plane
if (nodeToEdit && typeof nodeToEdit[1][0] === 'number') {
// Extract the plane name from the node to edit
const planeNameNode = getNodeFromPath<VariableDeclaration>(
ast,
nodeToEdit,
'VariableDeclaration'
)
if (err(planeNameNode)) {
console.error('Error extracting plane name')
} else {
planeName = planeNameNode.node.declaration.id.name
}
const newBody = [...ast.body]
newBody.splice(nodeToEdit[1][0], 1)
ast.body = newBody
insertIndex = nodeToEdit[1][0]
}
// Extract the default plane from selection
const plane = selection.otherSelections[0]
if (!(plane && plane instanceof Object && 'name' in plane))
return trap('No plane selected')
// Get the default plane name from the selection
const offsetPlaneResult = addOffsetPlane({
node: ast,
defaultPlane: plane.name,
offset:
'variableName' in distance
? distance.variableIdentifierAst
: distance.valueAst,
insertIndex,
planeName,
})
// Insert the distance variable if the user has provided a variable name
if (
'variableName' in distance &&
distance.variableName &&
typeof offsetPlaneResult.pathToNode[1][0] === 'number'
) {
const insertIndex = Math.min(
offsetPlaneResult.pathToNode[1][0],
distance.insertIndex
)
const newBody = [...offsetPlaneResult.modifiedAst.body]
newBody.splice(insertIndex, 0, distance.variableDeclarationAst)
offsetPlaneResult.modifiedAst.body = newBody
// Since we inserted a new variable, we need to update the path to the extrude argument
offsetPlaneResult.pathToNode[1][0]++
}
const updateAstResult = await kclManager.updateAst(
offsetPlaneResult.modifiedAst,
true,
{
focusPath: [offsetPlaneResult.pathToNode],
}
)
await codeManager.updateEditorWithAstAndWriteToFile(
updateAstResult.newAst
)
if (updateAstResult?.selections) {
editorManager.selectRange(updateAstResult?.selections)
}
}
),
helixAstMod: fromPromise(
async ({
input,
}: {
input: ModelingCommandSchema['Helix'] | undefined
}) => {
if (!input) return new Error('No input provided')
// Extract inputs
const ast = kclManager.ast
const {
revolutions,
angleStart,
counterClockWise,
radius,
axis,
length,
nodeToEdit,
} = input
let opInsertIndex: number | undefined = undefined
let opVariableName: string | undefined = undefined
// If this is an edit flow, first we're going to remove the old one
if (nodeToEdit && typeof nodeToEdit[1][0] === 'number') {
// Extract the old name from the node to edit
const oldNode = getNodeFromPath<VariableDeclaration>(
ast,
nodeToEdit,
'VariableDeclaration'
)
if (err(oldNode)) {
console.error('Error extracting plane name')
} else {
opVariableName = oldNode.node.declaration.id.name
}
const newBody = [...ast.body]
newBody.splice(nodeToEdit[1][0], 1)
ast.body = newBody
opInsertIndex = nodeToEdit[1][0]
}
for (const variable of [revolutions, angleStart, radius, length]) {
// Insert the variable if it exists
if (
'variableName' in variable &&
variable.variableName &&
variable.insertIndex !== undefined
) {
const newBody = [...ast.body]
newBody.splice(
variable.insertIndex,
0,
variable.variableDeclarationAst
)
ast.body = newBody
}
}
const valueOrVariable = (variable: KclCommandValue) =>
'variableName' in variable
? variable.variableIdentifierAst
: variable.valueAst
const result = addHelix({
node: ast,
revolutions: valueOrVariable(revolutions),
angleStart: valueOrVariable(angleStart),
counterClockWise,
radius: valueOrVariable(radius),
axis,
length: valueOrVariable(length),
insertIndex: opInsertIndex,
variableName: opVariableName,
})
const updateAstResult = await kclManager.updateAst(
result.modifiedAst,
true,
{
focusPath: [result.pathToNode],
}
)
await codeManager.updateEditorWithAstAndWriteToFile(
updateAstResult.newAst
)
if (updateAstResult?.selections) {
editorManager.selectRange(updateAstResult?.selections)
}
}
),
sweepAstMod: fromPromise(
async ({
input,
}: {
input: ModelingCommandSchema['Sweep'] | undefined
}) => {
if (!input) return new Error('No input provided')
// Extract inputs
const ast = kclManager.ast
const { target, trajectory, sectional, nodeToEdit } = input
let variableName: string | undefined = undefined
let insertIndex: number | undefined = undefined
// If this is an edit flow, first we're going to remove the old one
if (nodeToEdit !== undefined && typeof nodeToEdit[1][0] === 'number') {
// Extract the plane name from the node to edit
const variableNode = getNodeFromPath<VariableDeclaration>(
ast,
nodeToEdit,
'VariableDeclaration'
)
if (err(variableNode)) {
console.error('Error extracting name')
} else {
variableName = variableNode.node.declaration.id.name
}
// Removing the old statement
const newBody = [...ast.body]
newBody.splice(nodeToEdit[1][0], 1)
ast.body = newBody
insertIndex = nodeToEdit[1][0]
}
// Find the target declaration
const targetNodePath = getNodePathFromSourceRange(
ast,
target.graphSelections[0].codeRef.range
)
// Gotchas, not sure why
// - it seems like in some cases we get a list on edit, especially the state that e2e hits
// - looking for a VariableDeclaration seems more robust than VariableDeclarator
const targetNode = getNodeFromPath<
VariableDeclaration | VariableDeclaration[]
>(ast, targetNodePath, 'VariableDeclaration')
if (err(targetNode)) {
return new Error("Couldn't parse profile selection")
}
const targetDeclarator = isArray(targetNode.node)
? targetNode.node[0].declaration
: targetNode.node.declaration
// Find the trajectory (or path) declaration
const trajectoryNodePath = getNodePathFromSourceRange(
ast,
trajectory.graphSelections[0].codeRef.range
)
// Also looking for VariableDeclaration for consistency here
const trajectoryNode = getNodeFromPath<VariableDeclaration>(
ast,
trajectoryNodePath,
'VariableDeclaration'
)
if (err(trajectoryNode)) {
return new Error("Couldn't parse path selection")
}
const trajectoryDeclarator = trajectoryNode.node.declaration
// Perform the sweep
const { modifiedAst, pathToNode } = addSweep({
node: ast,
targetDeclarator,
trajectoryDeclarator,
sectional,
variableName,
insertIndex,
})
await updateModelingState(
modifiedAst,
{
kclManager,
editorManager,
codeManager,
},
{
focusPath: [pathToNode],
}
)
}
),
loftAstMod: fromPromise(
async ({
input,
}: {
input: ModelingCommandSchema['Loft'] | undefined
}) => {
if (!input) return new Error('No input provided')
// Extract inputs
const ast = kclManager.ast
const { selection } = input
const declarators = selection.graphSelections.flatMap((s) => {
const path = getNodePathFromSourceRange(ast, s?.codeRef.range)
const nodeFromPath = getNodeFromPath<VariableDeclarator>(
ast,
path,
'VariableDeclarator'
)
return err(nodeFromPath) ? [] : nodeFromPath.node
})
// TODO: add better validation on selection
if (!(declarators && declarators.length > 1)) {
trap('Not enough sketches selected')
}
// Perform the loft
const loftSketchesRes = loftSketches(ast, declarators)
const updateAstResult = await kclManager.updateAst(
loftSketchesRes.modifiedAst,
true,
{
focusPath: [loftSketchesRes.pathToNode],
}
)
await codeManager.updateEditorWithAstAndWriteToFile(
updateAstResult.newAst
)
if (updateAstResult?.selections) {
editorManager.selectRange(updateAstResult?.selections)
}
}
),
shellAstMod: fromPromise(
async ({
input,
}: {
input: ModelingCommandSchema['Shell'] | undefined
}) => {
if (!input) {
return new Error('No input provided')
}
// Extract inputs
const ast = kclManager.ast
const { selection, thickness, nodeToEdit } = input
let variableName: string | undefined = undefined
let insertIndex: number | undefined = undefined
// If this is an edit flow, first we're going to remove the old extrusion
if (nodeToEdit && typeof nodeToEdit[1][0] === 'number') {
// Extract the plane name from the node to edit
const variableNode = getNodeFromPath<VariableDeclaration>(
ast,
nodeToEdit,
'VariableDeclaration'
)
if (err(variableNode)) {
console.error('Error extracting name')
} else {
variableName = variableNode.node.declaration.id.name
}
// Removing the old statement
const newBody = [...ast.body]
newBody.splice(nodeToEdit[1][0], 1)
ast.body = newBody
insertIndex = nodeToEdit[1][0]
}
// Turn the selection into the faces list
const clonedAstForGetExtrude = structuredClone(ast)
const faces: Expr[] = []
let pathToExtrudeNode: PathToNode | undefined = undefined
for (const graphSelection of selection.graphSelections) {
const extrudeLookupResult = getPathToExtrudeForSegmentSelection(
clonedAstForGetExtrude,
graphSelection,
engineCommandManager.artifactGraph
)
if (err(extrudeLookupResult)) {
return new Error(
"Couldn't find extrude paths from getPathToExtrudeForSegmentSelection",
{ cause: extrudeLookupResult }
)
}
const extrudeNode = getNodeFromPath<VariableDeclaration>(
ast,
extrudeLookupResult.pathToExtrudeNode,
'VariableDeclaration'
)
if (err(extrudeNode)) {
return new Error("Couldn't find extrude node from selection", {
cause: extrudeNode,
})
}
const segmentNode = getNodeFromPath<VariableDeclaration>(
ast,
extrudeLookupResult.pathToSegmentNode,
'VariableDeclaration'
)
if (err(segmentNode)) {
return new Error("Couldn't find segment node from selection", {
cause: segmentNode,
})
}
if (
extrudeNode.node.declaration.init.type === 'CallExpression' ||
extrudeNode.node.declaration.init.type === 'CallExpressionKw'
) {
pathToExtrudeNode = extrudeLookupResult.pathToExtrudeNode
} else if (
segmentNode.node.declaration.init.type === 'PipeExpression'
) {
pathToExtrudeNode = extrudeLookupResult.pathToSegmentNode
} else {
return new Error(
"Couldn't find extrude node that was either a call expression or a pipe",
{ cause: segmentNode }
)
}
const selectedArtifact = graphSelection.artifact
if (!selectedArtifact) {
return new Error('Bad artifact from selection')
}
// Check on the selection, and handle the wall vs cap casees
let expr: Expr
if (selectedArtifact.type === 'cap') {
expr = createLiteral(selectedArtifact.subType)
} else if (selectedArtifact.type === 'wall') {
const tagResult = mutateAstWithTagForSketchSegment(
ast,
extrudeLookupResult.pathToSegmentNode
)
if (err(tagResult)) {
return tagResult
}
const { tag } = tagResult
expr = createIdentifier(tag)
} else {
return new Error('Artifact is neither a cap nor a wall')
}
faces.push(expr)
}
if (!pathToExtrudeNode) {
return new Error('No path to extrude node found')
}
const extrudeNode = getNodeFromPath<VariableDeclarator>(
ast,
pathToExtrudeNode,
'VariableDeclarator'
)
if (err(extrudeNode)) {
return new Error("Couldn't find extrude node", { cause: extrudeNode })
}
// Perform the shell op
const sweepName = extrudeNode.node.id.name
const addResult = addShell({
node: ast,
sweepName,
faces: faces,
thickness:
'variableName' in thickness
? thickness.variableIdentifierAst
: thickness.valueAst,
insertIndex,
variableName,
})
// Insert the thickness variable if the user has provided a variable name
if (
'variableName' in thickness &&
thickness.variableName &&
typeof addResult.pathToNode[1][0] === 'number'
) {
const insertIndex = Math.min(
addResult.pathToNode[1][0],
thickness.insertIndex
)
const newBody = [...addResult.modifiedAst.body]
newBody.splice(insertIndex, 0, thickness.variableDeclarationAst)
addResult.modifiedAst.body = newBody
// Since we inserted a new variable, we need to update the path to the extrude argument
addResult.pathToNode[1][0]++
}
const updateAstResult = await kclManager.updateAst(
addResult.modifiedAst,
true,
{
focusPath: [addResult.pathToNode],
}
)
await codeManager.updateEditorWithAstAndWriteToFile(
updateAstResult.newAst
)
if (updateAstResult?.selections) {
editorManager.selectRange(updateAstResult?.selections)
}
}
),
filletAstMod: fromPromise(
async ({
input,
}: {
input: ModelingCommandSchema['Fillet'] | undefined
}) => {
if (!input) {
return new Error('No input provided')
}
// Extract inputs
const ast = kclManager.ast
const { selection, radius } = input
const parameters: FilletParameters = {
type: EdgeTreatmentType.Fillet,
radius,
}
const dependencies = {
kclManager,
engineCommandManager,
editorManager,
codeManager,
}
// Apply fillet to selection
const filletResult = await applyEdgeTreatmentToSelection(
ast,
selection,
parameters,
dependencies
)
if (err(filletResult)) return filletResult
}
),
'actor.parameter.create': fromPromise(
async ({
input,
}: {
input: ModelingCommandSchema['event.parameter.create'] | undefined
}) => {
if (!input) return new Error('No input provided')
const { value } = input
if (!('variableName' in value)) {
return new Error('variable name is required')
}
const newAst = insertNamedConstant({
node: kclManager.ast,
newExpression: value,
})
const updateAstResult = await kclManager.updateAst(newAst, true)
await codeManager.updateEditorWithAstAndWriteToFile(
updateAstResult.newAst
)
if (updateAstResult?.selections) {
editorManager.selectRange(updateAstResult?.selections)
}
}
),
'actor.parameter.edit': fromPromise(
async ({
input,
}: {
input: ModelingCommandSchema['event.parameter.edit'] | undefined
}) => {
if (!input) return new Error('No input provided')
// Get the variable AST node to edit
const { nodeToEdit, value } = input
const newAst = structuredClone(kclManager.ast)
const variableNode = getNodeFromPath<Node<VariableDeclarator>>(
newAst,
nodeToEdit
)
if (
err(variableNode) ||
variableNode.node.type !== 'VariableDeclarator' ||
!variableNode.node
) {
return new Error('No variable found, this is a bug')
}
// Mutate the variable's value
variableNode.node.init = value.valueAst
await updateModelingState(newAst, {
codeManager,
editorManager,
kclManager,
})
}
),
'set-up-draft-circle': fromPromise(
async (_: {
input: Pick<ModelingMachineContext, 'sketchDetails'> & {
data: [x: number, y: number]
}
}) => {
return {} as SketchDetailsUpdate
}
),
'set-up-draft-circle-three-point': fromPromise(
async (_: {
input: Pick<ModelingMachineContext, 'sketchDetails'> & {
data: { p1: [x: number, y: number]; p2: [x: number, y: number] }
}
}) => {
return {} as SketchDetailsUpdate
}
),
'set-up-draft-rectangle': fromPromise(
async (_: {
input: Pick<ModelingMachineContext, 'sketchDetails'> & {
data: [x: number, y: number]
}
}) => {
return {} as SketchDetailsUpdate
}
),
'set-up-draft-center-rectangle': fromPromise(
async (_: {
input: Pick<ModelingMachineContext, 'sketchDetails'> & {
data: [x: number, y: number]
}
}) => {
return {} as SketchDetailsUpdate
}
),
'set-up-draft-arc': fromPromise(
async (_: {
input: Pick<ModelingMachineContext, 'sketchDetails'> & {
data: [x: number, y: number]
}
}) => {
return {} as SketchDetailsUpdate
}
),
'set-up-draft-arc-three-point': fromPromise(
async (_: {
input: Pick<ModelingMachineContext, 'sketchDetails'> & {
data: [x: number, y: number]
}
}) => {
return {} as SketchDetailsUpdate
}
),
'setup-client-side-sketch-segments': fromPromise(
async (_: {
input: Pick<ModelingMachineContext, 'sketchDetails' | 'selectionRanges'>
}) => {
return undefined
}
),
'split-sketch-pipe-if-needed': fromPromise(
async (_: { input: Pick<ModelingMachineContext, 'sketchDetails'> }) => {
return {} as SketchDetailsUpdate
}
),
chamferAstMod: fromPromise(
async ({
input,
}: {
input: ModelingCommandSchema['Chamfer'] | undefined
}) => {
if (!input) {
return new Error('No input provided')
}
// Extract inputs
const ast = kclManager.ast
const { selection, length } = input
const parameters: ChamferParameters = {
type: EdgeTreatmentType.Chamfer,
length,
}
const dependencies = {
kclManager,
engineCommandManager,
editorManager,
codeManager,
}
// Apply chamfer to selection
const chamferResult = await applyEdgeTreatmentToSelection(
ast,
selection,
parameters,
dependencies
)
if (err(chamferResult)) return chamferResult
}
),
'submit-prompt-edit': fromPromise(
async ({
input,
}: {
input: ModelingCommandSchema['Prompt-to-edit']
}) => {}
),
deleteSelectionAstMod: fromPromise(
({
input: { selectionRanges },
}: {
input: { selectionRanges: Selections }
}) => {
return new Promise((resolve, reject) => {
if (!selectionRanges) {
reject(new Error(deletionErrorMessage))
}
const selection = selectionRanges.graphSelections[0]
if (!selectionRanges) {
reject(new Error(deletionErrorMessage))
}
deleteSelectionPromise(selection)
.then((result) => {
if (err(result)) {
reject(result)
return
}
resolve(result)
})
.catch(reject)
})
}
),
appearanceAstMod: fromPromise(
async ({
input,
}: {
input: ModelingCommandSchema['Appearance'] | undefined
}) => {
if (!input) return new Error('No input provided')
// Extract inputs
const ast = kclManager.ast
const { color, nodeToEdit } = input
if (!(nodeToEdit && typeof nodeToEdit[1][0] === 'number')) {
return new Error('Appearance is only an edit flow')
}
const result = setAppearance({
ast,
nodeToEdit,
color,
})
if (err(result)) {
return err(result)
}
const updateAstResult = await kclManager.updateAst(
result.modifiedAst,
true,
{
focusPath: [result.pathToNode],
}
)
await codeManager.updateEditorWithAstAndWriteToFile(
updateAstResult.newAst
)
if (updateAstResult?.selections) {
editorManager.selectRange(updateAstResult?.selections)
}
}
),
exportFromEngine: fromPromise(
async ({}: { input?: ModelingCommandSchema['Export'] }) => {
return undefined as Error | undefined
}
),
makeFromEngine: fromPromise(
async ({}: {
input?: {
machineManager: MachineManager
} & ModelingCommandSchema['Make']
}) => {
return undefined as Error | undefined
}
),
},
// end actors
}).createMachine({
/** @xstate-layout N4IgpgJg5mDOIC5QFkD2EwBsCWA7KAxAMICGuAxlgNoAMAuoqAA6qzYAu2qujIAHogC0ANhoBWAHQAOAMwB2KQEY5AFgCcGqWqkAaEAE9Ew0RLEqa64TIBMKmTUXCAvk71oMOfAQDKYdgAJYLDByTm5aBiQQFjYwniiBBEFFGjUVUxoZKTEZMSkLRTVrPUMEDQk5a1FctTFFFRVrWxc3dCw8Ql8AgFtUAFcgwPYSdjAI3hiOLnjQROTFRWtpdQc1Rbk5NWE5EsQZbYkVOqyaOXEz60UxFpB3dq8u-3JuUb52cajJuN45lK2JBz2bZqLIKay6AxGFSKCSOS7WOrGFRSLQ3O6eTp+fy+KDdMC4AIAeQAbmAAE6YEj6WAfZisKbcH5CBZ5CS1ORiDZiNRyYQqDa7BB1JaZYRiUSVay5MVotoY4j40Zkp4kPFkkj+biBYKhaa06L074JZnKKQSezrdQ1FTCCGlfI0CTCaw0JQaHKKKSyjwdCTYCCYMAEACiBPJgQA1n5yAALfVfaZMpIImFW4QpT22IpiQWVNQVKQKQsKPmLa6uW5y33+wMhsPK2BR9ixqiKSJ02KJ43J2wFqpSS48lFi4qQhDGGFiKcLPmc7Rrb33KB+gNB4NvMl9DDxw1d2ZCblm52pNa2YQ8tSC6y8pYyT287Y5ZwV9HV1cEABKYGJqEwpJ3naMt2gicpIUjpjI6iNKI-IqIKjjOhUtSKHeRzcmKciLhiK61t4ADuYBgEwAEMjM-AHmk0g8uIVxbDahZXmICKHOYaSLPY+TWFhb61gAMqgABm7z0BMu5AfuSR5OkGwujY-Lpq6OZjk0iywp6YrcoUMhZCo3H4DhQbeDGWCYCRRoSSBKKwjQDjGNplyiIoV4LI6qjWEOjgNGeenLjWQYAGLYJggbCe2BqAWRcx5HI0iQU+KJKHyV7mOkVgWGcUjmO5Fg+QZxAxqqAnkmZe7kUkjlOjZVzIg44GZVeVjpJoyK1GINlis+rQ+vpfkhnwLBkqFokRUmghyPYTrgdp0JKKswiCpB2lOu5U6XOIzobLlvXICQUYleJZVjXeTraQsK3XgUC02I6WgteIVr7JhL5Vj174ACLBKM2qBrq4QiZ8YmRUI2SOhoRTXvUA42AtFiOlO3LaRcRydZW3W+e+AAqYBvII7CoIIRAAIJvftQOSaoBbaChaR2I08GQTdGgInk4oNNCW3voSAkCUEARMJSuBjP9HakaN6bpCDVxiosKTiPBTNsloCz2JsGzjRztYABLtHwpOjXkZp2IWNC2FO4i5FeKL5pkag0HZno8rpz1o3lAAKZKoN0TDsLj+OQBwevAc6TV2EUlSFkx6ZXvYkgggipuFF5Mga0GhNMEwYAkOqFBC2FCYHXM1SmFY7nS-YduXmOd6FuamxZLkVx207XVLgZEgxv6YCuwLcAEITg3YAJJChP4UDqkwMb+CwTB9JSowQIHFmiJIfL5KoZzIteo6lCkfLmraCj1E+izN6jrd+RIsAxqgeHd2Qvf95wQ8j2PJAT-4YBe5wkCL4dh4VJyRoGxFjbBNvBS4R4LC1HBtscaKNXz6W8E2WMxAyCUEwEg6McZhbhVFt2eoqk4TQltGzV029EApBrrbO8xg0hFkcLlTBzYYwEA+iFMA2pcSKl-okSwEgEQDjSDkJoJcFrCEVusTK+wZb8kYcglhzwMAfwgBwfwEA+hkg6JGLBPCKGcknFkShVxchclzJsKigJHAuUKE9Fu2EmGxgkA4mMABJd8O0oycLxASfwpIB7kBIKZHB+cyZXGUPw+QiUGj5DpmOFCLoARSKUKaNecisFOPkW42sHiOFBC4T46+miABeLxAm6IQI3R0dlOSgm2FDcBnpzTl05NeM4RY0nMIyVgrJQYiDcFgOwdUeB-CFOwCUgkgS1HYAGWg3Ow08ESUho6VIlQFCHhBFkaOJtYraHFCkY42wOmOOcT04g-TBkkGGX4zgATMBTJmTncp9RagAlOAOWGbUmbOXyOaRSKUpTghBEcmMXTmGnL6bgAZQzcD+EJgAIW8P4AAGk8uoqYijIn2LUFWMhLbpgqNCFC4o5CFB5MC0FsZwXnOhbChF-gACaqKFj8J5ChdYWwSXkIQO5F5nzbaVDMLyBQ5KTnvghVCy5MKyBQEDKi88hxQQ8kgoOOCyloTpAsO5aaJsobwJesuZxFLXFiupZK-wgZ8DsGwXnQGSYpYyEOC6TkCgtjaUYsxKc8hxrcldKIPVLtDWitrOKi5wyM5kgzrgFR5BZ5Z3ucMR5wTbX4KOOkKcjRkligjvNZSlRJyapyPIdK9QRWZJNZC0NMLRnjOGMFfQ-hAnYCgLgJ5yIzRrDitCVIjQULwUaA610JtMjyFyIWUt3Ty0SqueSG5gTMD1sbc21F4hDiFkygOGJKQuXGNvKoaqNhbYLHHWCydlbAi4Dfv4PGyLUUyHzNXNWbkESqp3mka2ssmgpi0DKZ2rdA1luDaa4ZsAL1MCvagBlt78xsvBGKBwCw1hXnuk0ji9q23HspaemlYAACOfRJkWqgFapl+ZxSO0yuefsOw4lWBipcG0U4LjcnBBh41gGK00qYFnOd1Ak0jXwZ6xJVxwIaBsIWJycTsjpDvXYElORCXXlY1SjjZqySf1QKSJ4QGCQ0j4wssq9QKbGDcm1ZEyg3VxPGjFfI6wJRrEUEp98X5BDYxCH0UYTzTy-MyNBHVWw7SIFtpIT1iJ1jjSuI52siiOH+wCOozR+BtHMNRSS0whjXTGOdUpe05hV2ZRslkbIVxbFn3sfIo1ymp0wrwhwSeF68QQF8YEvocyAb8cWW1JY15zx3scJBEcC1DP8NOPyEl9gtiHN-WV9JQagyxmlRwvGv5yk8tI8vZ0TELP2lUhyzkdt9hZHPOSwmeFLkBGraUu5KiHmUH8HgASqACAQG4GAP0uAfxRgkDAH2F2JmYEEPd1AK2chw0BGsm2RRcXKRsOI2jKSrjgnEMd07qjfu1vjbMu7uAHsEHJB7MkEh+YjAe2SboX2-CCDR4EgH2Ogd6fMmVBEx1HAghs6kew-IrzgjNAOdQU5NBrOR2d3xM7sC3IxznLHOPnuCzex91733BDXLF9TwHwOsiwjDpsGDbznK2ydOhU4Gw1hbCF6o5X4vrsJtu4D3HZJ8eE7niTsniuLeq9p+rnnEprwgl2dlxA9H23qFqPkRaiwzcBHhYipFUvHsy9e3geX5OfYkAAEawEEHwGnD3geNGkCzklDgOS1Dxe22wMk3nSwj7S6Pse7cO6J+wZ3yfBBp4z1ntX9PSqJGfQ6tt2QpRmHFFD0o4IpSwjsDZCORvk5Td9Iak7wuo8Mrr-HuXqBPuK7b4IfQ2e6c2va4zswSwu2VBtBsrQ-vuWO34bIIETR+Vejn4g8ri-VHL-pXXvHqACeN+b1v9PHfPfXPAdAcP1TITIcUHNUoFWG6MwIvLQcaU3Z-A1V-FHAIaVQMVfF7dfTfCnTAsAYArvAuAPMwB1aUFILKO9YwZKKwCoSGcUGzJWcsOxefNA4XAgr-e3H-R3YnH-F3fA-AQMIgg-fTHvMwaSH1fzTKW2bYZKcfRA7Mb9M2Fg0rNg9JNOfmetQjK1LTFTPAdgJ7HAxPDfV7QmbwDGQQZ4fQ3AQQHQ61eZBncQ6EVdOBLVAcU0RiNYZYIVcuDYfYY7dOedc1fEIjSeawqrQw7-X-J3fgiQcwywiIytOw0I4jYgsmZ9JYK4YRHrWwQsLlZ9fMBoEhYUZQd0avcNSNaNWNZUK3THW3NfEwvAn2So-EaoykMkQQOonOEQxw7vAPFqAEeJC-WhXkK8caLIqcAcJoNCdQCo8kKosXGoiXG3WnevHgv-OIxXVoqNJYjoro6Za3QgzvUQpwgYrQIYg9bScCc8MY5SEhWEKYz9WYtQclXiPARbVAX8AgebfAT45bdIpMI4bQSqaJYTfkEEeCfbV5HITYe6YlN4j48DX8CQFxXADgPuCARrGZQaKeVAAw8pMwFIc0RYQFdQJnK-FIVLO8eyW0dNBYJ-Vgl-dJd4wWZEzAVE9Eww0gHOIJU4-o6-PPZQW0Ok3kUdaAihWqFlG2WBbYeQBzFAo1CQVk-4jk3AfgwJYgTAVgXJeRQk7Scg04CuAfSCcBZEWEJ1MbUTMUxEtkpbDkwKdEq+KeD2ASIKIMRo97Uwy+PwPoJgKwnARUQQNgDAYM+RYMsAfJdgWAOQcpTFJYOiW2RAhoHIcBcEUwKUDtNeE2ZQW01UiQR06ZSeJgV0909YmIvg0nH09gP0gM7AIMkMwgxsLBCMqMmMwkgrfhMwBSS4GTEfChdSQ4LYSGEEC4Z0clNEjEp5cEVMCjO9fIbQI4ajHeeoEUIoFyYdYrErBBVA9JScww1sfkkgipGktkUbRc5QaCF9AckE84VCXnPkbc-VJUjGBbAkbASZLOcgcDH4gqP49kuMjbNkTkUQIvWoMBOJPhMU9QOudcnIclV8v498z8skb8vGTkjEiFTgXAFrD+PgQ4rREswSd0-U8CLslM+ubQBQeCPZJ0ElWTB2bSJiBCt8zgFCtC1ACQdU0nTUogbUwYZs5LQE7seA8RLYVmTYNqHzeCVQSQM8YrdyaES4VQncl81ij8u5L88DAsvAIsl04i2sT0pPXmWs8gQMgkYMzuMMlsvJbxaM4QOM79WEXkXnBjI4eCDCc0fLRGPIVnFipCtizS1C7Sws50oit02saI3gpvOIky-0sy+siyxs6y5hVsuy2ABy4SiSJiFCQ4EBRjExPIDyveQ7Q9eOCBV4xUw1PoKNCDHE9gZxQkXAIw2XJoswiwwQGq57YM4YQaRqltLKgzKGWEc8G0BodYMJKExpVlUo2TfI8lL8UITg+038hbACwa3hRofMWoa8aUcCxDZSE3A3WQGaDkBwWRKq8rRahNGVfMt-bCqAbUawxrZ4MkQWMkAgUKyeNTJaoQ1rEWM4oUfYG6cbbkDQUayuGAyhfhKCQfX1KUWfJk3czpa65ar4jk+6rRH-JtPAH4nAcgCMLHQISgQWfUs4WKcCGqXeW0S2BJfwu9CGHMlCBakIG6rA+0nSp04yRrVGv6lqhPL0z7OKushsqywS2MNKxUXTI8smBjMvSCPMJofYK-ewO9PKtmSoHzA7Fm36269kiQEgLErRCAdUISfwH6tmj04wwW17XmTq-0k2kgISQQC2gg-UqoaQbkLa9STMGGRwSaEsDSWmHKS69JXmvWjmw2lRRLR2s212vmqKzYqsu22s2On2eO26wC8xF0I4SCQoTKa87lNdAEBmuwMJIBclIgRUcMcO9m9G1a-8+0uM3rABZJJQZEbspDdM5QIkrdJiZEU+VSw1Ku+sfwWuu69ArRV696z63S50kmpUc21mt2jaxAeAw2ZVbYQlTkALblZ9fhBYLSLQA9J8gNcrEexe8e-WzGxLbGqAXGwmLEp4au5UDOrAu+vAfUooJ0RcpQVQaoBEJDbkDMnSc8W0KxSul+se5ev6-Wr6yAfwC+mumB26-m3A2230+K8yn2ZK8WmMSWnTawQC9QABVIFIBmfurZGKO2YzF48USB0eq+yOp+tO5+0et+q21qm26s+2ro02n2Be8kF2lB2VVeoG7+oEVNeEGyKcZyVkUOCBMEQBBhy+kR-MqOtRfhthxejh8s6K5vFOh2-hqwl+4R3W0RmWoE7YGEQoUQcCMc8aJDKhGwcaXrWwBoRGtQ5kzpMyzOGFWAOeIMJ5PkKpZeLeKTGmuJVYJCCwMUSfc60+v9c+7gZXRLa9erBB4kZrThgWpPEgAZENaFAAOVVEgAADVsngcHARruzn02peQVayxFZUgy46gB7K6UnRc0mIMMnGssnMAWs9Gk6yd8n2BCnJUSmGsKmBn-rcFAaUwYQi9TgZzxAtAFoXRrZQ9TggsUJMpq8kiaU1NehNMDnJVoy0G2qJBurTm8BhHjnCCbmCQqnHRolzBVnDMOQZLjozBtATYmJ8hjB9ntMRlsaa0519ALnuHrntNKdQXLt51gdrwDdCgZyt7wRlyKEXRDYURQ4bBaIbAgWbCRd-FwXIWk9oWbCldRdbkEWxGKTzR+QIEe6tAIKd4gRhsz9kQzg4QuJQ7Ol7q9CqsQXil4WF0cBm0yXvSKWqtYWRW-t51W9xWBrLHuwv1TAN40hHoRS+0rgKhAUQRE5ZA+RCWhW3c60G0lXJXPtpXkizWFXF1lW+jjyEQbIMgO1usGMJSKk0UJ8K9j7hRGSvHkbHEBXHnz1L1r0kUrXXsbXoVgzQNfZM9EXYdZDpihUvXE4YRU2HBwRhEyU+WQ30DBWz0QMI2IN6Vo2rn8ZHn4235E39B1clhI5ZBagt67jR9h1rIkQlAfdbZeWkalTQ3gXcN8M7l7DK3Y3JUXM8Nqd7DgdwkLxNaqhCh2oPKmg2RIILzqgj0C2QUh2iWuN1RgosAJ3q2YXD2eM+SnWMj5M2R0wOQ2ULx+yKlMpJAczVAgRqopRK6-yYB-AMZ0a7seYCA4yEQYomNGhoJhE0yboMxFgw5xQVLnzh7f2OEAPfwgPYAQO2xr2gSwOKg-nIOqhoO4lrwec4P3JrxEOf21r0O7lB4sOqBrAVWJJGhOQCPMioPoY4ltJxFZozoEOf0B2UPaPAOGOQOZAWOyo2PwPCPbBiPuO2XKJ+P4OqOhOg2lSiBUP-2xPgOqAVApPeF8OIP5OxRFOKEjglgVPKOOR1Oh7z7tO6PMOQOxBDPEAZOOPj8uPt06hYP4MbPEPPH7P0ktPROMPxOqBhA3OEAPOTPvOaLbR89-PBP4Ld2JBQv-ynOIu5Bou2OHVdlsipQt0MWKlnRsXoJwIGTCsaPMvdPGOpBcumdFY6hVo7wTYSvlBVIB6bPzAGE0uiBsBUK67vjfi-2m6xHQ4YQORNg+d006gFo2pxFeR87EpshUR+vBvfHr7J7b7NF77mrH6XrNv369vP6xGchMoFUrF+V0psgFvEuxRRM0gyujhK7juJ6zstEPxDbsABhZ6uanh3vCSjhHQsWlKihxAHAFoe2AFPQUz8gak3uhv8z4HGsBvkfK3haErRbQy8GCHoyVBAKpwKhzxfU4objoe7ZrIZjzZVZmaNvkf9ao7jatHyB3vMeKdU7jG2fkfgeXCpR2vriUQFgxFzFTgmKiU2JA3gvOl0etvmHo7HrWGeffGhnYjk7OejGnaBGgeJulAV4k486HJn3R0JZwRt7B0Nkau-26PL5+YOAHqktYwp5sAM5Mf7ecHwymBXfjiBJBBBZIAf46X+xDgVlKg71TNlBBR-5NI0pEozwkOz6QvHP0a7ecB2BHe8GXe3fE71eydYAPeUqJbveM4Ac-eA+MAF4xHlBx8y7V5vUYJn3iUMz4OFAzgchuRre0PU+4qnfJ4Sd2SOeaysHEqcGxbwzbKpbylxtyDiiY4mhVor8bQEz1Awl-MHBTgu+dOUTe+s+B+Vrc-Kz8-MGRakrx+bLIz0rp+t0LE3kJiqhC7C0LSWlCwRwRMkf5eYw1MOEWADDB-RvVScZftPnm5wuNc2hdUOE1FtAohVaxufthp2Hrvcr0X-QiHiT-4c0b6j1N0mSAGRoCCQmJRrNgNwG-8nm53cEH3k9BGs7wrbCTKUEg5vsSUVmVpIjA-7s0UBP-fEj4gwE7dHqQQZ6ngMMKHcnq3ARrCQKGhtYxCa9B-gfWMDixwKd4QUJPgdSddL8eLQ7KwMWzsCBBTPI2jHVZ5ICrU3-AQUP14Zp0rC73XGOwMEBiD9SqQCoLkBWSMEvIigrtGyB5ByDL8Gg5AUYLEHbdPuaTDuGSFEGcDDCX1QHoz0MGoCbBZA3kAqmMCZlKCj4RQR3zZDnBgSsEXMgz0-4+CQhcDOetzXCHZCohIQofqZWwaWVceE-S-lLTEBE9JA8kFytVF8rJCNcGaZ0PezyB-NjswVFagAPWq5cGMpgbIIXimh2Ar8iIJboCHAozgbAifJJhoR6Gp9MBQwLOHzBKFCD6qAgwkq0hOjPpyGkoSGu53ZCKxIk2kbFDaG6EcUUSywgfirywKCMPqCVAmkTVgAk1ZmISJMMIhhAogGmSSWYdH27QklnUd4WwEbkuHX1dBSvLRl+RMFc9tereVCvqWOhbwlUmtWCOMM9CTh6gfzTKONAjjgjuB-gx6m0UJh80whX5fUuxyLC2gN+F3L1h3wljUCQRtsfzASNT5fUtEMIoyt6Sx7lDcGVQtslIDjIkoii66AFNoGljR9ToXleTsxhsCKY0u-cNCloN8G9DtO43AYeaX7DDgH294aPgPmAq7xQQpcQesh1fw9CVRuQwkVMESybCxBBAlYbiWiHRd5Mk4BfuYFWRtRaBa9XkOkBQgchtIMjB9gSMtHoClhPA-wLcKQEPC8aYuQmsBjeGUiB0yMIoOmFPBN9i6UmHkFJDVgKlhO5o5UTkLDHXDIRmjbXg2gtFFj8B3IoWprz4bwivylg7-tYJCHbDbyaKI+FKE2C70waZofOnbFAhvJTRSfflpWOKHFiMaEYq1IN2CEGF-uelCked3lEnQaRUoKTI4zHAQQlgayBkhHDY4hiqxAQa0Y72nFBDjBfFHUn30pFpoEMA+HkHmEFAlw32mUFFjYH2BzFFRY4jgROM5pFlORqFUoSPxx5NkBR6VNQIBUoinARwaKOJlynTD+0yqYpUuJNnzELDCx44rgeyPyH6UIqOTdBtWTKGj8KhIEi-m2RSBZ0mohWN5Hi02CPj3IU3CGDBUaAIhgU-gdUpGOHiGQdQcWMAEPFnh8we45SZILyj2TUEGM9GBqOIiqDep4Y8IOBLlDIDYBugIwLRNeiJyCxK2ik5SaMETbPx3hyaVjmRTWBkNNgbEbFA1HA5ylOUHoJuApPRLaTVJEGdSUGEP4xUqyWkkYIQTxiCA9J05OSo+QUCZBBE8kBaDyBOheR86EMKoHZKUkqTumeFAiraPkSaT7JnkxNng0cpiU-geQDMFAS2CPjEIHyRYH80RxzDsIHkk8RBmxiJTeByU1yc3gqleT8YGUulhYFeR-BjAUxdMFyhkIsRIOxgc2APlyiaF50WibGIMi3B4TLm40zcBgEJgDJ3AcZUHnbCNG0YNkJXRiTDTkzxJbIZgYaUEX0BjSNwk0tXkfwkAzTJp809gItIm7LSFIVgNaYjHGLjQAQdQWQCiBdCOA9pipEaYdNvrcxeYU8HuJW0Eg8w-Ad8QWFdJunRdcgbIFCEFL5Ck9HA8scmiBW6lkFoI+0rQljQBlYhnJp0tyWTlBm8wIZYAKGegGn7iIpIawNqMbG0AogkMxJA7GcDM4L91YP0g6VomMg4A+AlbbmdgD4Dkyq+Lo0GPKLen90Pp3oourEPlEwRxQStdmQO1+lcydYBM5vPzMFkLSKZ53UWQv09ASyPCV4EEJIHWCUc7YCsoLs+WVm2iCIRETHrbKYBCy4ywWc-NQTLDwyvWWYZQaNgwiQxboWM0aTbMIhMA1ZsVB2U7Im5STKBjgcQIzXBCMRWhNCdVOdVsCWyXY1sx6tqSEiVss57ACOS6PzBVBzeXIQLs+0uDk0LAWgIcBlmyIBy-pmcwSFEW4IVlCZEgXOfnNw7dgxQG7Tdui1hIgIuc9QAEKbDzRmYBxdcrRFfBMiY9uZmADuRIMBrihpAVseoF2iTKcgkMiXSgu1wgQw4J5to2eaHOTqzz55ANAUvigenXh+QPwvFgUTOhpY7Gy7PLok2wgZzIxQUEKJWwiohRT5czAUuxw0AWA5wnKDnFLLXEap6mmQFSChBQkac3538vwEfLJwIK85Ws4WZ3OypHhnQ00QlEAjFDJQJoWqB2O7LvT7zHq82boEVA+o1jXsFCqhb-I+HdgNc4lSml2j5CQ4GozKIud2L+BbB4BqlN+XQvJBIKJAQiskAwoMllQ1atGVQIpFdBjVZG9xMwAfQ2bZgXGL830G-PdiexvYibWLJjz6Cp5ugHAawR7C-guYVE4gs+ceVhmZA+sCEYxNiivxNAyKCgTeGsAlCug05rcLRWYt0XeT9F9U2KoYuMUtE-FPsWLNP0kBnAHB-HOBO2wDzLiIei0c3jcQ0X6Q35bCPwLkm4nTAJ2n0MAL4B+hxAJFh+RIDCHLhFBxK5wWnocIQDlw4Y0hN5B4vAhkL-AWSr6EEGKV5KglVZDwNkqKWs1pgpSyQRUm8xVK4Me2WEnUqBDLImlLqRwK0o5nYzEsb8DONxhziaT04mcbOJQBGWA1GkWqecnE35Di9Gm5NGkWsHAiuhwa0vK2ZzNWXbKNllAERWsp2WzJ9lApQ5RsglESE2kFsKuAL1hALl1INy1kYqXXADQHqlbbGFCv8hmLQw+3fSWUoDwihaYZmIoIWBN6DCUWxU5JLE1yiQqf80K3pWTlhXEr4VnsRFR8RWzbiEcs0ejA+32ALRoQ0GHFqznlnGzcoHiDoJW2UlRhKV3QalaTTpYrxbQEfU8LYGEzrMwpSgU6PU20AEtFSPKrwKSokD8qwAgq4VcitGUe0GYV8nkJcHqDOgroDQV6bsxODnhjVLgCsOqQwDwAogO5DBYdC0DtomYjEqGFymSDVMzYdcNcT1lS4Ds-IzqwuOx3ZzzkXQqQVZoKBZYFhDMcFJMvwufIXwO4GAUmQ6usVkwRAdg8NdoEjVBZd6h9fMIWFTEiYZypCxUhfCvg3x01IaoQLkG3FVzzA9cbZnUr7q35G47kNpPNV3Z1qKkCwcDiyzSC4jJQJXZIBAqNxqxaZ-hO5SOOOQAYwAfauEPen7o1QO6b6eCLLDZDMxwIPuVZNXipxXZDi9RWnH2o2bpA+cdCBoXd2Ug8oLS4lKwAjlqDV4zWKxDhIDjPXQ16Mea8uECDkZ8dFI8pMsByGrzL4Y8H6heQKSNXQYTgVMZdsjOUjvSYaCwYhMeAHCga6Un+CDZmqTBGqzQeQBmOtA8LOLDVryDQPEmiRMR0lwbPdkW04LYa-5zrGWJNC9EORn0DQZKLq0flnQnwsEQIloRCKWpwi2mdgJ+v9pHBsg5Pa4ghtHxkF1WjUeyGYFnXzD+WRbHYu0TjTdFViD2M9WQk9ofNFlQ4QutqIyA9oqaboajUqRVLsk+118qASsGTK51BQiq-PKdDyLH0Q6qEzpNZo5r7lbNIIKmWASERspmVUTWIbBhhzsh4O-qFTY4h82p9uKykzAH2vTQxRIIMFKTHUDWRmlB1skO2JDz5DDjYtIKeLSiTCHhV3StmpynkHom2gmgNkQBqR0u4yQOQXtQFN4umydI-NkG48jOGiVDqB6Mka8JupsB5V00FoGcICzS6IUYAyFIKhxSq3mlhSrlOJpSQ8bSBYE94yHmOmm3qV2K2lbrThu7DB45Ka0UuHbBsyUkPQ+8OuFkHLyQR-Ks2wKhWI4pcUNSyWnrWTH5wOpRA7jLIB5GfbmYV4nqSCDEi9RJq51IKGbYqA0ovaQq2EirYGH83-AWRslRyP5hkqqQN+GaE2F6g63qFOkXVOqr1QaryImqZ6zrJTBCYeMPQhde8cXFDgSFY5KIHWpbRs2farGxJSHIy3BjK0Go7HUzBBFXnjUypBOxxEw3DFEjhBUaLTG9XJApb4MAIZ0OizzAD5BsXO30arCjV0JWdaNa4RGI-pkRGFrHRcrfg+nQRC8ns2Sq8lgipjzAMA3XbAw5qo9oG5jRdRzuO3iUhiopGaJiMGzeFd1d4ElFJgaCWbDUEulEszz0HliOG-m1kOhBMR0ljglscmnFEe6tJnkou7xo4iQbKgI9H2o7axwqhnVjJFDWwPBFjV0l-W7CrPTRvS5QN898RCMdPXl0e7sqvqCJCKNOCJw7YXOG0KYHlkC9tgG2FRsgzd1+CbRj1Q3QrrsH9gi0KZK5SjL7HXLS4r7JVKPrz1qM8hXNBBrntd2W1-NlQVwvUGuiWdPZeLAsFtXeSHgsgG+-fXro5IaNleUDWPW3uk6kauWu8T0JqjAWUFJoiMW6NMU35pdfGZAQIIEyXV+pQ+poBRdyGc0JJh8EPTiBXA6bvYumj1dJqUz6bZNdNA4T2sfjOCtQaCVcdYLCBbXZAPFdgGLZ1sLbC4w2RzDTBwkebRldN8qZCJR35ywJC1IoioGKSor7IGtJrM9IevnSsHtqwobMOhGopxJbG9BK0IsBEz8g8xCA9gqojDZ2tSghexnBXENEcGpDhajnKYHnCgiQQ6UfHdnto10HgWIhsVk2iN2SKe8Oh5CGJm-3QhTSkFTIMBSU0rsmc9PLzbQbUPAsNDFrOw2IcNEuHN0hdZ5Pej3QlgWZy-IQzSlLZgZI2uBqzqtFQ0Dqrg8sCaChDExgx7A3kRUUWzDbJHwMDKNI-galUThOuziyAmpBHDpgRMICRI2ahHYEZUiMYMI84ZnKbp3DO8GCAfSUMrJr5lVfw5YcCMHtuMx7AvYxoyIZZNcccByCi0pJ34YasgGbsR1SAQ7it6XFPuFx5i2by4tcG0JuwSZRH4kY2klDHOgiegvB9pWzRKBJ5JlGoeQYgzAWhAxQlNcqkxEzFr2acDBkuyfZqFO72GUVQoZZqkK+m8hkInzKuG1DfZctxArKFWMppoMgo5ew3ScVLu+4qIBgKWk+MtEaBTCQ8XreQMopXaKqPCWwMYyoZC6Amyt+Q3fe9wV3HQljLIGcpUGh5LyyGnrKBVYHMN17MT6jUscrxZNv7EgXoh1KCJoZjZz8YiD2rJEKAMFYIOx9E3sbC6zHjd2hs1ZVxNwKGUQroGGCfnJJVArAqYkClv1t4F90+mfeRNn3d1aHHD39PU8qe5xKBFBqeu9BzmdBSx1gVpnvpgz76Rif87Op06QTCnpaNofxyhmOGX5nkaZtjNnGHvPoGDQxmE38I8dOCHB02+vCTXUvMDscswmwXNi+MFMAmIh6Zo8UCcd5EC1hBhfzfikXLGBJNyzAs8qdIb5aEM2C+41WYn2Z8QgIggQXHodRFYSzLZvnXGeyj8IJzCkYUimfpOVnDxOgxXmWLNp3DNBh4-zdsh8ybls0u9UOIXPk4xxh8mwXs8uePGqTAhs4gkDPqyKJ6H2rqAs0+G3XZBvjZwO2OeYwnVnGTO+o7kue-PbmRQm9XjiBS5Q2hwkMhCuO3z5Bqmxde7RYZmYlPudrGq6TeAenAjzc4zf9OGZ1NMnYK0T8F+IohexPAm7RIQ-zcSWaMwWqOjWugUFjyrAht6KIf4wvhItN6pdUYxng8P80JJLe40WTPKQBGwzA654CQuXjZEljVzrDL8lVtkAAhTJVygcUabHCIgiimqCUZb1pMy8Q27F5YSSL+qH6ftoOzLKBXpFVBi1n0v+pZ05CSWHS+Q-8eQCq1GYyCKYKfAOHGF3azy94YcFasIsWHiL6E78Rma1MOGULHtVnGvFkDly6lU4OguST5Aqwes5Zti0Fe0GXnbRJO4c8hZi5pBgLHo1cuSSllkYYoTMftLMORAHjvz-ZrRFxa248WcrlgaSKPPgItNYr0xcihHzFLVdPxaV1UUsNFPQivx2V8MzF0zFtDU5vlErtyGJ4fSH8uqV0FVeCs-nSLJ468yNbmOfDbI1kGoJV0LAcbNxHe7qxOF5wkolr6VrCVzUcvbnxESlQ+IQYRqPjv9MND0YOBxZwWArSo7wdVed0I7SySOxq7ITypVAm4OUui0YF44-0PCKsRlv5Zo1sSIMekvtYIHjhOg4m+1+rd6dCmjnWZsc0lNmBikOT4pzkvtTXGwW+mqDRrKUPzqHKl1wagCag76AqmOSEpAySefIj7XmIZucV8Vebxk1GAyK9W9iAVb8NwKHlxI46RgGRtl188G2Y3PnQkLjENcoEa6L3mhD-G35xMvGT3GlvKKVby-OK4sHljyo7oeReEAEWWWBzHqGs6W5cDyouoRS5cg66Pkwsso9kVMGapZrfmwAHZtti9Y9zxH-0ImjEapmkAyxe19ejNjJeLfNSNzpbbUqmN1NnDsQb1o+HulRHJM2Q1gascs97dnnx2moM3TYMroZVlzTgsceCU0dHWqA2lKC+OwOipJuGaEG40fAzTIPgNVACV3OzHbEXS3IIGQSPvJn7ScL0UNxWJibHHKW365-gbReYoCWWLpbJDdCHKVnB1wzAXOPA3YF2qmx5ADQNpR0pyXdKDo2puYPGZglmdWURYGGNmfhhrcWoxeNpW8ueWOnNrwENmCNXI0mIEQD0-3VZ0Ssikk7CogdkSoHj4BF7Ylc2A5Eh7-MVanxgBF2sOzrRuVu0DoH3fzACowmuQJiBNiuieHWIsg0OB+aeguAgAA */
id: 'Modeling',
context: ({ input }) => ({
...modelingMachineDefaultContext,
...input,
}),
states: {
idle: {
on: {
'Enter sketch': [
{
target: 'animating to existing sketch',
guard: 'Selection is on face',
},
'Sketch no face',
],
Extrude: {
target: 'Applying extrude',
reenter: true,
},
Revolve: {
target: 'idle',
actions: ['AST revolve'],
reenter: false,
},
Sweep: {
target: 'Applying sweep',
reenter: true,
},
Loft: {
target: 'Applying loft',
reenter: true,
},
Shell: {
target: 'Applying shell',
reenter: true,
},
Fillet: {
target: 'Applying fillet',
reenter: true,
},
Chamfer: {
target: 'Applying chamfer',
reenter: true,
},
'event.parameter.create': {
target: '#Modeling.parameter.creating',
},
'event.parameter.edit': {
target: '#Modeling.parameter.editing',
},
Export: {
target: 'Exporting',
guard: 'Has exportable geometry',
},
Make: {
target: 'Making',
guard: 'Has exportable geometry',
},
'Delete selection': {
target: 'Applying Delete selection',
guard: 'has valid selection for deletion',
reenter: true,
},
'Text-to-CAD': {
target: 'idle',
reenter: false,
actions: ['Submit to Text-to-CAD API'],
},
'Offset plane': {
target: 'Applying offset plane',
reenter: true,
},
Helix: {
target: 'Applying helix',
reenter: true,
},
'Prompt-to-edit': 'Applying Prompt-to-edit',
Appearance: {
target: 'Applying appearance',
reenter: true,
},
},
entry: 'reset client scene mouse handlers',
states: {
hidePlanes: {
on: {
'Artifact graph populated': 'showPlanes',
},
entry: 'hide default planes',
},
showPlanes: {
on: {
'Artifact graph emptied': 'hidePlanes',
},
entry: [
'show default planes',
'reset camera position',
'set selection filter to curves only',
],
description: `We want to disable selections and hover highlights here, because users can't do anything with that information until they actually add something to the scene. The planes are just for orientation here.`,
exit: 'set selection filter to defaults',
},
},
initial: 'hidePlanes',
},
Sketch: {
states: {
SketchIdle: {
on: {
'Make segment vertical': {
guard: 'Can make selection vertical',
target: 'Await constrain vertically',
},
'Make segment horizontal': {
guard: 'Can make selection horizontal',
target: 'Await constrain horizontally',
},
'Constrain horizontal distance': {
target: 'Await horizontal distance info',
guard: 'Can constrain horizontal distance',
},
'Constrain vertical distance': {
target: 'Await vertical distance info',
guard: 'Can constrain vertical distance',
},
'Constrain ABS X': {
target: 'Await ABS X info',
guard: 'Can constrain ABS X',
},
'Constrain ABS Y': {
target: 'Await ABS Y info',
guard: 'Can constrain ABS Y',
},
'Constrain angle': {
target: 'Await angle info',
guard: 'Can constrain angle',
},
'Constrain length': {
target: 'Apply length constraint',
guard: 'Can constrain length',
},
'Constrain perpendicular distance': {
target: 'Await perpendicular distance info',
guard: 'Can constrain perpendicular distance',
},
'Constrain horizontally align': {
guard: 'Can constrain horizontally align',
target: 'Await constrain horizontally align',
},
'Constrain vertically align': {
guard: 'Can constrain vertically align',
target: 'Await constrain vertically align',
},
'Constrain snap to X': {
guard: 'Can constrain snap to X',
target: 'Await constrain snap to X',
},
'Constrain snap to Y': {
guard: 'Can constrain snap to Y',
target: 'Await constrain snap to Y',
},
'Constrain equal length': {
guard: 'Can constrain equal length',
target: 'Await constrain equal length',
},
'Constrain parallel': {
target: 'Await constrain parallel',
guard: 'Can canstrain parallel',
},
'Constrain remove constraints': {
guard: 'Can constrain remove constraints',
target: 'Await constrain remove constraints',
},
'Re-execute': {
target: 'SketchIdle',
reenter: false,
actions: ['set sketchMetadata from pathToNode'],
},
'code edit during sketch': 'clean slate',
'Constrain with named value': {
target: 'Converting to named value',
guard: 'Can convert to named value',
},
'change tool': {
target: 'Change Tool',
reenter: true,
},
},
entry: ['setup client side sketch segments'],
},
'Await horizontal distance info': {
invoke: {
src: 'Get horizontal info',
id: 'get-horizontal-info',
input: ({ context: { selectionRanges, sketchDetails } }) => ({
selectionRanges,
sketchDetails,
}),
onDone: {
target: 'SketchIdle',
actions: 'Set selection',
},
onError: 'SketchIdle',
},
},
'Await vertical distance info': {
invoke: {
src: 'Get vertical info',
id: 'get-vertical-info',
input: ({ context: { selectionRanges, sketchDetails } }) => ({
selectionRanges,
sketchDetails,
}),
onDone: {
target: 'SketchIdle',
actions: 'Set selection',
},
onError: 'SketchIdle',
},
},
'Await ABS X info': {
invoke: {
src: 'Get ABS X info',
id: 'get-abs-x-info',
input: ({ context: { selectionRanges, sketchDetails } }) => ({
selectionRanges,
sketchDetails,
}),
onDone: {
target: 'SketchIdle',
actions: 'Set selection',
},
onError: 'SketchIdle',
},
},
'Await ABS Y info': {
invoke: {
src: 'Get ABS Y info',
id: 'get-abs-y-info',
input: ({ context: { selectionRanges, sketchDetails } }) => ({
selectionRanges,
sketchDetails,
}),
onDone: {
target: 'SketchIdle',
actions: 'Set selection',
},
onError: 'SketchIdle',
},
},
'Await angle info': {
invoke: {
src: 'Get angle info',
id: 'get-angle-info',
input: ({ context: { selectionRanges, sketchDetails } }) => ({
selectionRanges,
sketchDetails,
}),
onDone: {
target: 'SketchIdle',
actions: 'Set selection',
},
onError: 'SketchIdle',
},
},
'Apply length constraint': {
invoke: {
src: 'astConstrainLength',
id: 'AST-constrain-length',
input: ({ context: { selectionRanges, sketchDetails }, event }) => {
const data =
event.type === 'Constrain length' ? event.data : undefined
return {
selectionRanges,
sketchDetails,
lengthValue: data?.length,
}
},
onDone: {
target: 'SketchIdle',
actions: 'Set selection',
},
onError: 'SketchIdle',
},
},
'Await perpendicular distance info': {
invoke: {
src: 'Get perpendicular distance info',
id: 'get-perpendicular-distance-info',
input: ({ context: { selectionRanges, sketchDetails } }) => ({
selectionRanges,
sketchDetails,
}),
onDone: {
target: 'SketchIdle',
actions: 'Set selection',
},
onError: 'SketchIdle',
},
},
'Line tool': {
exit: [],
states: {
Init: {
entry: 'setup noPoints onClick listener',
on: {
'Add start point': {
target: 'normal',
actions: 'set up draft line',
},
Cancel: '#Modeling.Sketch.undo startSketchOn',
},
exit: 'remove draft entities',
},
normal: {
on: {
'Close sketch': {
target: 'Finish profile',
reenter: true,
},
},
},
'Finish profile': {
invoke: {
src: 'setup-client-side-sketch-segments',
id: 'setup-client-side-sketch-segments7',
onDone: 'Init',
onError: 'Init',
input: ({ context: { sketchDetails, selectionRanges } }) => ({
sketchDetails,
selectionRanges,
}),
},
},
},
initial: 'Init',
on: {
'change tool': {
target: 'Change Tool',
reenter: true,
},
},
},
Init: {
always: [
{
target: 'SketchIdle',
guard: 'is editing existing sketch',
},
'Line tool',
],
},
'Tangential arc to': {
on: {
'change tool': {
target: 'Change Tool',
reenter: true,
},
},
states: {
Init: {
on: {
'Continue existing profile': {
target: 'normal',
actions: 'set up draft arc',
},
},
entry: 'setup noPoints onClick listener',
exit: 'remove draft entities',
},
normal: {
on: {
'Close sketch': {
target: 'Finish profile',
reenter: true,
},
},
},
'Finish profile': {
invoke: {
src: 'setup-client-side-sketch-segments',
id: 'setup-client-side-sketch-segments6',
onDone: 'Init',
onError: 'Init',
input: ({ context: { sketchDetails, selectionRanges } }) => ({
sketchDetails,
selectionRanges,
}),
},
},
},
initial: 'Init',
},
'undo startSketchOn': {
invoke: {
src: 'AST-undo-startSketchOn',
id: 'AST-undo-startSketchOn',
input: ({ context: { sketchDetails } }) => ({ sketchDetails }),
onDone: {
target: '#Modeling.idle',
actions: 'enter modeling mode',
},
},
},
'Rectangle tool': {
states: {
'Awaiting second corner': {
on: {
'Finish rectangle': {
target: 'Finished Rectangle',
actions: 'reset deleteIndex',
},
},
},
'Awaiting origin': {
on: {
'click in scene': {
target: 'adding draft rectangle',
reenter: true,
},
},
entry: 'listen for rectangle origin',
},
'Finished Rectangle': {
invoke: {
src: 'setup-client-side-sketch-segments',
id: 'setup-client-side-sketch-segments',
onDone: 'Awaiting origin',
input: ({ context: { sketchDetails, selectionRanges } }) => ({
sketchDetails,
selectionRanges,
}),
},
},
'adding draft rectangle': {
invoke: {
src: 'set-up-draft-rectangle',
id: 'set-up-draft-rectangle',
onDone: {
target: 'Awaiting second corner',
actions: 'update sketchDetails',
},
onError: 'Awaiting origin',
input: ({ context: { sketchDetails }, event }) => {
if (event.type !== 'click in scene')
return {
sketchDetails,
data: [0, 0],
}
return {
sketchDetails,
data: event.data,
}
},
},
},
},
initial: 'Awaiting origin',
on: {
'change tool': {
target: 'Change Tool',
reenter: true,
},
},
},
'Center Rectangle tool': {
states: {
'Awaiting corner': {
on: {
'Finish center rectangle': {
target: 'Finished Center Rectangle',
actions: 'reset deleteIndex',
},
},
},
'Awaiting origin': {
on: {
'Add center rectangle origin': {
target: 'add draft center rectangle',
reenter: true,
},
},
entry: 'listen for center rectangle origin',
},
'Finished Center Rectangle': {
invoke: {
src: 'setup-client-side-sketch-segments',
id: 'setup-client-side-sketch-segments2',
onDone: 'Awaiting origin',
input: ({ context: { sketchDetails, selectionRanges } }) => ({
sketchDetails,
selectionRanges,
}),
},
},
'add draft center rectangle': {
invoke: {
src: 'set-up-draft-center-rectangle',
id: 'set-up-draft-center-rectangle',
onDone: {
target: 'Awaiting corner',
actions: 'update sketchDetails',
},
onError: 'Awaiting origin',
input: ({ context: { sketchDetails }, event }) => {
if (event.type !== 'Add center rectangle origin')
return {
sketchDetails,
data: [0, 0],
}
return {
sketchDetails,
data: event.data,
}
},
},
},
},
initial: 'Awaiting origin',
on: {
'change tool': {
target: 'Change Tool',
reenter: true,
},
},
},
'clean slate': {
always: 'SketchIdle',
entry: 're-eval nodePaths',
},
'Converting to named value': {
invoke: {
src: 'Apply named value constraint',
id: 'astConstrainNamedValue',
input: ({ context: { selectionRanges, sketchDetails }, event }) => {
if (event.type !== 'Constrain with named value') {
return {
selectionRanges,
sketchDetails,
data: undefined,
}
}
return {
selectionRanges,
sketchDetails,
data: event.data,
}
},
onError: 'SketchIdle',
onDone: {
target: 'SketchIdle',
actions: 'Set selection',
},
},
},
'Await constrain remove constraints': {
invoke: {
src: 'do-constrain-remove-constraint',
id: 'do-constrain-remove-constraint',
input: ({ context: { selectionRanges, sketchDetails }, event }) => {
return {
selectionRanges,
sketchDetails,
data:
event.type === 'Constrain remove constraints'
? event.data
: undefined,
}
},
onDone: {
target: 'SketchIdle',
actions: 'Set selection',
},
},
},
'Await constrain horizontally': {
invoke: {
src: 'do-constrain-horizontally',
id: 'do-constrain-horizontally',
input: ({ context: { selectionRanges, sketchDetails } }) => ({
selectionRanges,
sketchDetails,
}),
onDone: {
target: 'SketchIdle',
actions: 'Set selection',
},
},
},
'Await constrain vertically': {
invoke: {
src: 'do-constrain-vertically',
id: 'do-constrain-vertically',
input: ({ context: { selectionRanges, sketchDetails } }) => ({
selectionRanges,
sketchDetails,
}),
onDone: {
target: 'SketchIdle',
actions: 'Set selection',
},
},
},
'Await constrain horizontally align': {
invoke: {
src: 'do-constrain-horizontally-align',
id: 'do-constrain-horizontally-align',
input: ({ context }) => ({
selectionRanges: context.selectionRanges,
sketchDetails: context.sketchDetails,
}),
onDone: {
target: 'SketchIdle',
actions: 'Set selection',
},
},
},
'Await constrain vertically align': {
invoke: {
src: 'do-constrain-vertically-align',
id: 'do-constrain-vertically-align',
input: ({ context }) => ({
selectionRanges: context.selectionRanges,
sketchDetails: context.sketchDetails,
}),
onDone: {
target: 'SketchIdle',
actions: 'Set selection',
},
},
},
'Await constrain snap to X': {
invoke: {
src: 'do-constrain-snap-to-x',
id: 'do-constrain-snap-to-x',
input: ({ context }) => ({
selectionRanges: context.selectionRanges,
sketchDetails: context.sketchDetails,
}),
onDone: {
target: 'SketchIdle',
actions: 'Set selection',
},
},
},
'Await constrain snap to Y': {
invoke: {
src: 'do-constrain-snap-to-y',
id: 'do-constrain-snap-to-y',
input: ({ context }) => ({
selectionRanges: context.selectionRanges,
sketchDetails: context.sketchDetails,
}),
onDone: {
target: 'SketchIdle',
actions: 'Set selection',
},
},
},
'Await constrain equal length': {
invoke: {
src: 'do-constrain-equal-length',
id: 'do-constrain-equal-length',
input: ({ context }) => ({
selectionRanges: context.selectionRanges,
sketchDetails: context.sketchDetails,
}),
onDone: {
target: 'SketchIdle',
actions: 'Set selection',
},
},
},
'Await constrain parallel': {
invoke: {
src: 'do-constrain-parallel',
id: 'do-constrain-parallel',
input: ({ context }) => ({
selectionRanges: context.selectionRanges,
sketchDetails: context.sketchDetails,
}),
onDone: {
target: 'SketchIdle',
actions: 'Set selection',
},
},
},
'Change Tool ifs': {
always: [
{
target: 'SketchIdle',
guard: 'next is none',
},
{
target: 'Line tool',
guard: 'next is line',
},
{
target: 'Rectangle tool',
guard: 'next is rectangle',
},
{
target: 'Tangential arc to',
guard: 'next is tangential arc',
},
{
target: 'Circle tool',
guard: 'next is circle',
},
{
target: 'Center Rectangle tool',
guard: 'next is center rectangle',
},
{
target: 'Circle three point tool',
guard: 'next is circle three point neo',
reenter: true,
},
{
target: 'Arc tool',
guard: 'next is arc',
reenter: true,
},
{
target: 'Arc three point tool',
guard: 'next is arc three point',
reenter: true,
},
],
},
'Circle tool': {
on: {
'change tool': {
target: 'Change Tool',
reenter: true,
},
},
states: {
'Awaiting origin': {
entry: 'listen for circle origin',
on: {
'Add circle origin': {
target: 'adding draft circle',
reenter: true,
},
},
},
'Awaiting Radius': {
on: {
'Finish circle': {
target: 'Finished Circle',
actions: 'reset deleteIndex',
},
},
},
'Finished Circle': {
invoke: {
src: 'setup-client-side-sketch-segments',
id: 'setup-client-side-sketch-segments4',
onDone: 'Awaiting origin',
input: ({ context: { sketchDetails, selectionRanges } }) => ({
sketchDetails,
selectionRanges,
}),
},
},
'adding draft circle': {
invoke: {
src: 'set-up-draft-circle',
id: 'set-up-draft-circle',
onDone: {
target: 'Awaiting Radius',
actions: 'update sketchDetails',
},
onError: 'Awaiting origin',
input: ({ context: { sketchDetails }, event }) => {
if (event.type !== 'Add circle origin')
return {
sketchDetails,
data: [0, 0],
}
return {
sketchDetails,
data: event.data,
}
},
},
},
},
initial: 'Awaiting origin',
},
'Change Tool': {
states: {
'splitting sketch pipe': {
invoke: {
src: 'split-sketch-pipe-if-needed',
id: 'split-sketch-pipe-if-needed',
onDone: {
target: 'setup sketch for tool',
actions: 'update sketchDetails',
},
onError: '#Modeling.Sketch.SketchIdle',
input: ({ context: { sketchDetails } }) => ({
sketchDetails,
}),
},
},
'setup sketch for tool': {
invoke: {
src: 'setup-client-side-sketch-segments',
id: 'setup-client-side-sketch-segments',
onDone: '#Modeling.Sketch.Change Tool ifs',
onError: '#Modeling.Sketch.SketchIdle',
input: ({ context: { sketchDetails, selectionRanges } }) => ({
sketchDetails,
selectionRanges,
}),
},
},
},
initial: 'splitting sketch pipe',
entry: [
'assign tool in context',
'reset selections',
'tear down client sketch',
],
},
'Circle three point tool': {
states: {
'Awaiting first point': {
on: {
'Add first point': 'Awaiting second point',
},
entry: 'listen for circle first point',
},
'Awaiting second point': {
on: {
'Add second point': {
target: 'adding draft circle three point',
actions: 'remove draft entities',
},
},
entry: 'listen for circle second point',
},
'adding draft circle three point': {
invoke: {
src: 'set-up-draft-circle-three-point',
id: 'set-up-draft-circle-three-point',
onDone: {
target: 'Awaiting third point',
actions: 'update sketchDetails',
},
input: ({ context: { sketchDetails }, event }) => {
if (event.type !== 'Add second point')
return {
sketchDetails,
data: { p1: [0, 0], p2: [0, 0] },
}
return {
sketchDetails,
data: event.data,
}
},
},
},
'Awaiting third point': {
on: {
'Finish circle three point': {
target: 'Finished circle three point',
actions: 'reset deleteIndex',
},
},
},
'Finished circle three point': {
invoke: {
src: 'setup-client-side-sketch-segments',
id: 'setup-client-side-sketch-segments5',
onDone: 'Awaiting first point',
input: ({ context: { sketchDetails, selectionRanges } }) => ({
sketchDetails,
selectionRanges,
}),
},
},
},
initial: 'Awaiting first point',
exit: 'remove draft entities',
on: {
'change tool': 'Change Tool',
},
},
'Arc tool': {
states: {
'Awaiting start point': {
on: {
'Add start point': {
target: 'Awaiting for circle center',
actions: 'update sketchDetails arc',
},
},
entry: 'setup noPoints onClick listener',
exit: 'remove draft entities',
},
'Awaiting for circle center': {
entry: ['listen for rectangle origin'],
on: {
'click in scene': 'Adding draft arc',
},
},
'Adding draft arc': {
invoke: {
src: 'set-up-draft-arc',
id: 'set-up-draft-arc',
onDone: {
target: 'Awaiting endAngle',
actions: 'update sketchDetails',
},
input: ({ context: { sketchDetails }, event }) => {
if (event.type !== 'click in scene')
return {
sketchDetails,
data: [0, 0],
}
return {
sketchDetails,
data: event.data,
}
},
},
},
'Awaiting endAngle': {
on: {
'Finish arc': 'Finishing arc',
},
},
'Finishing arc': {
invoke: {
src: 'setup-client-side-sketch-segments',
id: 'setup-client-side-sketch-segments8',
onDone: 'Awaiting start point',
input: ({ context: { sketchDetails, selectionRanges } }) => ({
sketchDetails,
selectionRanges,
}),
},
},
},
initial: 'Awaiting start point',
on: {
'change tool': {
target: 'Change Tool',
reenter: true,
},
},
},
'Arc three point tool': {
states: {
'Awaiting start point': {
on: {
'Add start point': {
target: 'Awaiting for circle center',
actions: 'update sketchDetails arc',
},
},
entry: 'setup noPoints onClick listener',
exit: 'remove draft entities',
},
'Awaiting for circle center': {
on: {
'click in scene': {
target: 'Adding draft arc three point',
actions: 'remove draft entities',
},
},
entry: ['listen for rectangle origin', 'add draft line'],
},
'Adding draft arc three point': {
invoke: {
src: 'set-up-draft-arc-three-point',
id: 'set-up-draft-arc-three-point',
onDone: {
target: 'Awaiting third point',
actions: 'update sketchDetails',
},
input: ({ context: { sketchDetails }, event }) => {
if (event.type !== 'click in scene')
return {
sketchDetails,
data: [0, 0],
}
return {
sketchDetails,
data: event.data,
}
},
},
},
'Awaiting third point': {
on: {
'Finish arc': {
target: 'Finishing arc',
actions: 'reset deleteIndex',
},
'Close sketch': 'Finish profile',
},
},
'Finishing arc': {
invoke: {
src: 'setup-client-side-sketch-segments',
id: 'setup-client-side-sketch-segments9',
onDone: {
target: 'Awaiting for circle center',
reenter: true,
},
input: ({ context: { sketchDetails, selectionRanges } }) => ({
sketchDetails,
selectionRanges,
}),
},
},
'Finish profile': {
invoke: {
src: 'setup-client-side-sketch-segments',
id: 'setup-client-side-sketch-segments10',
onDone: 'Awaiting start point',
input: ({ context: { sketchDetails, selectionRanges } }) => ({
sketchDetails,
selectionRanges,
}),
},
},
},
initial: 'Awaiting start point',
on: {
'change tool': 'Change Tool',
},
exit: 'remove draft entities',
},
},
initial: 'Init',
on: {
CancelSketch: '.SketchIdle',
'Delete segment': {
reenter: false,
actions: ['Delete segment', 'Set sketchDetails', 'reset selections'],
},
'code edit during sketch': '.clean slate',
},
exit: [
'sketch exit execute',
'tear down client sketch',
'remove sketch grid',
'engineToClient cam sync direction',
'Reset Segment Overlays',
'enable copilot',
],
entry: ['add axis n grid', 'clientToEngine cam sync direction'],
},
'Sketch no face': {
entry: [
'disable copilot',
'show default planes',
'set selection filter to faces only',
'enter sketching mode',
],
exit: ['hide default planes', 'set selection filter to defaults'],
on: {
'Select default plane': {
target: 'animating to plane',
actions: ['reset sketch metadata'],
},
},
},
'animating to plane': {
invoke: {
src: 'animate-to-face',
id: 'animate-to-face',
input: ({ event }) => {
if (event.type !== 'Select default plane') return undefined
return event.data
},
onDone: {
target: 'Sketch',
actions: 'set new sketch metadata',
},
onError: 'Sketch no face',
},
},
'animating to existing sketch': {
invoke: {
src: 'animate-to-sketch',
id: 'animate-to-sketch',
input: ({ context }) => ({
selectionRanges: context.selectionRanges,
sketchDetails: context.sketchDetails,
}),
onDone: {
target: 'Sketch',
actions: [
'disable copilot',
'set new sketch metadata',
'enter sketching mode',
],
},
onError: 'idle',
},
},
'Applying extrude': {
invoke: {
src: 'extrudeAstMod',
id: 'extrudeAstMod',
input: ({ event }) => {
if (event.type !== 'Extrude') return undefined
return event.data
},
onDone: ['idle'],
onError: {
target: 'idle',
actions: 'toastError',
},
},
},
'Applying offset plane': {
invoke: {
src: 'offsetPlaneAstMod',
id: 'offsetPlaneAstMod',
input: ({ event }) => {
if (event.type !== 'Offset plane') return undefined
return event.data
},
onDone: ['idle'],
onError: ['idle'],
},
},
'Applying helix': {
invoke: {
src: 'helixAstMod',
id: 'helixAstMod',
input: ({ event }) => {
if (event.type !== 'Helix') return undefined
return event.data
},
onDone: ['idle'],
onError: ['idle'],
},
},
'Applying sweep': {
invoke: {
src: 'sweepAstMod',
id: 'sweepAstMod',
input: ({ event }) => {
if (event.type !== 'Sweep') return undefined
return event.data
},
onDone: ['idle'],
onError: ['idle'],
},
},
'Applying loft': {
invoke: {
src: 'loftAstMod',
id: 'loftAstMod',
input: ({ event }) => {
if (event.type !== 'Loft') return undefined
return event.data
},
onDone: ['idle'],
onError: ['idle'],
},
},
'Applying shell': {
invoke: {
src: 'shellAstMod',
id: 'shellAstMod',
input: ({ event }) => {
if (event.type !== 'Shell') return undefined
return event.data
},
onDone: ['idle'],
onError: ['idle'],
},
},
'Applying fillet': {
invoke: {
src: 'filletAstMod',
id: 'filletAstMod',
input: ({ event }) => {
if (event.type !== 'Fillet') return undefined
return event.data
},
onDone: ['idle'],
onError: ['idle'],
},
},
'Applying chamfer': {
invoke: {
src: 'chamferAstMod',
id: 'chamferAstMod',
input: ({ event }) => {
if (event.type !== 'Chamfer') return undefined
return event.data
},
onDone: ['idle'],
onError: ['idle'],
},
},
parameter: {
type: 'parallel',
states: {
creating: {
invoke: {
src: 'actor.parameter.create',
id: 'actor.parameter.create',
input: ({ event }) => {
if (event.type !== 'event.parameter.create') return undefined
return event.data
},
onDone: ['#Modeling.idle'],
onError: ['#Modeling.idle'],
},
},
editing: {
invoke: {
src: 'actor.parameter.edit',
id: 'actor.parameter.edit',
input: ({ event }) => {
if (event.type !== 'event.parameter.edit') return undefined
return event.data
},
onDone: ['#Modeling.idle'],
onError: ['#Modeling.idle'],
},
},
},
},
'Applying Prompt-to-edit': {
invoke: {
src: 'submit-prompt-edit',
id: 'submit-prompt-edit',
input: ({ event }) => {
if (event.type !== 'Prompt-to-edit' || !event.data) {
return {
prompt: '',
selection: { graphSelections: [], otherSelections: [] },
}
}
return event.data
},
onDone: 'idle',
onError: 'idle',
},
},
'Applying Delete selection': {
invoke: {
src: 'deleteSelectionAstMod',
id: 'deleteSelectionAstMod',
input: ({ event, context }) => {
return { selectionRanges: context.selectionRanges }
},
onDone: 'idle',
onError: {
target: 'idle',
reenter: true,
actions: ({ event }) => {
if ('error' in event && err(event.error)) {
toast.error(event.error.message)
}
},
},
},
},
'Applying appearance': {
invoke: {
src: 'appearanceAstMod',
id: 'appearanceAstMod',
input: ({ event }) => {
if (event.type !== 'Appearance') return undefined
return event.data
},
onDone: ['idle'],
onError: ['idle'],
},
},
Exporting: {
invoke: {
src: 'exportFromEngine',
id: 'exportFromEngine',
input: ({ event }) => {
if (event.type !== 'Export') return undefined
return event.data
},
onDone: ['idle'],
onError: ['idle'],
},
},
Making: {
invoke: {
src: 'makeFromEngine',
id: 'makeFromEngine',
input: ({ event, context }) => {
if (event.type !== 'Make' || !context.machineManager) return undefined
return {
machineManager: context.machineManager,
...event.data,
}
},
onDone: ['idle'],
onError: ['idle'],
},
},
},
initial: 'idle',
on: {
Cancel: {
target: '.idle',
// TODO what if we're existing extrude equipped, should these actions still be fired?
// maybe cancel needs to have a guard for if else logic?
actions: [
'reset sketch metadata',
'enable copilot',
'enter modeling mode',
],
},
'Set selection': {
reenter: false,
actions: 'Set selection',
},
'Set mouse state': {
reenter: false,
actions: 'Set mouse state',
},
'Set context': {
reenter: false,
actions: 'Set context',
},
'Set Segment Overlays': {
reenter: false,
actions: 'Set Segment Overlays',
},
'Center camera on selection': {
reenter: false,
actions: 'Center camera on selection',
},
},
})
export function isEditingExistingSketch({
sketchDetails,
}: {
sketchDetails: SketchDetails | null
}): boolean {
// should check that the variable declaration is a pipeExpression
// and that the pipeExpression contains a "startProfileAt" callExpression
if (!sketchDetails?.sketchEntryNodePath) return false
const variableDeclaration = getNodeFromPath<VariableDeclarator>(
kclManager.ast,
sketchDetails.sketchEntryNodePath,
'VariableDeclarator',
false,
true // suppress noise because we know sketchEntryNodePath might not match up to the ast if the user changed the code
// and is dealt with in `re-eval nodePaths`
)
if (variableDeclaration instanceof Error) return false
if (variableDeclaration.node.type !== 'VariableDeclarator') return false
const maybePipeExpression = variableDeclaration.node.init
if (
(maybePipeExpression.type === 'CallExpression' ||
maybePipeExpression.type === 'CallExpressionKw') &&
(maybePipeExpression.callee.name === 'startProfileAt' ||
maybePipeExpression.callee.name === 'circle' ||
maybePipeExpression.callee.name === 'circleThreePoint')
)
return true
if (maybePipeExpression.type !== 'PipeExpression') return false
const hasStartProfileAt = maybePipeExpression.body.some(
(item) =>
item.type === 'CallExpression' && item.callee.name === 'startProfileAt'
)
const hasCircle =
maybePipeExpression.body.some(
(item) =>
item.type === 'CallExpressionKw' && item.callee.name === 'circle'
) ||
maybePipeExpression.body.some(
(item) =>
item.type === 'CallExpressionKw' &&
item.callee.name === 'circleThreePoint'
)
return (hasStartProfileAt && maybePipeExpression.body.length > 1) || hasCircle
}
export function pipeHasCircle({
sketchDetails,
}: {
sketchDetails: SketchDetails | null
}): boolean {
if (!sketchDetails?.sketchEntryNodePath) return false
const variableDeclaration = getNodeFromPath<VariableDeclarator>(
kclManager.ast,
sketchDetails.sketchEntryNodePath,
'VariableDeclarator'
)
if (err(variableDeclaration)) return false
if (variableDeclaration.node.type !== 'VariableDeclarator') return false
const pipeExpression = variableDeclaration.node.init
if (pipeExpression.type !== 'PipeExpression') return false
const hasCircle = pipeExpression.body.some(
(item) => item.type === 'CallExpressionKw' && item.callee.name === 'circle'
)
return hasCircle
}
export function canRectangleOrCircleTool({
sketchDetails,
}: {
sketchDetails: SketchDetails | null
}): boolean {
const node = getNodeFromPath<VariableDeclaration>(
kclManager.ast,
sketchDetails?.sketchEntryNodePath || [],
'VariableDeclaration'
)
// This should not be returning false, and it should be caught
// but we need to simulate old behavior to move on.
if (err(node)) return false
return node.node?.declaration.init.type !== 'PipeExpression'
}
/** If the sketch contains `close` or `circle` stdlib functions it must be closed */
export function isClosedSketch({
sketchDetails,
}: {
sketchDetails: SketchDetails | null
}): boolean {
const node = getNodeFromPath<VariableDeclaration>(
kclManager.ast,
sketchDetails?.sketchEntryNodePath || [],
'VariableDeclaration'
)
// This should not be returning false, and it should be caught
// but we need to simulate old behavior to move on.
if (err(node)) return false
if (node.node?.declaration?.init?.type !== 'PipeExpression') return false
return node.node.declaration.init.body.some(
(node) =>
(node.type === 'CallExpression' || node.type === 'CallExpressionKw') &&
(node.callee.name === 'close' || node.callee.name === 'circle')
)
}