* Add ability to pick default plane in feature tree in 'Sketch no face' mode * add ability to select deoffset plane where starting a new sketch * use selectDefaultSketchPlane * refactor: remove some duplication * warning cleanups * feature tree items selectable depedngin on no face sketch mode * lint * fix small jump because of border:none when going into and back from 'No face sketch' mode * grey out items other than offset planes in 'No face sketch' mode * start sketching on plane in context menu * sketch on offset plane with context menu * add ability to right click on default plane and start sketch on it * default planes in feature tree should be selectable because of right click context menu * add right click Start sketch option for selected plane on the canvas * selectDefaultSketchPlane returns error now * circular deps * move select functions to lib/selections.ts to avoid circular deps * add test for clicking on feature tree after starting a new sketch * graphite suggestion * fix bug of not being able to create offset plane using another offset plane with command bar * add ability to select default plane on feature when going through the Offset plane command bar flow
5571 lines
182 KiB
TypeScript
5571 lines
182 KiB
TypeScript
import toast from 'react-hot-toast'
|
||
import { Mesh, Vector2, Vector3 } from 'three'
|
||
import { assign, fromPromise, setup } from 'xstate'
|
||
|
||
import type { Node } from '@rust/kcl-lib/bindings/Node'
|
||
|
||
import { deleteSegment } from '@src/clientSideScene/deleteSegment'
|
||
import {
|
||
orthoScale,
|
||
quaternionFromUpNForward,
|
||
} from '@src/clientSideScene/helpers'
|
||
import type { Setting } from '@src/lib/settings/initialSettings'
|
||
import type { CameraProjectionType } from '@rust/kcl-lib/bindings/CameraProjectionType'
|
||
import { DRAFT_DASHED_LINE } from '@src/clientSideScene/sceneConstants'
|
||
import { DRAFT_POINT } from '@src/clientSideScene/sceneUtils'
|
||
import { createProfileStartHandle } from '@src/clientSideScene/segments'
|
||
import type { MachineManager } from '@src/components/MachineManagerProvider'
|
||
import type { ModelingMachineContext } from '@src/components/ModelingMachineProvider'
|
||
import type { SidebarType } from '@src/components/ModelingSidebar/ModelingPanes'
|
||
import {
|
||
applyConstraintEqualAngle,
|
||
equalAngleInfo,
|
||
} from '@src/components/Toolbar/EqualAngle'
|
||
import {
|
||
applyConstraintEqualLength,
|
||
setEqualLengthInfo,
|
||
} from '@src/components/Toolbar/EqualLength'
|
||
import {
|
||
applyConstraintHorzVert,
|
||
horzVertInfo,
|
||
} from '@src/components/Toolbar/HorzVert'
|
||
import { intersectInfo } from '@src/components/Toolbar/Intersect'
|
||
import {
|
||
applyRemoveConstrainingValues,
|
||
removeConstrainingValuesInfo,
|
||
} from '@src/components/Toolbar/RemoveConstrainingValues'
|
||
import {
|
||
absDistanceInfo,
|
||
applyConstraintAxisAlign,
|
||
} from '@src/components/Toolbar/SetAbsDistance'
|
||
import { angleBetweenInfo } from '@src/components/Toolbar/SetAngleBetween'
|
||
import {
|
||
applyConstraintHorzVertAlign,
|
||
horzVertDistanceInfo,
|
||
} from '@src/components/Toolbar/SetHorzVertDistance'
|
||
import { angleLengthInfo } from '@src/components/Toolbar/angleLengthInfo'
|
||
import { createLiteral, createLocalName } from '@src/lang/create'
|
||
import { updateModelingState } from '@src/lang/modelingWorkflows'
|
||
import {
|
||
addClone,
|
||
addHelix,
|
||
addOffsetPlane,
|
||
addShell,
|
||
insertNamedConstant,
|
||
insertVariableAndOffsetPathToNode,
|
||
replaceValueAtNodePath,
|
||
} from '@src/lang/modifyAst'
|
||
import type {
|
||
ChamferParameters,
|
||
FilletParameters,
|
||
} from '@src/lang/modifyAst/addEdgeTreatment'
|
||
import {
|
||
EdgeTreatmentType,
|
||
modifyAstWithEdgeTreatmentAndTag,
|
||
editEdgeTreatment,
|
||
getPathToExtrudeForSegmentSelection,
|
||
mutateAstWithTagForSketchSegment,
|
||
} from '@src/lang/modifyAst/addEdgeTreatment'
|
||
import {
|
||
addExtrude,
|
||
addLoft,
|
||
addRevolve,
|
||
addSweep,
|
||
getAxisExpressionAndIndex,
|
||
} from '@src/lang/modifyAst/addSweep'
|
||
import {
|
||
applyIntersectFromTargetOperatorSelections,
|
||
applySubtractFromTargetOperatorSelections,
|
||
applyUnionFromTargetOperatorSelections,
|
||
} from '@src/lang/modifyAst/boolean'
|
||
import {
|
||
deleteSelectionPromise,
|
||
deletionErrorMessage,
|
||
} from '@src/lang/modifyAst/deleteSelection'
|
||
import { setAppearance } from '@src/lang/modifyAst/setAppearance'
|
||
import {
|
||
setTranslate,
|
||
setRotate,
|
||
insertExpressionNode,
|
||
retrievePathToNodeFromTransformSelection,
|
||
} from '@src/lang/modifyAst/setTransform'
|
||
import {
|
||
getNodeFromPath,
|
||
findPipesWithImportAlias,
|
||
findImportNodeAndAlias,
|
||
isNodeSafeToReplacePath,
|
||
stringifyPathToNode,
|
||
updatePathToNodesAfterEdit,
|
||
valueOrVariable,
|
||
artifactIsPlaneWithPaths,
|
||
isCursorInFunctionDefinition,
|
||
} from '@src/lang/queryAst'
|
||
import {
|
||
getFaceCodeRef,
|
||
getPathsFromArtifact,
|
||
getPathsFromPlaneArtifact,
|
||
getPlaneFromArtifact,
|
||
} from '@src/lang/std/artifactGraph'
|
||
import type { Coords2d } from '@src/lang/std/sketch'
|
||
import type {
|
||
Artifact,
|
||
CallExpressionKw,
|
||
Expr,
|
||
KclValue,
|
||
Literal,
|
||
Name,
|
||
PathToNode,
|
||
PipeExpression,
|
||
Program,
|
||
VariableDeclaration,
|
||
VariableDeclarator,
|
||
} from '@src/lang/wasm'
|
||
import { parse, recast, resultIsOk, sketchFromKclValue } from '@src/lang/wasm'
|
||
import type { ModelingCommandSchema } from '@src/lib/commandBarConfigs/modelingCommandConfig'
|
||
import type { KclCommandValue } from '@src/lib/commandTypes'
|
||
import { EXECUTION_TYPE_REAL } from '@src/lib/constants'
|
||
import type { DefaultPlaneStr } from '@src/lib/planes'
|
||
import type {
|
||
Axis,
|
||
DefaultPlaneSelection,
|
||
Selection,
|
||
Selections,
|
||
} from '@src/lib/selections'
|
||
import { handleSelectionBatch, updateSelections } from '@src/lib/selections'
|
||
import {
|
||
codeManager,
|
||
editorManager,
|
||
engineCommandManager,
|
||
kclManager,
|
||
sceneEntitiesManager,
|
||
sceneInfra,
|
||
} from '@src/lib/singletons'
|
||
import type { ToolbarModeName } from '@src/lib/toolbar'
|
||
import { err, reportRejection, trap } from '@src/lib/trap'
|
||
import { uuidv4 } from '@src/lib/utils'
|
||
import type { ImportStatement } from '@rust/kcl-lib/bindings/ImportStatement'
|
||
import { isDesktop } from '@src/lib/isDesktop'
|
||
import {
|
||
crossProduct,
|
||
isCursorInSketchCommandRange,
|
||
updateSketchDetailsNodePaths,
|
||
} from '@src/lang/util'
|
||
import { kclEditorActor } from '@src/machines/kclEditorMachine'
|
||
import type { Plane } from '@rust/kcl-lib/bindings/Plane'
|
||
import type { Point3d } from '@rust/kcl-lib/bindings/ModelingCmd'
|
||
import { getNodePathFromSourceRange } from '@src/lang/queryAstNodePathUtils'
|
||
import { letEngineAnimateAndSyncCamAfter } from '@src/clientSideScene/CameraControls'
|
||
|
||
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 {
|
||
// there is no artifactGraph in sketch mode, so this is only used as vital information when entering sketch mode
|
||
// or on full/nonMock execution in sketch mode (manual code edit) as the entry point, as it will be accurate in these situations
|
||
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]
|
||
negated: boolean
|
||
}
|
||
|
||
export type SegmentOverlayPayload =
|
||
| {
|
||
type: 'set-one'
|
||
pathToNodeString: string
|
||
seg: SegmentOverlay[]
|
||
}
|
||
| {
|
||
type: 'delete-one'
|
||
pathToNodeString: string
|
||
}
|
||
| { type: 'clear' }
|
||
| {
|
||
type: 'set-many'
|
||
overlays: SegmentOverlays
|
||
}
|
||
|
||
export interface Store {
|
||
videoElement?: HTMLVideoElement
|
||
openPanes: SidebarType[]
|
||
cameraProjection?: Setting<CameraProjectionType>
|
||
}
|
||
|
||
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 sketch plane'
|
||
data: DefaultPlane | ExtrudeFacePlane | OffsetPlane
|
||
}
|
||
| {
|
||
type: 'Set selection'
|
||
data: SetSelections
|
||
}
|
||
| {
|
||
type: 'Delete selection'
|
||
}
|
||
| { type: 'Sketch no face' }
|
||
| { type: 'Cancel'; cleanup?: () => void }
|
||
| {
|
||
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: 'event.parameter.create'
|
||
data: ModelingCommandSchema['event.parameter.create']
|
||
}
|
||
| {
|
||
type: 'event.parameter.edit'
|
||
data: ModelingCommandSchema['event.parameter.edit']
|
||
}
|
||
| { type: 'Export'; data: ModelingCommandSchema['Export'] }
|
||
| {
|
||
type: 'Boolean Subtract'
|
||
data: ModelingCommandSchema['Boolean Subtract']
|
||
}
|
||
| {
|
||
type: 'Boolean Union'
|
||
data: ModelingCommandSchema['Boolean Union']
|
||
}
|
||
| {
|
||
type: 'Boolean Intersect'
|
||
data: ModelingCommandSchema['Boolean Intersect']
|
||
}
|
||
| { 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: 'Prompt-to-edit'; data: ModelingCommandSchema['Prompt-to-edit'] }
|
||
| {
|
||
type: 'Delete selection'
|
||
data: ModelingCommandSchema['Delete selection']
|
||
}
|
||
| { type: 'Appearance'; data: ModelingCommandSchema['Appearance'] }
|
||
| { type: 'Translate'; data: ModelingCommandSchema['Translate'] }
|
||
| { type: 'Rotate'; data: ModelingCommandSchema['Rotate'] }
|
||
| { type: 'Clone'; data: ModelingCommandSchema['Clone'] }
|
||
| {
|
||
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'
|
||
| 'xstate.done.actor.reeval-node-paths'
|
||
|
||
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' }
|
||
| { type: 'Artifact graph initialized' }
|
||
| {
|
||
type: 'Toggle default plane visibility'
|
||
planeId: string
|
||
planeKey: keyof PlaneVisibilityMap
|
||
}
|
||
| {
|
||
type: 'Save default plane visibility'
|
||
planeId: string
|
||
planeKey: keyof PlaneVisibilityMap
|
||
}
|
||
| {
|
||
type: 'Restore default plane visibility'
|
||
}
|
||
|
||
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: isDesktop()
|
||
? (['feature-tree', 'code', 'files'] satisfies Store['openPanes'])
|
||
: (['feature-tree', 'code'] satisfies Store['openPanes']),
|
||
}
|
||
return c
|
||
}
|
||
|
||
export interface ModelingMachineContext {
|
||
currentMode: ToolbarModeName
|
||
currentTool: SketchTool
|
||
toastId: string | null
|
||
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
|
||
defaultPlaneVisibility: PlaneVisibilityMap
|
||
savedDefaultPlaneVisibility: PlaneVisibilityMap
|
||
planesInitialized: boolean
|
||
}
|
||
|
||
export type PlaneVisibilityMap = {
|
||
xy: boolean
|
||
xz: boolean
|
||
yz: boolean
|
||
}
|
||
|
||
export const modelingMachineDefaultContext: ModelingMachineContext = {
|
||
currentMode: 'modeling',
|
||
currentTool: 'none',
|
||
toastId: null,
|
||
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'],
|
||
},
|
||
defaultPlaneVisibility: {
|
||
xy: true,
|
||
xz: true,
|
||
yz: true,
|
||
},
|
||
// Manually toggled plane visibility is saved and restored when going back to modeling mode
|
||
savedDefaultPlaneVisibility: {
|
||
xy: true,
|
||
xz: true,
|
||
yz: true,
|
||
},
|
||
planesInitialized: false,
|
||
}
|
||
|
||
const NO_INPUT_PROVIDED_MESSAGE = 'No input provided'
|
||
|
||
export const modelingMachine = setup({
|
||
types: {
|
||
context: {} as ModelingMachineContext,
|
||
events: {} as ModelingMachineEvent,
|
||
input: {} as ModelingMachineContext,
|
||
},
|
||
guards: {
|
||
'Selection is on face': ({
|
||
context: { selectionRanges },
|
||
event,
|
||
}): boolean => {
|
||
if (event.type !== 'Enter sketch') return false
|
||
if (event.data?.forceNewSketch) return false
|
||
if (artifactIsPlaneWithPaths(selectionRanges)) {
|
||
return true
|
||
} else if (selectionRanges.graphSelections[0]?.artifact) {
|
||
// See if the selection is "close enough" to be coerced to the plane later
|
||
const maybePlane = getPlaneFromArtifact(
|
||
selectionRanges.graphSelections[0].artifact,
|
||
kclManager.artifactGraph
|
||
)
|
||
return !err(maybePlane)
|
||
}
|
||
if (
|
||
isCursorInFunctionDefinition(
|
||
kclManager.ast,
|
||
selectionRanges.graphSelections[0]
|
||
)
|
||
) {
|
||
return false
|
||
}
|
||
return !!isCursorInSketchCommandRange(
|
||
kclManager.artifactGraph,
|
||
selectionRanges
|
||
)
|
||
},
|
||
'Has exportable geometry': () => false,
|
||
'has valid selection for deletion': () => false,
|
||
'is-error-free': (): boolean => {
|
||
return kclManager.errors.length === 0 && !kclManager.hasErrors()
|
||
},
|
||
'no kcl errors': () => {
|
||
return !kclManager.hasErrors()
|
||
},
|
||
'is editing existing sketch': ({ context: { sketchDetails } }) =>
|
||
isEditingExistingSketch({ sketchDetails }),
|
||
'Can make selection horizontal': ({ context: { selectionRanges } }) => {
|
||
const info = horzVertInfo(selectionRanges, 'horizontal')
|
||
if (err(info)) return false
|
||
return info.enabled
|
||
},
|
||
'Can make selection vertical': ({ context: { selectionRanges } }) => {
|
||
const info = horzVertInfo(selectionRanges, 'vertical')
|
||
if (err(info)) return false
|
||
return info.enabled
|
||
},
|
||
'Can constrain horizontal distance': ({ context: { selectionRanges } }) => {
|
||
const info = horzVertDistanceInfo({
|
||
selectionRanges: selectionRanges,
|
||
constraint: 'setHorzDistance',
|
||
})
|
||
if (err(info)) return false
|
||
return info.enabled
|
||
},
|
||
'Can constrain vertical distance': ({ context: { selectionRanges } }) => {
|
||
const info = horzVertDistanceInfo({
|
||
selectionRanges: selectionRanges,
|
||
constraint: 'setVertDistance',
|
||
})
|
||
if (err(info)) return false
|
||
return info.enabled
|
||
},
|
||
'Can constrain ABS X': ({ context: { selectionRanges } }) => {
|
||
const info = absDistanceInfo({
|
||
selectionRanges,
|
||
constraint: 'xAbs',
|
||
})
|
||
if (err(info)) return false
|
||
return info.enabled
|
||
},
|
||
'Can constrain ABS Y': ({ context: { selectionRanges } }) => {
|
||
const info = absDistanceInfo({
|
||
selectionRanges,
|
||
constraint: 'yAbs',
|
||
})
|
||
if (err(info)) return false
|
||
return info.enabled
|
||
},
|
||
'Can constrain angle': ({ context: { selectionRanges } }) => {
|
||
const angleBetween = angleBetweenInfo({
|
||
selectionRanges,
|
||
})
|
||
if (err(angleBetween)) return false
|
||
const angleLength = angleLengthInfo({
|
||
selectionRanges,
|
||
angleOrLength: 'setAngle',
|
||
})
|
||
if (err(angleLength)) return false
|
||
return angleBetween.enabled || angleLength.enabled
|
||
},
|
||
'Can constrain length': ({ context: { selectionRanges } }) => {
|
||
const angleLength = angleLengthInfo({
|
||
selectionRanges,
|
||
})
|
||
if (err(angleLength)) return false
|
||
return angleLength.enabled
|
||
},
|
||
'Can constrain perpendicular distance': ({
|
||
context: { selectionRanges },
|
||
}) => {
|
||
const info = intersectInfo({ selectionRanges })
|
||
if (err(info)) return false
|
||
return info.enabled
|
||
},
|
||
'Can constrain horizontally align': ({ context: { selectionRanges } }) => {
|
||
const info = horzVertDistanceInfo({
|
||
selectionRanges: selectionRanges,
|
||
constraint: 'setHorzDistance',
|
||
})
|
||
if (err(info)) return false
|
||
return info.enabled
|
||
},
|
||
'Can constrain vertically align': ({ context: { selectionRanges } }) => {
|
||
const info = horzVertDistanceInfo({
|
||
selectionRanges: selectionRanges,
|
||
constraint: 'setHorzDistance',
|
||
})
|
||
if (err(info)) return false
|
||
return info.enabled
|
||
},
|
||
'Can constrain snap to X': ({ context: { selectionRanges } }) => {
|
||
const info = absDistanceInfo({
|
||
selectionRanges,
|
||
constraint: 'snapToXAxis',
|
||
})
|
||
if (err(info)) return false
|
||
return info.enabled
|
||
},
|
||
'Can constrain snap to Y': ({ context: { selectionRanges } }) => {
|
||
const info = absDistanceInfo({
|
||
selectionRanges,
|
||
constraint: 'snapToYAxis',
|
||
})
|
||
if (err(info)) return false
|
||
return info.enabled
|
||
},
|
||
'Can constrain equal length': ({ context: { selectionRanges } }) => {
|
||
const info = setEqualLengthInfo({
|
||
selectionRanges,
|
||
})
|
||
if (err(info)) return false
|
||
return info.enabled
|
||
},
|
||
'Can constrain 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 pathToNodes = event.data
|
||
? [event.data]
|
||
: selectionRanges.graphSelections.map(({ codeRef }) => {
|
||
return codeRef.pathToNode
|
||
})
|
||
const info = removeConstrainingValuesInfo(pathToNodes)
|
||
if (err(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) {
|
||
console.error(event.output)
|
||
toast.error(event.output.message)
|
||
} else if ('data' in event && event.data instanceof Error) {
|
||
console.error(event.data)
|
||
toast.error(event.data.message)
|
||
} else if ('error' in event && event.error instanceof Error) {
|
||
console.error(event.error)
|
||
toast.error(event.error.message)
|
||
}
|
||
},
|
||
toastErrorAndExitSketch: ({ event }) => {
|
||
if ('output' in event && event.output instanceof Error) {
|
||
console.error(event.output)
|
||
toast.error(event.output.message)
|
||
} else if ('data' in event && event.data instanceof Error) {
|
||
console.error(event.data)
|
||
toast.error(event.data.message)
|
||
} else if ('error' in event && event.error instanceof Error) {
|
||
console.error(event.error)
|
||
toast.error(event.error.message)
|
||
}
|
||
|
||
// Clean up the THREE.js sketch scene
|
||
sceneEntitiesManager.tearDownSketch({ removeAxis: false })
|
||
sceneEntitiesManager.removeSketchGrid()
|
||
sceneEntitiesManager.resetOverlays()
|
||
},
|
||
'assign tool in context': assign({
|
||
currentTool: ({ event }) =>
|
||
event.type === 'change tool' ? event.data.tool || 'none' : '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': assign({
|
||
defaultPlaneVisibility: () => {
|
||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||
kclManager.hidePlanes()
|
||
return { xy: false, xz: false, yz: false }
|
||
},
|
||
}),
|
||
'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,
|
||
}
|
||
}),
|
||
'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,
|
||
'tangentialArc'
|
||
)
|
||
.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(
|
||
twoD,
|
||
args.intersects,
|
||
args.mouseEvent
|
||
).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' &&
|
||
event.type !== 'xstate.done.actor.reeval-node-paths'
|
||
) {
|
||
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,
|
||
},
|
||
}
|
||
}),
|
||
'show default planes': assign({
|
||
defaultPlaneVisibility: () => {
|
||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||
kclManager.showPlanes()
|
||
return { xy: true, xz: true, yz: true }
|
||
},
|
||
}),
|
||
'show default planes if no errors': assign({
|
||
defaultPlaneVisibility: ({ context }) => {
|
||
if (!kclManager.hasErrors()) {
|
||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||
kclManager.showPlanes()
|
||
return { xy: true, xz: true, yz: true }
|
||
}
|
||
return { ...context.defaultPlaneVisibility }
|
||
},
|
||
}),
|
||
'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.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'
|
||
},
|
||
/** 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)
|
||
})
|
||
},
|
||
'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(
|
||
intersectionPoint.twoD,
|
||
args.intersects,
|
||
args.mouseEvent
|
||
)
|
||
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': assign(
|
||
({ context: { selectionRanges, sketchDetails }, event }) => {
|
||
// this was needed for ts after adding 'Set selection' action to on done modal events
|
||
const setSelections =
|
||
('data' in event &&
|
||
event.data &&
|
||
'selectionType' in event.data &&
|
||
event.data) ||
|
||
('output' in event &&
|
||
event.output &&
|
||
'selectionType' in event.output &&
|
||
event.output) ||
|
||
null
|
||
if (!setSelections) return {}
|
||
|
||
let selections: Selections = {
|
||
graphSelections: [],
|
||
otherSelections: [],
|
||
}
|
||
if (setSelections.selectionType === 'singleCodeCursor') {
|
||
if (!setSelections.selection && editorManager.isShiftDown) {
|
||
// if the user is holding shift, but they didn't select anything
|
||
// don't nuke their other selections (frustrating to have one bad click ruin your
|
||
// whole selection)
|
||
selections = {
|
||
graphSelections: selectionRanges.graphSelections,
|
||
otherSelections: selectionRanges.otherSelections,
|
||
}
|
||
} else if (!setSelections.selection && !editorManager.isShiftDown) {
|
||
selections = {
|
||
graphSelections: [],
|
||
otherSelections: [],
|
||
}
|
||
} else if (setSelections.selection && !editorManager.isShiftDown) {
|
||
selections = {
|
||
graphSelections: [setSelections.selection],
|
||
otherSelections: [],
|
||
}
|
||
} else if (setSelections.selection && editorManager.isShiftDown) {
|
||
// selecting and deselecting multiple objects
|
||
|
||
/**
|
||
* There are two scenarios:
|
||
* 1. General case:
|
||
* When selecting and deselecting edges,
|
||
* faces or segment (during sketch edit)
|
||
* we use its artifact ID to identify the selection
|
||
* 2. Initial sketch setup:
|
||
* The artifact is not yet created
|
||
* so we use the codeRef.range
|
||
*/
|
||
|
||
let updatedSelections: typeof selectionRanges.graphSelections
|
||
|
||
// 1. General case: Artifact exists, use its ID
|
||
if (setSelections.selection.artifact?.id) {
|
||
// check if already selected
|
||
const alreadySelected = selectionRanges.graphSelections.some(
|
||
(selection) =>
|
||
selection.artifact?.id ===
|
||
setSelections.selection?.artifact?.id
|
||
)
|
||
if (alreadySelected && setSelections.selection?.artifact?.id) {
|
||
// remove it
|
||
updatedSelections = selectionRanges.graphSelections.filter(
|
||
(selection) =>
|
||
selection.artifact?.id !==
|
||
setSelections.selection?.artifact?.id
|
||
)
|
||
} else {
|
||
// add it
|
||
updatedSelections = [
|
||
...selectionRanges.graphSelections,
|
||
setSelections.selection,
|
||
]
|
||
}
|
||
} else {
|
||
// 2. Initial sketch setup: Artifact not yet created – use codeRef.range
|
||
const selectionRange = JSON.stringify(
|
||
setSelections.selection?.codeRef?.range
|
||
)
|
||
|
||
// check if already selected
|
||
const alreadySelected = selectionRanges.graphSelections.some(
|
||
(selection) => {
|
||
const existingRange = JSON.stringify(selection.codeRef?.range)
|
||
return existingRange === selectionRange
|
||
}
|
||
)
|
||
|
||
if (alreadySelected && setSelections.selection?.codeRef?.range) {
|
||
// remove it
|
||
updatedSelections = selectionRanges.graphSelections.filter(
|
||
(selection) =>
|
||
JSON.stringify(selection.codeRef?.range) !== selectionRange
|
||
)
|
||
} else {
|
||
// add it
|
||
updatedSelections = [
|
||
...selectionRanges.graphSelections,
|
||
setSelections.selection,
|
||
]
|
||
}
|
||
}
|
||
|
||
selections = {
|
||
graphSelections: updatedSelections,
|
||
otherSelections: selectionRanges.otherSelections,
|
||
}
|
||
}
|
||
|
||
const { engineEvents, codeMirrorSelection, updateSceneObjectColors } =
|
||
handleSelectionBatch({
|
||
selections,
|
||
})
|
||
if (codeMirrorSelection) {
|
||
kclEditorActor.send({
|
||
type: 'setLastSelectionEvent',
|
||
data: {
|
||
codeMirrorSelection,
|
||
scrollIntoView: setSelections.scrollIntoView ?? false,
|
||
},
|
||
})
|
||
}
|
||
|
||
// If there are engine commands that need sent off, send them
|
||
// TODO: This should be handled outside of an action as its own
|
||
// actor, so that the system state is more controlled.
|
||
engineEvents &&
|
||
engineEvents.forEach((event) => {
|
||
engineCommandManager
|
||
.sendSceneCommand(event)
|
||
.catch(reportRejection)
|
||
})
|
||
updateSceneObjectColors()
|
||
|
||
return {
|
||
selectionRanges: selections,
|
||
}
|
||
}
|
||
|
||
if (setSelections.selectionType === 'mirrorCodeMirrorSelections') {
|
||
return {
|
||
selectionRanges: setSelections.selection,
|
||
}
|
||
}
|
||
|
||
if (
|
||
setSelections.selectionType === 'axisSelection' ||
|
||
setSelections.selectionType === 'defaultPlaneSelection'
|
||
) {
|
||
if (editorManager.isShiftDown) {
|
||
selections = {
|
||
graphSelections: selectionRanges.graphSelections,
|
||
otherSelections: [setSelections.selection],
|
||
}
|
||
} else {
|
||
selections = {
|
||
graphSelections: [],
|
||
otherSelections: [setSelections.selection],
|
||
}
|
||
}
|
||
return {
|
||
selectionRanges: selections,
|
||
}
|
||
}
|
||
|
||
if (setSelections.selectionType === 'completeSelection') {
|
||
const codeMirrorSelection = editorManager.createEditorSelection(
|
||
setSelections.selection
|
||
)
|
||
kclEditorActor.send({
|
||
type: 'setLastSelectionEvent',
|
||
data: {
|
||
codeMirrorSelection,
|
||
scrollIntoView: false,
|
||
},
|
||
})
|
||
if (!sketchDetails)
|
||
return {
|
||
selectionRanges: setSelections.selection,
|
||
}
|
||
return {
|
||
selectionRanges: setSelections.selection,
|
||
sketchDetails: {
|
||
...sketchDetails,
|
||
sketchEntryNodePath:
|
||
setSelections.updatedSketchEntryNodePath ||
|
||
sketchDetails?.sketchEntryNodePath ||
|
||
[],
|
||
sketchNodePaths:
|
||
setSelections.updatedSketchNodePaths ||
|
||
sketchDetails?.sketchNodePaths ||
|
||
[],
|
||
planeNodePath:
|
||
setSelections.updatedPlaneNodePath ||
|
||
sketchDetails?.planeNodePath ||
|
||
[],
|
||
},
|
||
}
|
||
}
|
||
|
||
return {}
|
||
}
|
||
),
|
||
'Set mouse state': () => {},
|
||
'Set Segment Overlays': () => {},
|
||
'Center camera on selection': () => {},
|
||
'Submit to Text-to-CAD API': () => {},
|
||
'Set sketchDetails': () => {},
|
||
'debug-action': (data) => {
|
||
console.log('re-eval debug-action', data)
|
||
},
|
||
'Toggle default plane visibility': assign(({ context, event }) => {
|
||
if (event.type !== 'Toggle default plane visibility') return {}
|
||
|
||
const currentVisibilityMap = context.defaultPlaneVisibility
|
||
const currentVisibility = currentVisibilityMap[event.planeKey]
|
||
const newVisibility = !currentVisibility
|
||
|
||
kclManager.engineCommandManager
|
||
.setPlaneHidden(event.planeId, !newVisibility)
|
||
.catch(reportRejection)
|
||
|
||
return {
|
||
defaultPlaneVisibility: {
|
||
...currentVisibilityMap,
|
||
[event.planeKey]: newVisibility,
|
||
},
|
||
}
|
||
}),
|
||
// Saves the default plane visibility to be able to restore when going back from sketch mode
|
||
'Save default plane visibility': assign(({ context, event }) => {
|
||
return {
|
||
savedDefaultPlaneVisibility: {
|
||
...context.defaultPlaneVisibility,
|
||
},
|
||
}
|
||
}),
|
||
'Restore default plane visibility': assign(({ context }) => {
|
||
for (const planeKey of Object.keys(
|
||
context.savedDefaultPlaneVisibility
|
||
) as (keyof PlaneVisibilityMap)[]) {
|
||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||
kclManager.setPlaneVisibilityByKey(
|
||
planeKey,
|
||
context.savedDefaultPlaneVisibility[planeKey]
|
||
)
|
||
}
|
||
|
||
return {
|
||
defaultPlaneVisibility: {
|
||
...context.defaultPlaneVisibility,
|
||
...context.savedDefaultPlaneVisibility,
|
||
},
|
||
}
|
||
}),
|
||
'show sketch error toast': assign(() => {
|
||
// toast message that stays open until closed programmatically
|
||
const toastId = toast.error(
|
||
"Error in kcl script, sketch cannot be drawn until it's fixed",
|
||
{ duration: Infinity }
|
||
)
|
||
return {
|
||
toastId,
|
||
}
|
||
}),
|
||
'remove sketch error toast': assign(({ context }) => {
|
||
if (context.toastId) {
|
||
toast.dismiss(context.toastId)
|
||
return { toastId: null }
|
||
}
|
||
return {}
|
||
}),
|
||
},
|
||
// end actions
|
||
actors: {
|
||
sketchExit: fromPromise(
|
||
async (args: { input: { context: { store: Store } } }) => {
|
||
const store = args.input.context.store
|
||
|
||
// When cancelling the sketch mode we should disable sketch mode within the engine.
|
||
await engineCommandManager.sendSceneCommand({
|
||
type: 'modeling_cmd_req',
|
||
cmd_id: uuidv4(),
|
||
cmd: { type: 'sketch_mode_disable' },
|
||
})
|
||
|
||
sceneInfra.camControls.syncDirection = 'clientToEngine'
|
||
|
||
if (store.cameraProjection?.current === 'perspective') {
|
||
await sceneInfra.camControls.snapToPerspectiveBeforeHandingBackControlToEngine()
|
||
}
|
||
|
||
sceneInfra.camControls.syncDirection = 'engineToClient'
|
||
|
||
// TODO: Re-evaluate if this pause/play logic is needed.
|
||
store.videoElement?.pause()
|
||
|
||
await kclManager
|
||
.executeCode()
|
||
.then(() => {
|
||
if (engineCommandManager.idleMode) return
|
||
|
||
store.videoElement?.play().catch((e: Error) => {
|
||
console.warn('Video playing was prevented', e)
|
||
})
|
||
})
|
||
.catch(reportRejection)
|
||
|
||
sceneEntitiesManager.tearDownSketch({ removeAxis: false })
|
||
sceneEntitiesManager.removeSketchGrid()
|
||
sceneInfra.camControls.syncDirection = 'engineToClient'
|
||
sceneEntitiesManager.resetOverlays()
|
||
}
|
||
),
|
||
/* Below are all the do-constrain sketch actors,
|
||
* which aren't using updateModelingState and don't have the 'no kcl errors' guard yet */
|
||
'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,
|
||
}
|
||
}
|
||
),
|
||
|
||
/* Below are actors being defined in src/components/ModelingMachineProvider.tsx
|
||
* which aren't using updateModelingState and don't have the 'no kcl errors' guard yet */
|
||
'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']
|
||
}
|
||
),
|
||
'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
|
||
}
|
||
),
|
||
'setup-client-side-sketch-segments': fromPromise(
|
||
async ({
|
||
input: { sketchDetails, selectionRanges },
|
||
}: {
|
||
input: {
|
||
sketchDetails: SketchDetails | null
|
||
selectionRanges: Selections
|
||
}
|
||
}) => {
|
||
if (!sketchDetails) return
|
||
if (!sketchDetails.sketchEntryNodePath?.length) return
|
||
sceneInfra.resetMouseListeners()
|
||
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,
|
||
// We will want to pass sketchTools here
|
||
// to add their interactions
|
||
})
|
||
|
||
// We will want to update the context with sketchTools.
|
||
// They'll be used for their .destroy() in tearDownSketch
|
||
return undefined
|
||
}
|
||
),
|
||
'animate-to-sketch': fromPromise(
|
||
async ({
|
||
input: { selectionRanges },
|
||
}: {
|
||
input: {
|
||
selectionRanges: Selections
|
||
}
|
||
}): Promise<ModelingMachineContext['sketchDetails']> => {
|
||
const artifact = selectionRanges.graphSelections[0].artifact
|
||
const plane = getPlaneFromArtifact(artifact, kclManager.artifactGraph)
|
||
if (err(plane)) return Promise.reject(plane)
|
||
// if the user selected a segment, make sure we enter the right sketch as there can be multiple on a plane
|
||
// but still works if the user selected a plane/face by defaulting to the first path
|
||
const mainPath =
|
||
artifact?.type === 'segment' || artifact?.type === 'solid2d'
|
||
? artifact?.pathId
|
||
: plane?.pathIds[0]
|
||
let sketch: KclValue | null = null
|
||
let planeVar: Plane | null = null
|
||
|
||
for (const variable of Object.values(kclManager.execState.variables)) {
|
||
// find programMemory that matches path artifact
|
||
if (
|
||
variable?.type === 'Sketch' &&
|
||
variable.value.artifactId === mainPath
|
||
) {
|
||
sketch = variable
|
||
break
|
||
}
|
||
if (
|
||
// if the variable is an sweep, check if the underlying sketch matches the artifact
|
||
variable?.type === 'Solid' &&
|
||
variable.value.sketch.on.type === 'plane' &&
|
||
variable.value.sketch.artifactId === mainPath
|
||
) {
|
||
sketch = {
|
||
type: 'Sketch',
|
||
value: variable.value.sketch,
|
||
}
|
||
break
|
||
}
|
||
if (variable?.type === 'Plane' && plane.id === variable.value.id) {
|
||
planeVar = variable.value
|
||
}
|
||
}
|
||
|
||
if (!sketch || sketch.type !== 'Sketch') {
|
||
if (artifact?.type !== 'plane')
|
||
return Promise.reject(new Error('No sketch'))
|
||
const planeCodeRef = getFaceCodeRef(artifact)
|
||
if (planeVar && planeCodeRef) {
|
||
const toTuple = (point: Point3d): [number, number, number] => [
|
||
point.x,
|
||
point.y,
|
||
point.z,
|
||
]
|
||
const planPath = getNodePathFromSourceRange(
|
||
kclManager.ast,
|
||
planeCodeRef.range
|
||
)
|
||
await letEngineAnimateAndSyncCamAfter(
|
||
engineCommandManager,
|
||
artifact.id
|
||
)
|
||
const normal = crossProduct(planeVar.xAxis, planeVar.yAxis)
|
||
return {
|
||
sketchEntryNodePath: [],
|
||
planeNodePath: planPath,
|
||
sketchNodePaths: [],
|
||
zAxis: toTuple(normal),
|
||
yAxis: toTuple(planeVar.yAxis),
|
||
origin: toTuple(planeVar.origin),
|
||
}
|
||
}
|
||
return Promise.reject(new Error('No sketch'))
|
||
}
|
||
const info = await sceneEntitiesManager.getSketchOrientationDetails(
|
||
sketch.value
|
||
)
|
||
await letEngineAnimateAndSyncCamAfter(
|
||
engineCommandManager,
|
||
info?.sketchDetails?.faceId || ''
|
||
)
|
||
|
||
const sketchArtifact = kclManager.artifactGraph.get(mainPath)
|
||
if (sketchArtifact?.type !== 'path')
|
||
return Promise.reject(new Error('No sketch artifact'))
|
||
const sketchPaths = getPathsFromArtifact({
|
||
artifact: kclManager.artifactGraph.get(plane.id),
|
||
sketchPathToNode: sketchArtifact?.codeRef?.pathToNode,
|
||
artifactGraph: kclManager.artifactGraph,
|
||
ast: kclManager.ast,
|
||
})
|
||
if (err(sketchPaths)) return Promise.reject(sketchPaths)
|
||
let codeRef = getFaceCodeRef(plane)
|
||
if (!codeRef) return Promise.reject(new Error('No plane codeRef'))
|
||
// codeRef.pathToNode is not always populated correctly
|
||
const planeNodePath = getNodePathFromSourceRange(
|
||
kclManager.ast,
|
||
codeRef.range
|
||
)
|
||
return {
|
||
sketchEntryNodePath: sketchArtifact.codeRef.pathToNode || [],
|
||
sketchNodePaths: sketchPaths,
|
||
planeNodePath,
|
||
zAxis: info.sketchDetails.zAxis || null,
|
||
yAxis: info.sketchDetails.yAxis || null,
|
||
origin: info.sketchDetails.origin.map(
|
||
(a) => a / sceneInfra._baseUnitMultiplier
|
||
) as [number, number, number],
|
||
animateTargetId: info?.sketchDetails?.faceId || '',
|
||
}
|
||
}
|
||
),
|
||
'Apply named value constraint': fromPromise(
|
||
async ({
|
||
input,
|
||
}: {
|
||
input: Pick<
|
||
ModelingMachineContext,
|
||
'sketchDetails' | 'selectionRanges'
|
||
> & {
|
||
data?: ModelingCommandSchema['Constrain with named value']
|
||
}
|
||
}): Promise<SetSelections> => {
|
||
const { selectionRanges, sketchDetails, data } = input
|
||
if (!sketchDetails) {
|
||
return Promise.reject(new Error('No sketch details'))
|
||
}
|
||
if (!data) {
|
||
return Promise.reject(new Error('No data from command flow'))
|
||
}
|
||
let pResult = parse(recast(kclManager.ast))
|
||
if (trap(pResult) || !resultIsOk(pResult))
|
||
return Promise.reject(new Error('Unexpected compilation error'))
|
||
let parsed = pResult.program
|
||
|
||
let result: {
|
||
modifiedAst: Node<Program>
|
||
pathToReplaced: PathToNode | null
|
||
exprInsertIndex: number
|
||
} = {
|
||
modifiedAst: parsed,
|
||
pathToReplaced: null,
|
||
exprInsertIndex: -1,
|
||
}
|
||
// If the user provided a constant name,
|
||
// we need to insert the named constant
|
||
// and then replace the node with the constant's name.
|
||
if ('variableName' in data.namedValue) {
|
||
const astAfterReplacement = replaceValueAtNodePath({
|
||
ast: parsed,
|
||
pathToNode: data.currentValue.pathToNode,
|
||
newExpressionString: data.namedValue.variableName,
|
||
})
|
||
if (trap(astAfterReplacement)) {
|
||
return Promise.reject(astAfterReplacement)
|
||
}
|
||
const parseResultAfterInsertion = parse(
|
||
recast(
|
||
insertNamedConstant({
|
||
node: astAfterReplacement.modifiedAst,
|
||
newExpression: data.namedValue,
|
||
})
|
||
)
|
||
)
|
||
result.exprInsertIndex = data.namedValue.insertIndex
|
||
|
||
if (
|
||
trap(parseResultAfterInsertion) ||
|
||
!resultIsOk(parseResultAfterInsertion)
|
||
)
|
||
return Promise.reject(parseResultAfterInsertion)
|
||
result = {
|
||
modifiedAst: parseResultAfterInsertion.program,
|
||
pathToReplaced: astAfterReplacement.pathToReplaced,
|
||
exprInsertIndex: result.exprInsertIndex,
|
||
}
|
||
} else if ('valueText' in data.namedValue) {
|
||
// If they didn't provide a constant name,
|
||
// just replace the node with the value.
|
||
const astAfterReplacement = replaceValueAtNodePath({
|
||
ast: parsed,
|
||
pathToNode: data.currentValue.pathToNode,
|
||
newExpressionString: data.namedValue.valueText,
|
||
})
|
||
if (trap(astAfterReplacement)) {
|
||
return Promise.reject(astAfterReplacement)
|
||
}
|
||
// The `replacer` function returns a pathToNode that assumes
|
||
// an identifier is also being inserted into the AST, creating an off-by-one error.
|
||
// This corrects that error, but TODO we should fix this upstream
|
||
// to avoid this kind of error in the future.
|
||
astAfterReplacement.pathToReplaced[1][0] =
|
||
(astAfterReplacement.pathToReplaced[1][0] as number) - 1
|
||
result = astAfterReplacement
|
||
}
|
||
|
||
pResult = parse(recast(result.modifiedAst))
|
||
if (trap(pResult) || !resultIsOk(pResult))
|
||
return Promise.reject(new Error('Unexpected compilation error'))
|
||
parsed = pResult.program
|
||
|
||
if (trap(parsed)) return Promise.reject(parsed)
|
||
if (!result.pathToReplaced)
|
||
return Promise.reject(new Error('No path to replaced node'))
|
||
|
||
const {
|
||
updatedSketchEntryNodePath,
|
||
updatedSketchNodePaths,
|
||
updatedPlaneNodePath,
|
||
} = updateSketchDetailsNodePaths({
|
||
sketchEntryNodePath: sketchDetails.sketchEntryNodePath,
|
||
sketchNodePaths: sketchDetails.sketchNodePaths,
|
||
planeNodePath: sketchDetails.planeNodePath,
|
||
exprInsertIndex: result.exprInsertIndex,
|
||
})
|
||
|
||
const updatedAst = await sceneEntitiesManager.updateAstAndRejigSketch(
|
||
updatedSketchEntryNodePath,
|
||
updatedSketchNodePaths,
|
||
updatedPlaneNodePath,
|
||
parsed,
|
||
sketchDetails.zAxis,
|
||
sketchDetails.yAxis,
|
||
sketchDetails.origin
|
||
)
|
||
if (err(updatedAst)) return Promise.reject(updatedAst)
|
||
|
||
await codeManager.updateEditorWithAstAndWriteToFile(updatedAst.newAst)
|
||
|
||
const selection = updateSelections(
|
||
{ 0: result.pathToReplaced },
|
||
selectionRanges,
|
||
updatedAst.newAst
|
||
)
|
||
if (err(selection)) return Promise.reject(selection)
|
||
return {
|
||
selectionType: 'completeSelection',
|
||
selection,
|
||
updatedSketchEntryNodePath,
|
||
updatedSketchNodePaths,
|
||
updatedPlaneNodePath,
|
||
}
|
||
}
|
||
),
|
||
'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
|
||
}
|
||
),
|
||
'split-sketch-pipe-if-needed': fromPromise(
|
||
async (_: { input: Pick<ModelingMachineContext, 'sketchDetails'> }) => {
|
||
return {} as SketchDetailsUpdate
|
||
}
|
||
),
|
||
'submit-prompt-edit': fromPromise(
|
||
async ({
|
||
input,
|
||
}: {
|
||
input: ModelingCommandSchema['Prompt-to-edit']
|
||
}) => {}
|
||
),
|
||
|
||
/* Below are recent modeling codemods that are using updateModelinState,
|
||
* trigger toastError on Error, and have the 'no kcl errors' guard yet */
|
||
extrudeAstMod: fromPromise(
|
||
async ({
|
||
input,
|
||
}: {
|
||
input: ModelingCommandSchema['Extrude'] | undefined
|
||
}) => {
|
||
if (!input) {
|
||
return Promise.reject(new Error(NO_INPUT_PROVIDED_MESSAGE))
|
||
}
|
||
|
||
const {
|
||
nodeToEdit,
|
||
sketches,
|
||
length,
|
||
symmetric,
|
||
bidirectionalLength,
|
||
twistAngle,
|
||
} = input
|
||
const { ast } = kclManager
|
||
const astResult = addExtrude({
|
||
ast,
|
||
sketches,
|
||
length,
|
||
symmetric,
|
||
bidirectionalLength,
|
||
twistAngle,
|
||
nodeToEdit,
|
||
})
|
||
if (err(astResult)) {
|
||
return Promise.reject(new Error("Couldn't add extrude statement"))
|
||
}
|
||
|
||
const { modifiedAst, pathToNode } = astResult
|
||
await updateModelingState(
|
||
modifiedAst,
|
||
EXECUTION_TYPE_REAL,
|
||
{
|
||
kclManager,
|
||
editorManager,
|
||
codeManager,
|
||
},
|
||
{
|
||
focusPath: [pathToNode],
|
||
}
|
||
)
|
||
}
|
||
),
|
||
sweepAstMod: fromPromise(
|
||
async ({
|
||
input,
|
||
}: {
|
||
input: ModelingCommandSchema['Sweep'] | undefined
|
||
}) => {
|
||
if (!input) {
|
||
return Promise.reject(new Error(NO_INPUT_PROVIDED_MESSAGE))
|
||
}
|
||
|
||
const { ast } = kclManager
|
||
const astResult = addSweep({
|
||
...input,
|
||
ast,
|
||
})
|
||
if (err(astResult)) {
|
||
return Promise.reject(astResult)
|
||
}
|
||
|
||
const { modifiedAst, pathToNode } = astResult
|
||
await updateModelingState(
|
||
modifiedAst,
|
||
EXECUTION_TYPE_REAL,
|
||
{
|
||
kclManager,
|
||
editorManager,
|
||
codeManager,
|
||
},
|
||
{
|
||
focusPath: [pathToNode],
|
||
}
|
||
)
|
||
}
|
||
),
|
||
loftAstMod: fromPromise(
|
||
async ({
|
||
input,
|
||
}: {
|
||
input: ModelingCommandSchema['Loft'] | undefined
|
||
}) => {
|
||
if (!input) {
|
||
return Promise.reject(new Error(NO_INPUT_PROVIDED_MESSAGE))
|
||
}
|
||
|
||
const { ast } = kclManager
|
||
const astResult = addLoft({ ast, ...input })
|
||
if (err(astResult)) {
|
||
return Promise.reject(astResult)
|
||
}
|
||
|
||
const { modifiedAst, pathToNode } = astResult
|
||
await updateModelingState(
|
||
modifiedAst,
|
||
EXECUTION_TYPE_REAL,
|
||
{
|
||
kclManager,
|
||
editorManager,
|
||
codeManager,
|
||
},
|
||
{
|
||
focusPath: [pathToNode],
|
||
}
|
||
)
|
||
}
|
||
),
|
||
revolveAstMod: fromPromise(
|
||
async ({
|
||
input,
|
||
}: {
|
||
input: ModelingCommandSchema['Revolve'] | undefined
|
||
}) => {
|
||
if (!input) {
|
||
return Promise.reject(new Error(NO_INPUT_PROVIDED_MESSAGE))
|
||
}
|
||
|
||
const { ast } = kclManager
|
||
const astResult = addRevolve({
|
||
ast,
|
||
...input,
|
||
})
|
||
if (err(astResult)) {
|
||
return Promise.reject(astResult)
|
||
}
|
||
|
||
const { modifiedAst, pathToNode } = astResult
|
||
await updateModelingState(
|
||
modifiedAst,
|
||
EXECUTION_TYPE_REAL,
|
||
{
|
||
kclManager,
|
||
editorManager,
|
||
codeManager,
|
||
},
|
||
{
|
||
focusPath: [pathToNode],
|
||
}
|
||
)
|
||
}
|
||
),
|
||
offsetPlaneAstMod: fromPromise(
|
||
async ({
|
||
input,
|
||
}: {
|
||
input: ModelingCommandSchema['Offset plane'] | undefined
|
||
}) => {
|
||
if (!input) {
|
||
return Promise.reject(new Error(NO_INPUT_PROVIDED_MESSAGE))
|
||
}
|
||
|
||
// 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]
|
||
}
|
||
|
||
const selectedPlane = getSelectedPlane(selection)
|
||
if (!selectedPlane) {
|
||
return trap('No plane selected')
|
||
}
|
||
|
||
// Get the default plane name from the selection
|
||
const offsetPlaneResult = addOffsetPlane({
|
||
node: ast,
|
||
plane: selectedPlane,
|
||
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]++
|
||
}
|
||
|
||
await updateModelingState(
|
||
offsetPlaneResult.modifiedAst,
|
||
EXECUTION_TYPE_REAL,
|
||
{
|
||
kclManager,
|
||
editorManager,
|
||
codeManager,
|
||
},
|
||
{
|
||
focusPath: [offsetPlaneResult.pathToNode],
|
||
}
|
||
)
|
||
}
|
||
),
|
||
helixAstMod: fromPromise(
|
||
async ({
|
||
input,
|
||
}: {
|
||
input: ModelingCommandSchema['Helix'] | undefined
|
||
}) => {
|
||
if (!input) {
|
||
return Promise.reject(new Error(NO_INPUT_PROVIDED_MESSAGE))
|
||
}
|
||
// Extract inputs
|
||
const ast = kclManager.ast
|
||
const {
|
||
mode,
|
||
axis,
|
||
edge,
|
||
cylinder,
|
||
revolutions,
|
||
angleStart,
|
||
ccw,
|
||
radius,
|
||
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]
|
||
}
|
||
|
||
let cylinderDeclarator: VariableDeclarator | undefined
|
||
let axisExpression:
|
||
| Node<CallExpressionKw | Name>
|
||
| Node<Literal>
|
||
| undefined
|
||
|
||
if (mode === 'Cylinder') {
|
||
if (
|
||
!(
|
||
cylinder &&
|
||
cylinder.graphSelections[0] &&
|
||
cylinder.graphSelections[0].artifact?.type === 'wall'
|
||
)
|
||
) {
|
||
return Promise.reject(new Error('Cylinder argument not valid'))
|
||
}
|
||
const clonedAstForGetExtrude = structuredClone(ast)
|
||
const extrudeLookupResult = getPathToExtrudeForSegmentSelection(
|
||
clonedAstForGetExtrude,
|
||
cylinder.graphSelections[0],
|
||
kclManager.artifactGraph
|
||
)
|
||
if (err(extrudeLookupResult)) {
|
||
return Promise.reject(extrudeLookupResult)
|
||
}
|
||
const extrudeNode = getNodeFromPath<VariableDeclaration>(
|
||
ast,
|
||
extrudeLookupResult.pathToExtrudeNode,
|
||
'VariableDeclaration'
|
||
)
|
||
if (err(extrudeNode)) {
|
||
return Promise.reject(extrudeNode)
|
||
}
|
||
cylinderDeclarator = extrudeNode.node.declaration
|
||
} else if (mode === 'Axis' || mode === 'Edge') {
|
||
const getAxisResult = getAxisExpressionAndIndex(mode, axis, edge, ast)
|
||
if (err(getAxisResult)) {
|
||
return Promise.reject(getAxisResult)
|
||
}
|
||
axisExpression = getAxisResult.generatedAxis
|
||
} else {
|
||
return Promise.reject(
|
||
new Error(
|
||
'Generated axis or cylinder declarator selection is missing.'
|
||
)
|
||
)
|
||
}
|
||
|
||
// TODO: figure out if we want to smart insert after the sketch as below
|
||
// *or* after the sweep that consumes the sketch, in which case the below code doesn't work
|
||
// If an axis was selected in KCL, find the max index to insert the revolve command
|
||
// if (axisIndexIfAxis) {
|
||
// opInsertIndex = axisIndexIfAxis + 1
|
||
// }
|
||
|
||
for (const v of [revolutions, angleStart, radius, length]) {
|
||
if (v === undefined) {
|
||
continue
|
||
}
|
||
const variable = v as KclCommandValue
|
||
// Insert the variable if it exists
|
||
if ('variableName' in variable && variable.variableName) {
|
||
const newBody = [...ast.body]
|
||
newBody.splice(
|
||
variable.insertIndex,
|
||
0,
|
||
variable.variableDeclarationAst
|
||
)
|
||
ast.body = newBody
|
||
}
|
||
}
|
||
|
||
const { modifiedAst, pathToNode } = addHelix({
|
||
node: ast,
|
||
revolutions: valueOrVariable(revolutions),
|
||
angleStart: valueOrVariable(angleStart),
|
||
ccw,
|
||
radius: radius ? valueOrVariable(radius) : undefined,
|
||
axis: axisExpression,
|
||
cylinder: cylinderDeclarator,
|
||
length: length ? valueOrVariable(length) : undefined,
|
||
insertIndex: opInsertIndex,
|
||
variableName: opVariableName,
|
||
})
|
||
await updateModelingState(
|
||
modifiedAst,
|
||
EXECUTION_TYPE_REAL,
|
||
{
|
||
kclManager,
|
||
editorManager,
|
||
codeManager,
|
||
},
|
||
{
|
||
focusPath: [pathToNode],
|
||
}
|
||
)
|
||
}
|
||
),
|
||
shellAstMod: fromPromise(
|
||
async ({
|
||
input,
|
||
}: {
|
||
input: ModelingCommandSchema['Shell'] | undefined
|
||
}) => {
|
||
if (!input) {
|
||
return Promise.reject(new Error(NO_INPUT_PROVIDED_MESSAGE))
|
||
}
|
||
|
||
// 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) ||
|
||
variableNode.node.type !== 'VariableDeclaration'
|
||
) {
|
||
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,
|
||
kclManager.artifactGraph
|
||
)
|
||
if (err(extrudeLookupResult)) {
|
||
return Promise.reject(
|
||
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 Promise.reject(
|
||
new Error("Couldn't find segment node from selection", {
|
||
cause: segmentNode,
|
||
})
|
||
)
|
||
}
|
||
|
||
if (extrudeNode.node.declaration.init.type === 'CallExpressionKw') {
|
||
pathToExtrudeNode = extrudeLookupResult.pathToExtrudeNode
|
||
} else if (
|
||
segmentNode.node.declaration.init.type === 'PipeExpression'
|
||
) {
|
||
pathToExtrudeNode = extrudeLookupResult.pathToSegmentNode
|
||
} else {
|
||
return Promise.reject(
|
||
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 Promise.reject(new Error('Bad artifact from selection'))
|
||
}
|
||
|
||
// Check on the selection, and handle the wall vs cap cases
|
||
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 Promise.reject(tagResult)
|
||
}
|
||
|
||
const { tag } = tagResult
|
||
expr = createLocalName(tag)
|
||
} else {
|
||
return Promise.reject(
|
||
new Error('Artifact is neither a cap nor a wall')
|
||
)
|
||
}
|
||
|
||
faces.push(expr)
|
||
}
|
||
|
||
if (!pathToExtrudeNode) {
|
||
return Promise.reject(new Error('No path to extrude node found'))
|
||
}
|
||
|
||
const extrudeNode = getNodeFromPath<VariableDeclarator>(
|
||
ast,
|
||
pathToExtrudeNode,
|
||
'VariableDeclarator'
|
||
)
|
||
if (err(extrudeNode)) {
|
||
return Promise.reject(
|
||
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]++
|
||
}
|
||
|
||
await updateModelingState(
|
||
addResult.modifiedAst,
|
||
EXECUTION_TYPE_REAL,
|
||
{
|
||
kclManager,
|
||
editorManager,
|
||
codeManager,
|
||
},
|
||
{
|
||
focusPath: [addResult.pathToNode],
|
||
}
|
||
)
|
||
}
|
||
),
|
||
filletAstMod: fromPromise(
|
||
async ({
|
||
input,
|
||
}: {
|
||
input: ModelingCommandSchema['Fillet'] | undefined
|
||
}) => {
|
||
if (!input) {
|
||
return Promise.reject(new Error(NO_INPUT_PROVIDED_MESSAGE))
|
||
}
|
||
|
||
// Extract inputs
|
||
const ast = kclManager.ast
|
||
let modifiedAst = structuredClone(ast)
|
||
let focusPath: PathToNode[] = []
|
||
const { nodeToEdit, selection, radius } = input
|
||
|
||
const parameters: FilletParameters = {
|
||
type: EdgeTreatmentType.Fillet,
|
||
radius,
|
||
}
|
||
|
||
const dependencies = {
|
||
kclManager,
|
||
engineCommandManager,
|
||
editorManager,
|
||
codeManager,
|
||
}
|
||
|
||
// Apply or edit fillet
|
||
if (nodeToEdit) {
|
||
// Edit existing fillet
|
||
// selection is not the edge treatment itself,
|
||
// but just the first edge in the fillet expression >
|
||
// we need to find the edgeCut artifact
|
||
// and build a new selection from it
|
||
// TODO: this is a bit of a hack, we should be able
|
||
// to get the edgeCut artifact from the selection
|
||
const firstSelection = selection.graphSelections[0]
|
||
const edgeCutArtifact = Array.from(
|
||
kclManager.artifactGraph.values()
|
||
).find(
|
||
(artifact) =>
|
||
artifact.type === 'edgeCut' &&
|
||
artifact.consumedEdgeId === firstSelection.artifact?.id
|
||
)
|
||
if (!edgeCutArtifact || edgeCutArtifact.type !== 'edgeCut') {
|
||
return Promise.reject(
|
||
new Error(
|
||
'Failed to retrieve edgeCut artifact from sweepEdge selection'
|
||
)
|
||
)
|
||
}
|
||
const edgeTreatmentSelection = {
|
||
artifact: edgeCutArtifact,
|
||
codeRef: edgeCutArtifact.codeRef,
|
||
}
|
||
|
||
const editResult = await editEdgeTreatment(
|
||
ast,
|
||
edgeTreatmentSelection,
|
||
parameters
|
||
)
|
||
if (err(editResult)) return Promise.reject(editResult)
|
||
|
||
modifiedAst = editResult.modifiedAst
|
||
focusPath = [editResult.pathToEdgeTreatmentNode]
|
||
} else {
|
||
// Apply fillet to selection
|
||
const filletResult = await modifyAstWithEdgeTreatmentAndTag(
|
||
ast,
|
||
selection,
|
||
parameters,
|
||
dependencies
|
||
)
|
||
if (err(filletResult)) return Promise.reject(filletResult)
|
||
modifiedAst = filletResult.modifiedAst
|
||
focusPath = filletResult.pathToEdgeTreatmentNode
|
||
}
|
||
|
||
await updateModelingState(
|
||
modifiedAst,
|
||
EXECUTION_TYPE_REAL,
|
||
{
|
||
kclManager,
|
||
editorManager,
|
||
codeManager,
|
||
},
|
||
{
|
||
focusPath: focusPath,
|
||
}
|
||
)
|
||
}
|
||
),
|
||
chamferAstMod: fromPromise(
|
||
async ({
|
||
input,
|
||
}: {
|
||
input: ModelingCommandSchema['Chamfer'] | undefined
|
||
}) => {
|
||
if (!input) {
|
||
return Promise.reject(new Error(NO_INPUT_PROVIDED_MESSAGE))
|
||
}
|
||
|
||
// Extract inputs
|
||
const ast = kclManager.ast
|
||
let modifiedAst = structuredClone(ast)
|
||
let focusPath: PathToNode[] = []
|
||
const { nodeToEdit, selection, length } = input
|
||
|
||
const parameters: ChamferParameters = {
|
||
type: EdgeTreatmentType.Chamfer,
|
||
length,
|
||
}
|
||
const dependencies = {
|
||
kclManager,
|
||
engineCommandManager,
|
||
editorManager,
|
||
codeManager,
|
||
}
|
||
|
||
// Apply or edit chamfer
|
||
if (nodeToEdit) {
|
||
// Edit existing chamfer
|
||
// selection is not the edge treatment itself,
|
||
// but just the first edge in the chamfer expression >
|
||
// we need to find the edgeCut artifact
|
||
// and build a new selection from it
|
||
// TODO: this is a bit of a hack, we should be able
|
||
// to get the edgeCut artifact from the selection
|
||
const firstSelection = selection.graphSelections[0]
|
||
const edgeCutArtifact = Array.from(
|
||
kclManager.artifactGraph.values()
|
||
).find(
|
||
(artifact) =>
|
||
artifact.type === 'edgeCut' &&
|
||
artifact.consumedEdgeId === firstSelection.artifact?.id
|
||
)
|
||
if (!edgeCutArtifact || edgeCutArtifact.type !== 'edgeCut') {
|
||
return Promise.reject(
|
||
new Error(
|
||
'Failed to retrieve edgeCut artifact from sweepEdge selection'
|
||
)
|
||
)
|
||
}
|
||
const edgeTreatmentSelection = {
|
||
artifact: edgeCutArtifact,
|
||
codeRef: edgeCutArtifact.codeRef,
|
||
}
|
||
|
||
const editResult = await editEdgeTreatment(
|
||
ast,
|
||
edgeTreatmentSelection,
|
||
parameters
|
||
)
|
||
if (err(editResult)) return Promise.reject(editResult)
|
||
|
||
modifiedAst = editResult.modifiedAst
|
||
focusPath = [editResult.pathToEdgeTreatmentNode]
|
||
} else {
|
||
// Apply chamfer to selection
|
||
const chamferResult = await modifyAstWithEdgeTreatmentAndTag(
|
||
ast,
|
||
selection,
|
||
parameters,
|
||
dependencies
|
||
)
|
||
if (err(chamferResult)) return Promise.reject(chamferResult)
|
||
modifiedAst = chamferResult.modifiedAst
|
||
focusPath = chamferResult.pathToEdgeTreatmentNode
|
||
}
|
||
|
||
await updateModelingState(
|
||
modifiedAst,
|
||
EXECUTION_TYPE_REAL,
|
||
{
|
||
kclManager,
|
||
editorManager,
|
||
codeManager,
|
||
},
|
||
{
|
||
focusPath: focusPath,
|
||
}
|
||
)
|
||
}
|
||
),
|
||
'actor.parameter.create': fromPromise(
|
||
async ({
|
||
input,
|
||
}: {
|
||
input: ModelingCommandSchema['event.parameter.create'] | undefined
|
||
}) => {
|
||
if (!input) {
|
||
return Promise.reject(new Error(NO_INPUT_PROVIDED_MESSAGE))
|
||
}
|
||
|
||
const { value } = input
|
||
if (!('variableName' in value)) {
|
||
return Promise.reject(new Error('variable name is required'))
|
||
}
|
||
const newAst = insertNamedConstant({
|
||
node: kclManager.ast,
|
||
newExpression: value,
|
||
})
|
||
await updateModelingState(newAst, EXECUTION_TYPE_REAL, {
|
||
kclManager,
|
||
editorManager,
|
||
codeManager,
|
||
})
|
||
}
|
||
),
|
||
'actor.parameter.edit': fromPromise(
|
||
async ({
|
||
input,
|
||
}: {
|
||
input: ModelingCommandSchema['event.parameter.edit'] | undefined
|
||
}) => {
|
||
if (!input) {
|
||
return Promise.reject(new Error(NO_INPUT_PROVIDED_MESSAGE))
|
||
}
|
||
|
||
// 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 Promise.reject(new Error('No variable found, this is a bug'))
|
||
}
|
||
|
||
// Mutate the variable's value
|
||
variableNode.node.init = value.valueAst
|
||
|
||
await updateModelingState(newAst, EXECUTION_TYPE_REAL, {
|
||
codeManager,
|
||
editorManager,
|
||
kclManager,
|
||
})
|
||
}
|
||
),
|
||
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 Promise.reject(new Error(NO_INPUT_PROVIDED_MESSAGE))
|
||
}
|
||
|
||
// Extract inputs
|
||
const ast = kclManager.ast
|
||
const { color, nodeToEdit } = input
|
||
if (!(nodeToEdit && typeof nodeToEdit[1][0] === 'number')) {
|
||
return Promise.reject(new Error('Appearance is only an edit flow'))
|
||
}
|
||
|
||
const result = setAppearance({
|
||
ast,
|
||
nodeToEdit,
|
||
color,
|
||
})
|
||
|
||
if (err(result)) {
|
||
return Promise.reject(err(result))
|
||
}
|
||
|
||
await updateModelingState(
|
||
result.modifiedAst,
|
||
EXECUTION_TYPE_REAL,
|
||
{
|
||
kclManager,
|
||
editorManager,
|
||
codeManager,
|
||
},
|
||
{
|
||
focusPath: [result.pathToNode],
|
||
}
|
||
)
|
||
}
|
||
),
|
||
translateAstMod: fromPromise(
|
||
async ({
|
||
input,
|
||
}: {
|
||
input: ModelingCommandSchema['Translate'] | undefined
|
||
}) => {
|
||
if (!input) {
|
||
return Promise.reject(new Error(NO_INPUT_PROVIDED_MESSAGE))
|
||
}
|
||
|
||
const ast = kclManager.ast
|
||
const modifiedAst = structuredClone(ast)
|
||
const { x, y, z, nodeToEdit, selection } = input
|
||
let pathToNode = nodeToEdit
|
||
if (!(pathToNode && typeof pathToNode[1][0] === 'number')) {
|
||
const result = retrievePathToNodeFromTransformSelection(
|
||
selection,
|
||
kclManager.artifactGraph,
|
||
ast
|
||
)
|
||
if (err(result)) {
|
||
return Promise.reject(result)
|
||
}
|
||
|
||
pathToNode = result
|
||
}
|
||
|
||
// Look for the last pipe with the import alias and a call to translate, with a fallback to rotate.
|
||
// Otherwise create one
|
||
const importNodeAndAlias = findImportNodeAndAlias(ast, pathToNode)
|
||
if (importNodeAndAlias) {
|
||
const pipes = findPipesWithImportAlias(ast, pathToNode, 'translate')
|
||
const lastPipe = pipes.at(-1)
|
||
if (lastPipe && lastPipe.pathToNode) {
|
||
pathToNode = lastPipe.pathToNode
|
||
} else {
|
||
const otherRelevantPipes = findPipesWithImportAlias(
|
||
ast,
|
||
pathToNode,
|
||
'rotate'
|
||
)
|
||
const lastRelevantPipe = otherRelevantPipes.at(-1)
|
||
if (lastRelevantPipe && lastRelevantPipe.pathToNode) {
|
||
pathToNode = lastRelevantPipe.pathToNode
|
||
} else {
|
||
pathToNode = insertExpressionNode(
|
||
modifiedAst,
|
||
importNodeAndAlias.alias
|
||
)
|
||
}
|
||
}
|
||
}
|
||
|
||
insertVariableAndOffsetPathToNode(x, modifiedAst, pathToNode)
|
||
insertVariableAndOffsetPathToNode(y, modifiedAst, pathToNode)
|
||
insertVariableAndOffsetPathToNode(z, modifiedAst, pathToNode)
|
||
const result = setTranslate({
|
||
pathToNode,
|
||
modifiedAst,
|
||
x: valueOrVariable(x),
|
||
y: valueOrVariable(y),
|
||
z: valueOrVariable(z),
|
||
})
|
||
if (err(result)) {
|
||
return Promise.reject(result)
|
||
}
|
||
|
||
await updateModelingState(
|
||
result.modifiedAst,
|
||
EXECUTION_TYPE_REAL,
|
||
{
|
||
kclManager,
|
||
editorManager,
|
||
codeManager,
|
||
},
|
||
{
|
||
focusPath: [result.pathToNode],
|
||
}
|
||
)
|
||
}
|
||
),
|
||
rotateAstMod: fromPromise(
|
||
async ({
|
||
input,
|
||
}: {
|
||
input: ModelingCommandSchema['Rotate'] | undefined
|
||
}) => {
|
||
if (!input) {
|
||
return Promise.reject(new Error(NO_INPUT_PROVIDED_MESSAGE))
|
||
}
|
||
|
||
const ast = kclManager.ast
|
||
const modifiedAst = structuredClone(ast)
|
||
const { roll, pitch, yaw, nodeToEdit, selection } = input
|
||
let pathToNode = nodeToEdit
|
||
if (!(pathToNode && typeof pathToNode[1][0] === 'number')) {
|
||
const result = retrievePathToNodeFromTransformSelection(
|
||
selection,
|
||
kclManager.artifactGraph,
|
||
ast
|
||
)
|
||
if (err(result)) {
|
||
return Promise.reject(result)
|
||
}
|
||
|
||
pathToNode = result
|
||
}
|
||
|
||
// Look for the last pipe with the import alias and a call to rotate, with a fallback to translate.
|
||
// Otherwise create one
|
||
const importNodeAndAlias = findImportNodeAndAlias(ast, pathToNode)
|
||
if (importNodeAndAlias) {
|
||
const pipes = findPipesWithImportAlias(ast, pathToNode, 'rotate')
|
||
const lastPipe = pipes.at(-1)
|
||
if (lastPipe && lastPipe.pathToNode) {
|
||
pathToNode = lastPipe.pathToNode
|
||
} else {
|
||
const otherRelevantPipes = findPipesWithImportAlias(
|
||
ast,
|
||
pathToNode,
|
||
'translate'
|
||
)
|
||
const lastRelevantPipe = otherRelevantPipes.at(-1)
|
||
if (lastRelevantPipe && lastRelevantPipe.pathToNode) {
|
||
pathToNode = lastRelevantPipe.pathToNode
|
||
} else {
|
||
pathToNode = insertExpressionNode(
|
||
modifiedAst,
|
||
importNodeAndAlias.alias
|
||
)
|
||
}
|
||
}
|
||
}
|
||
|
||
insertVariableAndOffsetPathToNode(roll, modifiedAst, pathToNode)
|
||
insertVariableAndOffsetPathToNode(pitch, modifiedAst, pathToNode)
|
||
insertVariableAndOffsetPathToNode(yaw, modifiedAst, pathToNode)
|
||
const result = setRotate({
|
||
pathToNode,
|
||
modifiedAst,
|
||
roll: valueOrVariable(roll),
|
||
pitch: valueOrVariable(pitch),
|
||
yaw: valueOrVariable(yaw),
|
||
})
|
||
if (err(result)) {
|
||
return Promise.reject(result)
|
||
}
|
||
|
||
await updateModelingState(
|
||
result.modifiedAst,
|
||
EXECUTION_TYPE_REAL,
|
||
{
|
||
kclManager,
|
||
editorManager,
|
||
codeManager,
|
||
},
|
||
{
|
||
focusPath: [result.pathToNode],
|
||
}
|
||
)
|
||
}
|
||
),
|
||
cloneAstMod: fromPromise(
|
||
async ({
|
||
input,
|
||
}: {
|
||
input: ModelingCommandSchema['Clone'] | undefined
|
||
}) => {
|
||
if (!input) {
|
||
return Promise.reject(new Error(NO_INPUT_PROVIDED_MESSAGE))
|
||
}
|
||
|
||
const ast = kclManager.ast
|
||
const { nodeToEdit, selection, variableName } = input
|
||
let pathToNode = nodeToEdit
|
||
if (!(pathToNode && typeof pathToNode[1][0] === 'number')) {
|
||
const result = retrievePathToNodeFromTransformSelection(
|
||
selection,
|
||
kclManager.artifactGraph,
|
||
ast
|
||
)
|
||
if (err(result)) {
|
||
return Promise.reject(result)
|
||
}
|
||
|
||
pathToNode = result
|
||
}
|
||
|
||
const returnEarly = true
|
||
const geometryNode = getNodeFromPath<
|
||
VariableDeclaration | ImportStatement | PipeExpression
|
||
>(
|
||
ast,
|
||
pathToNode,
|
||
['VariableDeclaration', 'ImportStatement', 'PipeExpression'],
|
||
returnEarly
|
||
)
|
||
if (err(geometryNode)) {
|
||
return Promise.reject(
|
||
new Error("Couldn't find corresponding path to node")
|
||
)
|
||
}
|
||
|
||
let geometryName: string | undefined
|
||
if (geometryNode.node.type === 'VariableDeclaration') {
|
||
geometryName = geometryNode.node.declaration.id.name
|
||
} else if (
|
||
geometryNode.node.type === 'ImportStatement' &&
|
||
geometryNode.node.selector.type === 'None' &&
|
||
geometryNode.node.selector.alias
|
||
) {
|
||
geometryName = geometryNode.node.selector.alias?.name
|
||
} else {
|
||
return Promise.reject(
|
||
new Error("Couldn't find corresponding geometry")
|
||
)
|
||
}
|
||
|
||
const result = addClone({
|
||
ast,
|
||
geometryName,
|
||
variableName,
|
||
})
|
||
if (err(result)) {
|
||
return Promise.reject(err(result))
|
||
}
|
||
|
||
await updateModelingState(
|
||
result.modifiedAst,
|
||
EXECUTION_TYPE_REAL,
|
||
{
|
||
kclManager,
|
||
editorManager,
|
||
codeManager,
|
||
},
|
||
{
|
||
focusPath: [result.pathToNode],
|
||
}
|
||
)
|
||
}
|
||
),
|
||
exportFromEngine: fromPromise(
|
||
async ({}: { input?: ModelingCommandSchema['Export'] }) => {
|
||
return undefined as Error | undefined
|
||
}
|
||
),
|
||
makeFromEngine: fromPromise(
|
||
async ({}: {
|
||
input?: {
|
||
machineManager: MachineManager
|
||
} & ModelingCommandSchema['Make']
|
||
}) => {
|
||
return undefined as Error | undefined
|
||
}
|
||
),
|
||
boolSubtractAstMod: fromPromise(
|
||
async ({
|
||
input,
|
||
}: {
|
||
input: ModelingCommandSchema['Boolean Subtract'] | undefined
|
||
}) => {
|
||
if (!input) {
|
||
return Promise.reject(new Error(NO_INPUT_PROVIDED_MESSAGE))
|
||
}
|
||
|
||
const { solids, tools } = input
|
||
if (
|
||
!solids.graphSelections.some((selection) => selection.artifact) ||
|
||
!tools.graphSelections.some((selection) => selection.artifact)
|
||
) {
|
||
return Promise.reject(new Error('No artifact in selections found'))
|
||
}
|
||
|
||
const result = await applySubtractFromTargetOperatorSelections(
|
||
solids,
|
||
tools,
|
||
{
|
||
kclManager,
|
||
codeManager,
|
||
engineCommandManager,
|
||
editorManager,
|
||
}
|
||
)
|
||
if (err(result)) {
|
||
return Promise.reject(result)
|
||
}
|
||
}
|
||
),
|
||
boolUnionAstMod: fromPromise(
|
||
async ({
|
||
input,
|
||
}: {
|
||
input: ModelingCommandSchema['Boolean Union'] | undefined
|
||
}) => {
|
||
if (!input) {
|
||
return Promise.reject(new Error(NO_INPUT_PROVIDED_MESSAGE))
|
||
}
|
||
|
||
const { solids } = input
|
||
if (!solids.graphSelections[0].artifact) {
|
||
return Promise.reject(new Error('No artifact in selections found'))
|
||
}
|
||
|
||
const result = await applyUnionFromTargetOperatorSelections(solids, {
|
||
kclManager,
|
||
codeManager,
|
||
engineCommandManager,
|
||
editorManager,
|
||
})
|
||
if (err(result)) {
|
||
return Promise.reject(result)
|
||
}
|
||
}
|
||
),
|
||
boolIntersectAstMod: fromPromise(
|
||
async ({
|
||
input,
|
||
}: {
|
||
input: ModelingCommandSchema['Boolean Union'] | undefined
|
||
}) => {
|
||
if (!input) {
|
||
return Promise.reject(new Error(NO_INPUT_PROVIDED_MESSAGE))
|
||
}
|
||
|
||
const { solids } = input
|
||
if (!solids.graphSelections[0].artifact) {
|
||
return Promise.reject(new Error('No artifact in selections found'))
|
||
}
|
||
|
||
const result = await applyIntersectFromTargetOperatorSelections(
|
||
solids,
|
||
{
|
||
kclManager,
|
||
codeManager,
|
||
engineCommandManager,
|
||
editorManager,
|
||
}
|
||
)
|
||
if (err(result)) {
|
||
return Promise.reject(result)
|
||
}
|
||
}
|
||
),
|
||
|
||
/* Pierre: looks like somewhat of a one-off */
|
||
'reeval-node-paths': fromPromise(
|
||
async ({
|
||
input: { sketchDetails },
|
||
}: {
|
||
input: Pick<ModelingMachineContext, 'sketchDetails'>
|
||
}) => {
|
||
const errorMessage =
|
||
'Unable to maintain sketch mode - code changes affected sketch references. Please re-enter.'
|
||
if (!sketchDetails) {
|
||
return Promise.reject(new Error(errorMessage))
|
||
}
|
||
|
||
// hasErrors is for parse errors, errors is for runtime errors
|
||
if (kclManager.errors.length > 0 || kclManager.hasErrors()) {
|
||
// if there's an error in the execution, we don't actually want to disable sketch mode
|
||
// instead we'll give the user the chance to fix their error
|
||
return {
|
||
updatedEntryNodePath: sketchDetails.sketchEntryNodePath,
|
||
updatedSketchNodePaths: sketchDetails.sketchNodePaths,
|
||
updatedPlaneNodePath: sketchDetails.planeNodePath,
|
||
}
|
||
}
|
||
|
||
const updatedPlaneNodePath = updatePathToNodesAfterEdit(
|
||
kclManager._lastAst,
|
||
kclManager.ast,
|
||
sketchDetails.planeNodePath
|
||
)
|
||
|
||
if (err(updatedPlaneNodePath)) {
|
||
return Promise.reject(new Error(errorMessage))
|
||
}
|
||
const maybePlaneArtifact = [...kclManager.artifactGraph.values()].find(
|
||
(artifact) => {
|
||
const codeRef = getFaceCodeRef(artifact)
|
||
if (!codeRef) return false
|
||
|
||
return (
|
||
stringifyPathToNode(codeRef.pathToNode) ===
|
||
stringifyPathToNode(updatedPlaneNodePath)
|
||
)
|
||
}
|
||
)
|
||
if (
|
||
!maybePlaneArtifact ||
|
||
(maybePlaneArtifact.type !== 'plane' &&
|
||
maybePlaneArtifact.type !== 'startSketchOnFace')
|
||
) {
|
||
return Promise.reject(new Error(errorMessage))
|
||
}
|
||
let planeArtifact: Artifact | undefined
|
||
if (maybePlaneArtifact.type === 'plane') {
|
||
planeArtifact = maybePlaneArtifact
|
||
} else {
|
||
const face = kclManager.artifactGraph.get(maybePlaneArtifact.faceId)
|
||
if (face) {
|
||
planeArtifact = face
|
||
}
|
||
}
|
||
if (
|
||
!planeArtifact ||
|
||
(planeArtifact.type !== 'cap' &&
|
||
planeArtifact.type !== 'wall' &&
|
||
planeArtifact.type !== 'plane')
|
||
) {
|
||
return Promise.reject(new Error(errorMessage))
|
||
}
|
||
|
||
const newPaths = getPathsFromPlaneArtifact(
|
||
planeArtifact,
|
||
kclManager.artifactGraph,
|
||
kclManager.ast
|
||
)
|
||
|
||
return {
|
||
updatedEntryNodePath: newPaths[0],
|
||
updatedSketchNodePaths: newPaths,
|
||
updatedPlaneNodePath,
|
||
}
|
||
}
|
||
),
|
||
},
|
||
// end actors
|
||
}).createMachine({
|
||
/** @xstate-layout N4IgpgJg5mDOIC5QFkD2EwBsCWA7KAxAMICGuAxlgNoAMAuoqAA6qzYAu2qujIAHogC0ANmEBmAHQBGAJwBWABwKpSqTQDsUsQBoQAT0QAmdQBYJYmmPVjDh4YsMKZAX2e60GHPgIBlMOwACWCwwck5uWgYkEBY2cJ5ogQRBNRkzOUsFOTFFGhNZQ10DBBkZCXU7Szl5KRMTWxNXd3QsPEI-QIBbVABXYKD2EnYwSN5Yji4E0CSUqSlDCQUTGRpZefV1GWF1IsQxbYkTOS0FDRo5dRpDKTkmkA9W7w6A8m5hvnZR6PH43hnU4QSVYWbYyMQKdQKRy7BDCfLSYTXQzHUQmJQuNz3Fpedr+AJ+KCdMC4QIAeQAbmAAE6YEh6WBfZisCbcP5COaKCTydQXHkydRwjYw44LSz2YQaWw5ex3B444jE4ZUl4kIlUkgBbhBEJhSaMmLM36JdlSSHmVbGZY5NLCBQw040CSImjKUrZFSy7FtAgAFVQUCgmDAAQwADMSD1MIEmLTcMHydg2AAjbA4dh6fU-SZs5LzNGHGgSmTzCG1Wow+SO11bMQmHKVRqYuVtCTYCBBggAURJ1KCAGt-OQABaZw3Z43JZFSQ5g4RqFSGZbImEVMqQyEQyFw+a3Jte-Ct9tgLs95WwAfsYdUKRRJlxcfTIQNcpQ23XflKeyFfSIUTTuQAXMcIXDIThSJ6ngtm2Hadh8VI9Bgo73qyE4iDQjqbLUpiFhYVgmDChhWJIjhXBcci2ks5EQY8UCHh2PgAO5gGATBISyUz8EI1TqEC2zZHUO7GN+xTXPsBaLjYeQSiYArUTidHHgAMqgoafPQYxjihj7JIogJpFOciGDI1zbDCC6SC6qwmNstaKGiclQUeBAAEpgOSqCYJSbFGtpgiaDxOSaLpwjFlYwgwmIpQKE65E2P5FS1A5B7QcepKhqGwTRrGIzqd8mkcTMc48VcOFgo4NwyGZEqSJW1VOBkhHqEltEpQQAASrR8N5D6cTpKxOjQxZZMYczzARGSOuh5wXFo9SGM1Cm+EOWCYN1Wm9YI1SOsYhbLLUUIqOFP4IKa1zlHk1lOJs2zgXukHJU5ABiqZBmpt4GshBVcf16EZLIlxzcux2GKscgSNUshzvYAFWAtrVEEOqqhtSa1fck1XlJc4gCqaxm2gRi5g-IUViu6813TRi1uYqEhMCQ6pEkqEjkFSYBDDl71ZutMyYeDRk8mIppqMcdrHYLtrg6WSwqJY4Jw051MkrT9Oqv41ISJAHCozmfmReUxg5CLdaEUdxS1jyhxLJsKyrF+8swXwLBUm9GmfTr1gLFktpSCF-IaKLZsXeDxZ5EZIMQrW9vHsgJADtrqHWJIpzWhctoCkDIlgtOXvglowFQo2zT3S1TkACIhMM2pBrqES5Xe7E61kkg+37pRwkZeQEfY-6mvURympcthR76YAfII7CoIIRAAIKl-H2liILEiOORBfrMc+HHaB06zqB1jHJkGJF5TrUAApUqgnRMOw4+T5rLt5W7qGIjxcwCasVSUWZxaAk4Jig4vS6R8sTF0WtPJgTA2bqgoBzV2Ddn7nHKIvDYZUVj2B0MDTY0VawulMIvSwt1j7yVaj6aBsBaTDHnhtContkTFmMjJOoCgMHFHyKYRYs5kTImqDYYezlUCDEoXXD68DfKaAWLaQyEJyKbGslIGE9QliSxBoRQaAoXTDyIJgbgsDH6iOoYNLk-IeRwlAnkOQm9WF1h4ksFe-cLClGHgAIVQB5NmuB8Q9CTOwdUYQqE8zsIcEKdlrHyFNnsIs5gLFgjqBoXOYhnGuKDGQAIABVXAephFczRn5fMWQFCIgYY4fIEUKjRSyBY7INRbSJLcSkgAkqeYIfisn5XdtULkmgbAgykmiCKSgeL-0XIDRehCQEnyPBIIcbYwCn2yrAAg09nbYHDGEAIUB1RMCHAEFgTBIzswgP4oQmgzClGsqsGhyh9hmWRGYTQh1TC2ElIXcZxDJmwCHKgBicyyBwEWcs1ZgQNkkC2QEMAV9OCQCObmYqYJIpQnQisVOOxjpqHYecOo2wQqiBBmM5sB4fAXmHMQMglBVqtKftpPOZQtgFPWPyOaEU6yHEhLcpcIMNgLUJYOIcJKYGYG5ZeEcFL9FJFqGdNEAN-JXDSCwxAag9bLGqH+NE3JdxEJbIK4l5dXrBmCISRU0KljEUhDYX2TDwQRWnOhFQQU4Tgm2DJLlRLeWvAwGCiAHAQw9CpG0fsPLoU3E0ODXOLobiBR5CuTYHDgQ+zmKsfkzqeXEG4LAHxJA8ABAYhwbZuBVYQACOSEgmAei6Prj5XqcwNhckGuK3SS5Kpi2kgiUw1l8hBvVa8zVLqJBaqHPUpyMcBzagNSSQt1JODkGLYGwydyQkXAsDJRcZkBRmDmDkSK+xcGCyTUK3tLqB0diHXqsAo7AifN9QALzeNOkVFaxUZGiioeoBDqhIgDvKnFUTjI3COMWI4u7hz7p5Ye48RBU3pszRe7A16STFpDImQYMDA2SqdLjf2Vz7UEVML-EKVlHAVEsIBocwGhWgZTbgNN6pM2UmWVOzACG02krLSI+98qGp8yWMsNOhl+Rd0XOYMKoEJTZAAi8-FtE+2keHOR8DlHIMeOnk4nwAQAAaM7ZBAi0HMfmGwrAfoQPgsGGwZXggsThBJFN5JSb7bJiD1HFPKYCAATQ0+UgeInETzhRWbOsZQqmFgsfsZh4n9ySZ7bZpycmqMZo8WQQMLHsk5iDXpao9QcXWUJgRfJQJCPFjsuiJqVnu08uk-2qL9nYsBCDPgdgwrOZtInEGx0aR5gaFNBCCU4SEBGRCsorOGww7bGI2Vuz8mHM7OpJA3AnryD7OVJ6pjyG709TFZl3Le0LrMMLPIsWPswbZFrByQy2QeQjcix2aLCmAjQdg4MTAmA9ABGLdgKAuAUNbEODg5hkUVAizMgU1+g19g3AAug87B6Kvjaq7RydxbHvPZwG9jTFkXQyRMcUjQ38BQFlGVONFRWNUEoi5Dy7lXM2wDzUwAIE81MzusosSK-9B6XSOARTdQJfunDrJFcQEOQNQ5ixTqnNPUAufp4Cbu+XTXRO64uUC5hESEQZQBCx5MifhdKxdsD5OPFgAAI49HgzVqAdWZ3GCBCsKwGwUSlGEkYMNy86xXKUNZao-OyOC+u3TdUD3qAre5uxiUA1bA+2MDNAUBMFf1HrPyONmwPcya9xN1m3RKQvF1+wBkAe0b7QWCoU4QWQSZAIiobOi7ax2CrXCRP5WOxuuDPfb1vr8D+qFTO4N2RlBhtEzb+0-9LZLHQuCLIQba-keHPF4ME8PLQqMqUcGnmuGL3tPMLkfEsb7AdcAiTZXRvvLxD0an+qiQkgWRAHRrZcDuQHBITKR-BDkBwIqQQbAMCv5da-09p+s9iEDZWLkRQQjLIQsG0MyRdcwLYQaGSUQcRYQcfA-QII-EdH-c-S-PAG-MAO-fwB-J-bAF-N-MAD-HlL-M9WAP-G8OBNjE6NIaKI4QyRcWQZQOoSxeVKwR0POLGK0VdQnLtYnLXUnLAzKAIZAk-RUBZakC+KkWmChUMVAKkTobA9gXA5-EkV-GZYgoVUg1Av-HPd2dCPmH2awayAUJwRtYoHkMwCwawOqbhYwEbaeBiDNc9eQmDG9BjRbJDSgAIPAOQggC-OMK-TAiQGAG+W7dwwQXw1AOfLvIEJ5DQLcGPbDS4J0UQReX2Yw4bYrfgvdRw5wm7Vwu7eDTw5jHw3APwyQ+QmQoYOQhQkI-wQQcIuDTASI8o6IvQicZEcEJ0G2YHYsfebDGwRYCxN+GWUfBwpwr1WHbAejRjLw4MKI-w9A6-VAW-UIwQaY+jVouQmIo4coWaNHPOcEOVHrcQMwCiAUZVeYRECY-IzY4oxDUoxYyo6QmMGo+QxQ9Y+4loqI3YwEeJC4WwWwLIHzIwN9TpYsMPH2fTSzDXPfPIr1JTFTVTMovwgIrAjA1YrA9YkgJMWAQQPgbY9ohrSlXqbhR0ZBCUSwKaLoiKQWBYeQTdK4CUfydXPgzXXIyYwIJEtTVE1AAgF46o9gWoz4ho3E-Ewk34jo7Sbha1aA5QcQX2FfMWOwV+BKUQMqZhHfMLeErkgIHk5zPkpYwIzEtYsUvEwQPQIk3YmlQWDdfkBwJlFQcwPMOEdtWoULUBKTBE7kpzQ054qkKQoUkU+om+cUy0606UskgCR0aqQiMmdcCKDeRYZhU0Fkm0Nk3fb0vU+LIMI09EoIrE0MwQXMogqUkk0VMEjIRYAUROEEXnOQAiFYHiVtKyUCSKVOW4r1Uso0wUt44Uj44s0syMis6guw5eTYCoKGROCobLA4YyS4IfLcH2eA7IjkoDcBGMJ7E3OrDPaHPAdgY0jElY2-aeHwH0R-XXQQHc+rKg1bME5YIEDQO3J5OEKECKKoGs20QWFQMxTMnU70iBBHG8vcoXEkAUwMqo-skMs8i814fc3Aa84kU3W8vRMc6JJ0Pkcia4PaIUYGUoSQGA4fXCApKiNc3U-IyBKkabWbebOYp4too8wss0m+KimimY+bQQEomBEcu8wPHrbjENSKWwDUksK1HHZ9MxeoHGbILs6MKbYkWi2kBbR4mBXsyC142Qwc9YtixSji5Sri1SygXitC+8gSz7Q7YyOwEKMSvbRwaQJYKS4wOcWS8iqTRSPAafJJAgSffALy2fKMpIP9aKaSNEG4K2MEMyHCJ807JFI4LIuE9yzy0XDyCQRpDgRZCAAtJjZ2HZVAA86FEYik+YRwNIYyUTKK4NO0s1CpdtBQEbDyuMFKzACQXAD44tYgbRfoc8ANQKxAQ7SQaw0A4aWsG5fMNrGaSKMEAUWE9kvfRq-ylq56DJD5HZC+UMVMY8As00oQnApgR-VQm+QgzQ4cbQ8Q9QaFNEIYrYZYF0BlI4E4kq6KQyX7cEM4a4XgrMntBa5qiQZaxMbZJgdazaiCoM6Cwc+-favAggjQnqrQsQs-C6vqhACaYiCxOcK4LQWVMyQ6GcOcVVdguwf8r0ntdKw8wNMPaQJVFYApRcLLVFeYMoYCTdUQQLeqty0mjJcmyg0y-itrKw0oS4MmGwVgk6JgxXQsAhELLrEbH0KfEkbAeDemcgUXHyxGPy5qy6xEImVOG1TYBqMyQSma5YTYYS8NWW+WzgJWqkFWieNKrmijTgXAUtMFPgRDP1IGlSTawqh1ZeCxB6rUiEAzG4OcTC2oKwX8xeQyC2vyhW62221AVq9qzATq1gPVF1Qq-+PScUGSSsSwUW3uMGRcFy1cfIa4TtL60rOW2Oq2hjZW0XP6vAAGtar2jsbak83a5QqGw69Q9-OG06hGrPYQS6pwQENMguN3NnVFewAKIfZBRQMEdmxKntaumAOOuum2hu-61az2jajsPsrSuoyGg6-AtQ46-uocM6s-Ye5GwyLQQ4a4gCfeHkYO1EcwApFYQSa4JwEbHoGbMXHK9gPtUkXAJinaiQWCwQP+i-V-QYZ2YB97ZGkQYsZeN+bCKEC4EpY6fuJBWleM2WRNDm0raBgBuBoBl1EB0GqCw+xQyBkh2B+mchnlEB6FZBx0KpS4MEK4OwRs46EYw4D6hKUwMEEbVyMIHsmfFO3ymATW5GuoIyQAg2ciSsYsJszTewELW1P6J1IhvdMRpDBLX6n0v1ZpbgAtV4KkOMKkAgbe7ZVmcR-AIMH24PbbYmNuMqq1F0J3fSHIF0REReUR0IAxvMyRiBrkv1VwqAPAHynAcgPsMooISgOMH2lI5hApNED+UQAzRwEGTGReBctQa4LQQJhxwx0J2xyAAIfR0ssBjupQlQ0+o62Gz-Qe7PUcsyt3aKYyc2MOE2XhwORVD0pdfOrfEp4JxaiQEgLKv1CAdUVSAIex8Z2p4IzKKB-a2ZkgVSQQRZ0sn2wJLIfSIaOcd8sWW2J0IO0QMTHBT0ymKTapxxiZqZz1VvDZ+ZnZh5qhzS94o+hoh-V5m+d5hLLW6NEGB6-IUCOobLM0SwBhQWDeew3RoDIgRUXse5sp7y6Rxay637coRQOYZgrIayAiK4J9Sw+NLhNEG56zHtZF08KpoJiRpJMJ5wv1Cxqxmxpu1apJpUBZhlh5zOrxusdLUwDkSEAmCxVBuYRe1BT6gCmllF5UNFkJpl4x1vSJ6J6eLKl4BV3l0pvM9VxB9p-iqpCRCF5QHDSwDOB3DpF6pYbFBUvFOV0rWlnlpViZipgtF11FvlhLZZos4+6Gs+5pkg1pwwLWx8nkWtfO84ZdYGaw3iWA+gpVVc5e51nVt136p5kMOZwIbl3sQFtu5YlZ359ZnNx-BV7Zn1px2+4S80YCeoNraaUvTkPzb+1lTBkbL1xVqtx5rV-57Vulgt48A+75xQ1Zv5stvNqkStvVxLRrbSEw6cSGPxweGyYliELkOKfJvMOoWayuvdJ-dxIIChLaotos1mNyYtQQNq9-OmOrNpvi3PcQMGQaDI66IyPpVFdCc4su2abFVeEbQ9lJchdmT54Mwci9otFom9ogu9ocB93mnJLFQ4aobvbhfkQiYUYyRfa45EL8aGTt7gaY1vWnPNIkAtKD0tP12-EgNNK7BzAAOXzQADVi1S0YivHbQiwFxI3QTDMLhJp5xCJBYEoANEWSM5NiOoBRcAgyPKnKPh2NLwO6jaP2B6PYsmPyPWOS053SSkgpxnrrcSouHIYIp0FpBoDWaIRQWK6nXOT8j4KwLeW09gxHOFMs9qOsCYG3OHNK2XPLyEKH5y0zLkQNB18VgjIClIoXRS8LYLhR72tgRFw5LQLrsmj7tHtPOJBvOrz0v4cMxkauiwZTon66hQJqgqpDJwZTRuIMh9geEUufOYcJ0Zj8usucuEKNiWv6NHtdjPZtgrYjhmEOUV1qyTZ-4LAGDHA927ONy9SmuoNCj3CEcXs3t2vJ4FvEK8uHsrTVvDXH2cwuFARa15h6vVh9sAcOkX6bgtABQIuU25rsyHPddx06N8vEdXtQH27giOuwKuu3uduSykd9vEPDvH04j+RnQZI-H+n5V5BivTDNgNAn6f7xPmWvVNuggRdadVN1uAu-vKcQVb4CSYjaxJWfyAJ6FCx2cshN3qg0sbIwlGuXvCfqdadnM8fNvX8qdieCujW0ZblJpGEKJlg0E5d0dpAchThxA1BzgZQ0efTUuJsDcjcGMbzOeryVer2byYjkRwZh8lVd3BZiXRB198gY8Y89NmeEKdkVY-cU7vuizfuFNBAfd4d-d+ewf6hLZCJpfuHGoCYnAgQMhP6JIJQl7HuaX1aZG-QPIfCMoCBLrkQWzSI+4eGbAblcnu8dMFzyJbOSbnXo-gxY+GMVkFlrxPeJx6gLhzpbk6b0F7cTpjB3Nzuw4TF8-bmo+p8AgS-4-y-DBK+F3k-a+1d0tshG+tAxJs-Gbw95fU290EZu-e+y-E+xBB-epq+U+6+x+M-UUmdFh5wZ-2-O2i+e+kk+-E+TB1+grh-B5R-0+J+jhPZD+2+8+T+l-z+V+qA5Br-EBN+R+0+DfMyAfAP6t9c+4ONHovw1rL8E+VAYQL-wQD-87+gA8fsAIljT9X+EA+fki1P4wDy+6gBAdXxqij4AIhEQpnxzDx0FbEXHfFiPnf7QDP+sAhQIQK6JcgSBL1cgSujXyUs2+-8H2J22wA21lWHkNWt30kbYsUGPIWRPD0qS7YzYGQQEDjFAgKl8k2pAvgv0EGHsjG4TNVr6iiagNNW5jTQfqz0F4AfaSiOsHGmbJ5BIQsPfjhLHsAEUMylEAQUIImaqtpOzkKZtgD6AcsVq2ycgMYN06VkUaRwbaIcTDjnBVg-SNfDyGfRDdzg1gVwVoPKactlonrIIVlwDY91z6LTb-OIRMBa0AI5QPDFFyG6iB+khYCzg0HOBWA48M3dQUiyCGZtpmLzHNi8EyGO9b847UtpsxviBC3BmdeEKogNgj5lAcgiJNGjiQ5AtAaWD0JAOaGhMnmMzdoQMMPZgdwaPzG+BOz6GP4ghI9G4E6HkaAJ5g0JJMkoihDSwbglkOFPQJj5MtYAMYDgE7Wk4X0dk2ASBFkKeFHVP8TAD4WWVDDXtmIGAQ5IVzsB0E1EFQDsnkFNBYdnqP8CwAqWLod9qWhfD-qlUeFpgXhbeYcO8M+EjsByR9b4SdUvp-DIEkRQEXGEgBQpkap0QiuLElTSg8gJxGRIYX5gQhah7uSAbgIeF7UcR2yWos1SyF7UT6MNPunkLILQoHEg1TFJN1sCkC7B62Mqjd1pQfxZWjQiTjyIxF8i3hgoyRhsJob1Nu6jTXukQQvpX0s8UowpjGn9gew7AotQ7NIGT4EYvwH9ZISEyHAXs8qB5IUZi1kaECY8B-RwMNzKQWIFE+-WlEoFwgoJianfZ1osM9HMRvRY6UJh4ICAbUqQaaZMYeUMHpjBBWYlgAVVvrTcH6UYmXmEgmGIDCYuWU0FYGMAzl3R0+RMcGELEpiVWOg14aEDMbZjMq2VLsTNmzGFV7RqDUQPjUrDG9sGuEaQJoHqg2ATgD3fdk0LcE05mx2Yloc82k79s1hHor0a2MPJdDO6azLipOyCHjxmxrvfKiSB9qGJ94aiVePIwUT5AWskPWoKBHkCNiVxu4y8YEFTEdiVxgggtHuL8HN1txTYr8UWIQGGQccdYHFL9ikiRpsGomGtJgzSwit1RcYjQcuLqzgS2xqVD1h0KwmrigJB4o0aKKDbiiQ2+Qs-HICKFgwYCphX9AvQUSxFFEiIIqLkGRAOFN6+ov0eILkZu5JYM0PxrWByDCgVy50LGn9GhKcSFe3E9sSy1byAMexuYpSXuKHHVpxAtyBVDQnMJ-9uQbAmEh2VrALjZuJGJZAnVSppjBRoEgdkqBiYzF4mFOJJsEOoLj9s4nWOyMcStYo1oCLpXkPSWZzoTURuROSZZNaGbj2hytYUdsN6FbNlaPtJeEJH5B1ibBU9YoCLB7hDI7I1gaRFxIsktU0xilaeB81sbPYbaPtGvhuFtAfwu83WUTGYFTJZwO4tKPKb9VsZ+oopJE7ISaNyGUSyCCgS6poDKCUsoQNgYTMcGFCjJ36dNN9N0iCklYQpttIid+N9Gn8+JAY-MOCM-BxDbUwoYaIATUDiBrON1VqdhKTF7jtBCk14WQ2UlatVJ34n2vCAYLmYKgkbSsanDXRWBDsv0OIadOWk+jfxV09MfIQIlaCp29kuJgk1gDOSKpFkeKnjDWCYc+GQdYYibWykbBHWGoiBtxP+m4SCp4U7Nn0LKlLScJ+4s9t0JLbHjdhytM8RewvEQSDuE4XjNFHOCjQn+dQgzNUAVxMFAsvHJYH9NJmXSJgJHaZFSEAnfjgJq1eKTW1sBOhgsVwMzDJBOJzghitg-FtImr4CzzpK0wGcLOk51YAJPYrRGnX5EVT0gUrYaMYiMgwhNJL7aWN03G5qCMJG5HGYLNSH+COpNtaKQ0zFFmiJRqBGQFrTSC5YvwxwfbPUBtlAQa06wTSVsASqR9Ss5kz8drIBlMtSpu9EGl1JFGBsmmFE+GlRKzxqBgWpyEfP7DnGbAbZRkacK9NfGPIZJ2A3NGLlWTHg-A1cQIG8LeLJMkGloLkPI1qBHT2yFwHGsNO-ZwtFES6eaQeDIDYBOgQwP1LTk7mnsTSdTaebPOGDE9m5I9PSKkFxitZGSXcFsjZHETuhdoC0VeXPJI5i5F5Bo0dpMwyRryiCE8QQJvNpF697RGwZOFCDSCR4xY-IOWX3NAgjQ7AZ8++RfP1li5R47tRSS6iy7nz15T8i+lvK5CpA8WoMDUt1i-R5BWZKiKROcBAUzywFMnSBWmhMYwKCRIZOBY-MniIKkGsBFlGKClYyUDMIUCyFBMXhjzbUC0Tco9j9SjwfECEJeceWCJ8L4IGAaeGmg8BSiWZ0oeMujlrC6SesNgIun4wbY5B20KIlsNwr0C8K4IAim+YSMUIiKBF4i9gJIuRrdEqg-jCxHpmWA08zAwIMac+3UVcKgK2ixSUxBYhZCPFTAExWYsIGDVV0MiG6pUgUXXA8gFncqugmBAaKDwWikxt4v0UhlYA3i3xegEur2Krqi8E2N3GYKl5Hym6LQKHlgIuKtyfqbRKpCy7lL2AqS0EZBNhSOyOytBR6pCGtRqA0QtNdIrIBKU8LW8VSxJYOSqU1LoU4gEoRYFl48J6UAOATMJkLC9Yo6RkbpW4uk6sx3InkQRcxSwIrKPIlIIZbSJsSL1rIjqGkooAij8gygw+OrmHn7iLK-UWytZf0rqJ3KdlEitJbSOeoz1rAbS2QHdW6xDJzlBGd+SVGYQ3K1W6UYQtfJIkqQMo-gH5HGF2UIDF45QavgBG2zGotAwA5QCGm7xR1Ac9cuanEtBXQqsovyB5YoShWZRYVYAeFYzIXhmBzexsEaFgqwbFAoS5gKcMlJkh1dJ5tEAldJ2Wg4A+AWXfldgD4DUrQeVfaKDjDu5ih6gDKAiKhkxyMkNgV1EFXys6ikqpknUMVcF34r5gpVeGfYLKtMDjQGcLoIOgRlMDAryKvKoIPyod7kyhCdq7VaxjMoSsZxlgtLCAUrFGFzl0MGWFHShGqrbVK0DVR8hWjOqksTM4aTPXoRTkbC3WOkTOCkGV53x1q1xX6j3qvQsuma-wBGvna9Rih1YOIYBCGlEtUUboLTO+Am5HSFlaa0pa3hzWHlyFg5RtXmr079VAQe0EgYyRspLBiWww9GtbAm75Ig1k+ToMjGsYkSx1E6ttSELEj61c4aWLaJcQJiyyGgwY8quHCpaaL01readdSA1X7qqQs66glYCCQVBcg5sCEKUAJj2Bpx1odCJIhEoLQ3ejMdWCzDZgvDYFYQKCirDfXSEP17MOfAyTbjLBCWImYxBFBMhOhGCjlMrhkG5XKwGYasADazDAUaqSAP614n+pQ3Mw0NQiBAYRDDojFKe+mZVOJUmgQh-43ObuEkPIqvrcN98b0CRMw0TxsNyGpmPfCoS+ZVZacfLEMi+m6AkgesOJG+GUDkQjCOQF9Ths42eov1zalTlhqQ2qxZNWscxecpsKXCQ6kmuwSJIWCI9+NCss7HWp6XSdz4l8a+MT3vhZCvEnQDgK7wvgQpBAXG8xf1zfiu4LE3eCgVdCRXliHS8gMTnCRtXmanNT86zQprHa2b7NntJzS5oRWSBvpXCL2KygUWyBEVqgTRjakZpBqdUasKuEE0mDtcK4YAVuQVu4AnqzKggKsMEndD2BJUxkJwF3DmDgxFAEdUKjcBy3Fb8tNcUBhFuy7FbStPWirfxSq2LAat4sTzZsFKoHzrUrWzQO1piU8rd10nEFJAhVgwJYFECKBMxmG1PsnRVc1rdLFsB8dm+04duP-H0h9wGhlMG1atu20ba+td29bZQF23JYx6tgfLGkzLrGBss8IcVBdrT51gg16aSjCeyy4g6QOwwV7Z0WrltYRJUg8FrOWwayAwYtkZvlEo60mallNOMhGDr60Q6T20OmUnRM-qvslw4gGwQog9JRI0U+mYbtutiXLaFmAiUDiRIviCIqVLy2pTSo36msBkomgTm3AUSOBjuCKYMUrNNBBr2doHPrdLqh1c6UMgBMIRNzIHghrg4Y7otYEAWXFpqi2iBkzqfw6IsuhuuFQrvMXFcSNKCQpW0uYlVDPw+2RmvkAj674bVJuxTmDUNFu6idvUHIIsEqDggwqWwQiKLQEiS5oSZqF+EPHIqwQnYX6kiaPFj2PRHN3YfQS5LMqaZjp6CBiWCw-ISVsgc4MPIUkIgLQY98heTUp02GGLHYZepPZfBT2eU587DOoFghBgjFYCH5CwFTSiiLsqkJk0BEOmY0OqJAs8gcLXs6D16u5hGwimcq0CvsVAP5Jsl42VWF56oZchaAPu8B9aR9YAMfRPrT38UBMLcKbSHTl6-omyxQi0FRqVxxoFoLiOpB4lgBeJ00uobwCRKTBJIfAT+3xNUrN0Irt5kkQsFNvuRZAzOXjVeG+R3CxRb9SSI9o-u8Tf7vQfW9-R5E-3wHWN3upINWQQ3S08wwYgzPVzHrLAhWs4J+n3sph37kkHiP+nrKy7IHMA6SSYBgaMDTg6ap3HkMcHIjQxSk3RFWZjTAjF7yKFBo9tQfL0e7b5dBhg+Vt-086xU5SWyIFlKDGRuQpSU4Af0uDW5w9ZB+SEIZSQHlqQzSOPUProONIlQBhpg030l65AsUz6RQCAbFilgE2yIcYaukjiCGYDuhppAVs30V7DRxhzw2EHMNlARawekGDZRKi6af4B-GXl0Vz5FZMQMHeANEAkwyGhAn9BEK3BuodxRaKQVYC1o0C51rYaia7W8iDApHkg6cIxOclOjlRrkW8AwjUfUO4VOU5FFKFMhmSUrEjOqpDoYmBzwoekSKYOvnRfD9y306wQsFHDvyfJvk8yMo4IDiijKy84-bYN5J0yxk76ELfI1IOIxlG2ZXeraLTT7h2CUg9ihDbdSeTjLYxwUoDNrh2Mm1BMpQVINNDwosrZokrbvIoZCgM71yJGbXEoRELH4C5nRl1XzUqT3GQ4SKXkJn1h22oA92UvXTZkEJ35nJhMhiBxEjVUpOGD9IwhDEXBRdwCNfU4Hh06aAKtDC0644ifNFTpEKbVG+EmCIIbNUTOx1muDBnE24RdsbFlbKoLB4JvlF6y42SbMl6ltu9FNSlETKNhLRQ+2WsqMaHmYJmU4VZgibRBwpdviIp7wmKfFUykOQSCCTeFQNj2Au4BhZhHd14xtpMZzswU-kR5IokNTXRw7tcRijWd0G6NUWjYChATkZUELc3s7tMno9fSKmf0m0XFMOm2lMqEAsBF035Bju0XE5HYHoQpceytp4EwL0ZrLw5e9KtGdlg6TYx9gsqSavCZ7RaLqsyFXcpt3YDBmiNM+o6RozGj4UN2ImP6AhvRrFGBTfpybNRT0pzZlKaphYkGc1NklBoFJBKLtA3iDQrUQxAaquDSYFIGqyVSRmUZkhKIbqNNe6tkArDMID+27WwZcR9NYyfqoTMmgudnDKJPjxwO0gmo0CAgRdZqbkIzVJM5EgM+5plm1QULFoyjYmAKGV1sH9xbBY1FsgimLwKC6gs5pqu7OboZzSj-ZoKqPX6w+woQPSFY8Ukxjh40spVFsw+ZIyHmoL8qESsMU4aYRaC5EKKkvFXCWEgoxwWSGj1XqKhFaG9BOguaWCks3wmKSpIbWZSA4ORH4KwzHTXq11iZDdLC3aar4VrQ8MqLrKcBuDACl4+wU2mrqVlfG981F9evxbtovnZ5mAd8zwhDy7sroVyFdL7uhiAJuc1ufkxhYkBKW+L9dO2unOBqQWhLC7NuDWhVwShIYwdcRFplDiEnrc6F74xIBIYDBGGCDHY-GQcpq7VgdrZVQohSJ7wNgVJQaDWDGaMsPIC5jGlyGD2rgpQBp4GDPX167sVFxkIjGjwza6zsRpjAcWy2pDvnzuvEYEoNnK6N8fyLWHgq3GgKIa7mPbIWdiINZHmlEeB+vpoGp5ymX2qUvGNRqtUNyJAxVtOWkMqZusjzd622CCSd1l4rUKDDpULEJbN7ErDzdcSsKJlDsjznIZVIFEkRnmoWFJeKm+gxlKpO26bDq-OewuICXLuLKNjglnQA5+okiOOV9udy3W6WU1yyX+IqtUgqrBSZeBHWbKyAs6BMBnGDilCAHtaf111vdfkl6zNQpgtE-mswOIpl435AGH+hUDY4n0BSMS5RGSlI3vWs7NqTNc9Z3XZ2R5ioJbBOSSQn+cuOcS+FlUIpUOcsSAXTfGbrjCZ8zKdrq3GYM2FgkqQ6TLAbal5wl35fTLQSeRtWe0QHB-Sex2NVIgQhxnbLMshZ1H0gUYpLThT5yQCiOLXS+bJ3zSFo2OYAcU4OfMDqJYoKCLYLppmgtpDlemfC9byc6p5UA6eMs0CfRMDnq0MkAoM+jCXMJS8IywvGDicB4c5+Cc+zhjxe7bdHstt4O0NO-oNtcEVUAfK2T-h4ZJ+Xt67N8VTuPWOUgyDO2HbUAQgV0+YREKcHgs7SpNCvebsnaW7NEVuwPNOxXZR04ZikBdK2GwLbToQybxmia4r0x4l2nse3buyyl7uZYuMj1WVLjnSLDRxEPliiknZt6s8ZOqmW2-1C+mcWYJVwdnOwiuVhVI2xYIuxNh3vs997Q5oAtAS3SN9Fw0aWoPeOCxnLr7VWLXmrxLNDhZ7Id7kAvd1siQ8g0UCSB-RDoVJv7maN3vb1ttKB9iboIZobDsHHaeI9Y07OHzIh3Di+jAgO1jb-4WAhz1keRVZGNUM0hi+QGaD7FDjHMPxD1+yxvxZIlCaaZxYKE6R4iebwQUg0SIQwmtEBFhqNrqxjaquM3P6M9YJDVyTKIJwHQteof7EYciO-UXgz1H0HfN5gYNL6LGvIAKSlIJW3y9sgdC2BOyrjEnYR3hJpsBAhHbgqq0vDKhFMpEp0fpOREtwjG4y4gDe1JlscpCmWywtoUTNAlVX76i4R9TdGMkKL9ggSEGILH-SEtHkeDs-slbLtBw6U9CEsEoBi6nNxb7KC6C3DshJOS+d+b4aVZdR4ibbqT7Duk8RlKBlAUVikkyUyzaaEWgjrUS1Uhr8jgZyoJh8mbB5-zawmWa2HYRD2Ihe5+WVy2qMYe4yfxSSFK2F3onexKkjW7BokO8bBLOVO6BYYRLdkqOG1+Y6MN+Pms7w7WkiawtbOR2SCJoLlyGAIcEcJjdngNoGWVfFkHlDrScCi3jG-JZXWEHcXG98+8zxzFxFjnZynLxmTMCZW4h52C-LOPWzjnOCboLGkT4xJxC+ddZN0dtmPWzvjncTC86vzzRZrzkkFVauCWHpTkMSKMxLEgNaxM-O8Y9s5SEzPqb-gypjZLOktijncLiLgi7sDpFU4jfORDYluohQha7cU6XM7hc3RLYaIQiJF27gKJzWyCkKAUkbvWRxXTztG-dLedcu1AY2-onL3DzeTbqdEn1eoqUAFnE5oUgqX+OsnNCp2R53JjcOMJERNAwoT8luA1L+1X76r-GRuMFuBBlaDFjc8+QePS9H0YkvrP-E5vh94rPrv07wpmzFSEsDNyQGcXE1MjBr6UuM37vCuF1k+cb9qa3kDdwuqNMUY2AfDNXeSu8DJJ3VVN7VayOXqclJ8w6CpsTN2WT01B9QUVg5p9srm3IvBnOySSZeLkqyYxul7ijz3vaNrGaXDvSRXRiOmlks80Nu1xo7htSDJsn2uuXcIZC+-f-ivtu38Fv2n5lTgDvdzFp7GcO8bfgvDBe1+ZlZZmcMWzQtiA41Lz4x8N4eKZIEoAq3TnvzHl75Ode9mcavsRBssWdmJCdj0qgwmW1KA9-B+NRl+wY5q-fNP-uk57L1d9NY9lFubaR5qoWXRaWcNhOkc1Q8MklBMFbEK7i6WBZ3q2XKnLbvSfh+uIjnlA3krx5LkAWz7TAZSYjLJybmYb6PfT1CFwlSKTa5oMLE4hzlIqSR1jduPXXAvnlXzsoZR+sybBwrBZonJxHhjOGEqi8Z6ar8igp-NvEKynPKMo9GlkRg5vyxSH2DbLBvnHg97WYpljp0X8KMAsxnnGwKOUxu-M7OR8vnvGGgEgCQa5JcxCYAefAkipeKnhjwShKdMNaVRFihwT8CXPvSlSLC4Y-JBmc0gQyINHRr4skZLKrcMMVnSDnB4sMVL8srcjbLBPgdmYLEgnJdIZxBSPiIqJHsFhmcvvIL4hptXkq8Qi8jz249MAOBIb2wTuKilyAodWZsUN8sl0q83ZOoEXhLVGOuiso3T6DmAn7XuQ4RuMmOoLUzrDUPYPPYXV8UHudwqvTI09MG1ZXw7SJlcQaxtcd+K6sfGCci8sMDGHyW5UV9okPhvdd2Ixx1lVx64IBgmYVSBdYDIGFXQdAkgkoMdeI+mk0cagfmXkHwvgEj6Rn0XkvjoLDqAlCnAtiaHo4no0yb31+GtoIN+nDo+G2xqEMVBqGLgjMI5mRqHroY2qaXhFPw4OC2p-O44oTKXjdet-RGa9dwWxzZZrC1yaOftZEOnImGgfXBq+kWV7aFKCrBOtuqbrRWjq9CA205zHCgdBsB1iTtxzarsYS4YTRhfTOp7dAkoAeeGcOCXnCPiVzZZLAfuxQKhPsBo5gduO9mDb7KBB0haBzK0CHpu6QEjsAyTFHt-xVM65dtXoh1l91cOIVyQ01dCHuDEOU5wP6P3j14N3aI4wHn00AiELpSCL1Ci3diwaOmWQ7feu0vcsnwA+-csuQPuaNFGo5PJVHpOEHxGl7r7Y45P4HwJA4Th9C8CpysVXKsL3JAS0SNtNAfv1BAv9rGnvyj5wbo+RiInOFKAf004+EoyqQllP8oMiEua8-oT75HkZoZLItNDkK3tKQW5ZVYQ5w5DB39Hs9DmYrw1AEG-YIfYN-naOivsPZBFgiiBu9xmUBXAVwCAA */
|
||
id: 'Modeling',
|
||
|
||
context: ({ input }) => ({
|
||
...modelingMachineDefaultContext,
|
||
...input,
|
||
}),
|
||
|
||
states: {
|
||
idle: {
|
||
on: {
|
||
'Enter sketch': [
|
||
{
|
||
target: 'animating to existing sketch',
|
||
guard: 'Selection is on face',
|
||
},
|
||
{
|
||
target: 'Sketch no face',
|
||
guard: 'no kcl errors',
|
||
},
|
||
],
|
||
|
||
Extrude: {
|
||
target: 'Applying extrude',
|
||
reenter: true,
|
||
guard: 'no kcl errors',
|
||
},
|
||
|
||
Sweep: {
|
||
target: 'Applying sweep',
|
||
reenter: true,
|
||
guard: 'no kcl errors',
|
||
},
|
||
|
||
Loft: {
|
||
target: 'Applying loft',
|
||
reenter: true,
|
||
guard: 'no kcl errors',
|
||
},
|
||
|
||
Revolve: {
|
||
target: 'Applying revolve',
|
||
reenter: true,
|
||
guard: 'no kcl errors',
|
||
},
|
||
|
||
'Offset plane': {
|
||
target: 'Applying offset plane',
|
||
reenter: true,
|
||
guard: 'no kcl errors',
|
||
},
|
||
|
||
Helix: {
|
||
target: 'Applying helix',
|
||
reenter: true,
|
||
guard: 'no kcl errors',
|
||
},
|
||
|
||
Shell: {
|
||
target: 'Applying shell',
|
||
reenter: true,
|
||
guard: 'no kcl errors',
|
||
},
|
||
|
||
Fillet: {
|
||
target: 'Applying fillet',
|
||
reenter: true,
|
||
guard: 'no kcl errors',
|
||
},
|
||
|
||
Chamfer: {
|
||
target: 'Applying chamfer',
|
||
reenter: true,
|
||
guard: 'no kcl errors',
|
||
},
|
||
|
||
'event.parameter.create': {
|
||
target: '#Modeling.state:parameter:creating',
|
||
guard: 'no kcl errors',
|
||
},
|
||
|
||
'event.parameter.edit': {
|
||
target: '#Modeling.state:parameter:editing',
|
||
guard: 'no kcl errors',
|
||
},
|
||
|
||
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'],
|
||
},
|
||
|
||
'Prompt-to-edit': 'Applying Prompt-to-edit',
|
||
|
||
Appearance: {
|
||
target: 'Applying appearance',
|
||
reenter: true,
|
||
guard: 'no kcl errors',
|
||
},
|
||
|
||
Translate: {
|
||
target: 'Applying translate',
|
||
reenter: true,
|
||
guard: 'no kcl errors',
|
||
},
|
||
|
||
Rotate: {
|
||
target: 'Applying rotate',
|
||
reenter: true,
|
||
guard: 'no kcl errors',
|
||
},
|
||
|
||
Clone: {
|
||
target: 'Applying clone',
|
||
reenter: true,
|
||
guard: 'no kcl errors',
|
||
},
|
||
|
||
'Boolean Subtract': {
|
||
target: 'Boolean subtracting',
|
||
guard: 'no kcl errors',
|
||
},
|
||
'Boolean Union': {
|
||
target: 'Boolean uniting',
|
||
guard: 'no kcl errors',
|
||
},
|
||
'Boolean Intersect': {
|
||
target: 'Boolean intersecting',
|
||
guard: 'no kcl errors',
|
||
},
|
||
},
|
||
|
||
entry: 'reset client scene mouse handlers',
|
||
|
||
states: {
|
||
hidePlanes: {
|
||
on: {
|
||
'Artifact graph populated': {
|
||
target: 'showPlanes',
|
||
guard: 'no kcl errors',
|
||
},
|
||
},
|
||
|
||
entry: 'hide default planes',
|
||
},
|
||
|
||
showPlanes: {
|
||
on: {
|
||
'Artifact graph emptied': 'hidePlanes',
|
||
},
|
||
|
||
entry: ['show default planes'],
|
||
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 constrain parallel',
|
||
},
|
||
|
||
'Constrain remove constraints': {
|
||
guard: 'Can constrain remove constraints',
|
||
target: 'Await constrain remove constraints',
|
||
},
|
||
|
||
'code edit during sketch': 'clean slate',
|
||
|
||
'change tool': {
|
||
target: 'Change Tool',
|
||
reenter: true,
|
||
},
|
||
},
|
||
|
||
states: {
|
||
'set up segments': {
|
||
invoke: {
|
||
src: 'setup-client-side-sketch-segments',
|
||
id: 'setup-client-side-sketch-segments3',
|
||
input: ({ context: { sketchDetails, selectionRanges } }) => ({
|
||
sketchDetails,
|
||
selectionRanges,
|
||
}),
|
||
onDone: [
|
||
{
|
||
target: 'scene drawn',
|
||
guard: 'is-error-free',
|
||
},
|
||
{
|
||
target: 'sketch-can-not-be-drawn',
|
||
reenter: true,
|
||
},
|
||
],
|
||
onError: {
|
||
target: '#Modeling.idle',
|
||
reenter: true,
|
||
},
|
||
},
|
||
},
|
||
|
||
'scene drawn': {},
|
||
'sketch-can-not-be-drawn': {
|
||
entry: 'show sketch error toast',
|
||
exit: 'remove sketch error toast',
|
||
},
|
||
},
|
||
|
||
initial: 'set up 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',
|
||
},
|
||
},
|
||
|
||
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: [
|
||
{
|
||
id: 'sketchExit',
|
||
src: 'sketchExit',
|
||
input: ({ context }) => ({ context }),
|
||
},
|
||
{
|
||
src: 'AST-undo-startSketchOn',
|
||
id: 'AST-undo-startSketchOn',
|
||
input: ({ context: { sketchDetails } }) => ({ sketchDetails }),
|
||
|
||
onDone: {
|
||
target: '#Modeling.idle',
|
||
actions: 'enter modeling mode',
|
||
reenter: true,
|
||
},
|
||
|
||
onError: {
|
||
target: '#Modeling.idle',
|
||
reenter: true,
|
||
},
|
||
},
|
||
],
|
||
},
|
||
|
||
'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': {
|
||
invoke: {
|
||
src: 'reeval-node-paths',
|
||
id: 'reeval-node-paths',
|
||
input: ({ context: { sketchDetails } }) => ({
|
||
sketchDetails,
|
||
}),
|
||
|
||
onDone: {
|
||
target: 'SketchIdle',
|
||
actions: 'update sketchDetails',
|
||
},
|
||
onError: {
|
||
target: '#Modeling.idle',
|
||
actions: 'toastErrorAndExitSketch',
|
||
reenter: true,
|
||
},
|
||
},
|
||
},
|
||
|
||
'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'],
|
||
},
|
||
'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: {
|
||
Cancel: '.undo startSketchOn',
|
||
|
||
'Delete segment': {
|
||
reenter: false,
|
||
actions: ['Delete segment', 'Set sketchDetails', 'reset selections'],
|
||
},
|
||
'code edit during sketch': '.clean slate',
|
||
'Constrain with named value': {
|
||
target: '.Converting to named value',
|
||
guard: 'Can convert to named value',
|
||
},
|
||
},
|
||
|
||
exit: ['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 sketch 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 sketch 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 sweep': {
|
||
invoke: {
|
||
src: 'sweepAstMod',
|
||
id: 'sweepAstMod',
|
||
input: ({ event }) => {
|
||
if (event.type !== 'Sweep') return undefined
|
||
return event.data
|
||
},
|
||
onDone: ['idle'],
|
||
onError: {
|
||
target: 'idle',
|
||
actions: 'toastError',
|
||
},
|
||
},
|
||
},
|
||
|
||
'Applying loft': {
|
||
invoke: {
|
||
src: 'loftAstMod',
|
||
id: 'loftAstMod',
|
||
input: ({ event }) => {
|
||
if (event.type !== 'Loft') return undefined
|
||
return event.data
|
||
},
|
||
onDone: ['idle'],
|
||
onError: {
|
||
target: 'idle',
|
||
actions: 'toastError',
|
||
},
|
||
},
|
||
},
|
||
|
||
'Applying revolve': {
|
||
invoke: {
|
||
src: 'revolveAstMod',
|
||
id: 'revolveAstMod',
|
||
input: ({ event }) => {
|
||
if (event.type !== 'Revolve') 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: {
|
||
target: 'idle',
|
||
actions: 'toastError',
|
||
},
|
||
},
|
||
},
|
||
|
||
'Applying helix': {
|
||
invoke: {
|
||
src: 'helixAstMod',
|
||
id: 'helixAstMod',
|
||
input: ({ event }) => {
|
||
if (event.type !== 'Helix') return undefined
|
||
return event.data
|
||
},
|
||
onDone: ['idle'],
|
||
onError: {
|
||
target: 'idle',
|
||
actions: 'toastError',
|
||
},
|
||
},
|
||
},
|
||
|
||
'Applying shell': {
|
||
invoke: {
|
||
src: 'shellAstMod',
|
||
id: 'shellAstMod',
|
||
input: ({ event }) => {
|
||
if (event.type !== 'Shell') return undefined
|
||
return event.data
|
||
},
|
||
onDone: ['idle'],
|
||
onError: {
|
||
target: 'idle',
|
||
actions: 'toastError',
|
||
},
|
||
},
|
||
},
|
||
|
||
'Applying fillet': {
|
||
invoke: {
|
||
src: 'filletAstMod',
|
||
id: 'filletAstMod',
|
||
input: ({ event }) => {
|
||
if (event.type !== 'Fillet') return undefined
|
||
return event.data
|
||
},
|
||
onDone: ['idle'],
|
||
onError: {
|
||
target: 'idle',
|
||
actions: 'toastError',
|
||
},
|
||
},
|
||
},
|
||
|
||
'Applying chamfer': {
|
||
invoke: {
|
||
src: 'chamferAstMod',
|
||
id: 'chamferAstMod',
|
||
input: ({ event }) => {
|
||
if (event.type !== 'Chamfer') return undefined
|
||
return event.data
|
||
},
|
||
onDone: ['idle'],
|
||
onError: {
|
||
target: 'idle',
|
||
actions: 'toastError',
|
||
},
|
||
},
|
||
},
|
||
|
||
'state:parameter: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: {
|
||
target: '#Modeling.idle',
|
||
actions: 'toastError',
|
||
},
|
||
},
|
||
},
|
||
'state:parameter: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: {
|
||
target: '#Modeling.idle',
|
||
actions: 'toastError',
|
||
},
|
||
},
|
||
},
|
||
|
||
'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: {
|
||
target: 'idle',
|
||
actions: 'toastError',
|
||
},
|
||
},
|
||
},
|
||
|
||
'Applying translate': {
|
||
invoke: {
|
||
src: 'translateAstMod',
|
||
id: 'translateAstMod',
|
||
input: ({ event }) => {
|
||
if (event.type !== 'Translate') return undefined
|
||
return event.data
|
||
},
|
||
onDone: ['idle'],
|
||
onError: {
|
||
target: 'idle',
|
||
actions: 'toastError',
|
||
},
|
||
},
|
||
},
|
||
|
||
'Applying rotate': {
|
||
invoke: {
|
||
src: 'rotateAstMod',
|
||
id: 'rotateAstMod',
|
||
input: ({ event }) => {
|
||
if (event.type !== 'Rotate') return undefined
|
||
return event.data
|
||
},
|
||
onDone: ['idle'],
|
||
onError: {
|
||
target: 'idle',
|
||
actions: 'toastError',
|
||
},
|
||
},
|
||
},
|
||
|
||
'Applying clone': {
|
||
invoke: {
|
||
src: 'cloneAstMod',
|
||
id: 'cloneAstMod',
|
||
input: ({ event }) => {
|
||
if (event.type !== 'Clone') return undefined
|
||
return event.data
|
||
},
|
||
onDone: ['idle'],
|
||
onError: {
|
||
target: 'idle',
|
||
actions: 'toastError',
|
||
},
|
||
},
|
||
},
|
||
|
||
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'],
|
||
},
|
||
},
|
||
|
||
'Boolean subtracting': {
|
||
invoke: {
|
||
src: 'boolSubtractAstMod',
|
||
id: 'boolSubtractAstMod',
|
||
input: ({ event }) =>
|
||
event.type !== 'Boolean Subtract' ? undefined : event.data,
|
||
onDone: 'idle',
|
||
onError: {
|
||
target: 'idle',
|
||
actions: 'toastError',
|
||
},
|
||
},
|
||
},
|
||
|
||
'Boolean uniting': {
|
||
invoke: {
|
||
src: 'boolUnionAstMod',
|
||
id: 'boolUnionAstMod',
|
||
input: ({ event }) =>
|
||
event.type !== 'Boolean Union' ? undefined : event.data,
|
||
onDone: 'idle',
|
||
onError: {
|
||
target: 'idle',
|
||
actions: 'toastError',
|
||
},
|
||
},
|
||
},
|
||
|
||
'Boolean intersecting': {
|
||
invoke: {
|
||
src: 'boolIntersectAstMod',
|
||
id: 'boolIntersectAstMod',
|
||
input: ({ event }) =>
|
||
event.type !== 'Boolean Intersect' ? undefined : event.data,
|
||
onDone: 'idle',
|
||
onError: {
|
||
target: 'idle',
|
||
actions: 'toastError',
|
||
},
|
||
},
|
||
},
|
||
},
|
||
|
||
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',
|
||
},
|
||
'Toggle default plane visibility': {
|
||
reenter: false,
|
||
actions: 'Toggle default plane visibility',
|
||
},
|
||
},
|
||
})
|
||
|
||
export function isEditingExistingSketch({
|
||
sketchDetails,
|
||
}: {
|
||
sketchDetails: SketchDetails | null
|
||
}): boolean {
|
||
// should check that the variable declaration is a pipeExpression
|
||
// and that the pipeExpression contains a "startProfile" 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 === 'CallExpressionKw' &&
|
||
(maybePipeExpression.callee.name.name === 'startProfile' ||
|
||
maybePipeExpression.callee.name.name === 'circle' ||
|
||
maybePipeExpression.callee.name.name === 'circleThreePoint')
|
||
)
|
||
return true
|
||
if (maybePipeExpression.type !== 'PipeExpression') return false
|
||
const hasStartProfileAt = maybePipeExpression.body.some(
|
||
(item) =>
|
||
item.type === 'CallExpressionKw' &&
|
||
item.callee.name.name === 'startProfile'
|
||
)
|
||
const hasCircle =
|
||
maybePipeExpression.body.some(
|
||
(item) =>
|
||
item.type === 'CallExpressionKw' && item.callee.name.name === 'circle'
|
||
) ||
|
||
maybePipeExpression.body.some(
|
||
(item) =>
|
||
item.type === 'CallExpressionKw' &&
|
||
item.callee.name.name === 'circleThreePoint'
|
||
)
|
||
return (hasStartProfileAt && maybePipeExpression.body.length > 1) || hasCircle
|
||
}
|
||
|
||
const getSelectedPlane = (
|
||
selection: Selections
|
||
): Node<Name> | Node<Literal> | undefined => {
|
||
const defaultPlane = selection.otherSelections[0]
|
||
if (
|
||
defaultPlane &&
|
||
defaultPlane instanceof Object &&
|
||
'name' in defaultPlane
|
||
) {
|
||
return createLiteral(defaultPlane.name.toUpperCase())
|
||
}
|
||
|
||
const offsetPlane = selection.graphSelections[0]
|
||
if (offsetPlane.artifact?.type === 'plane') {
|
||
const artifactId = offsetPlane.artifact?.id
|
||
const variableName = Object.entries(kclManager.variables).find(
|
||
([_, value]) => {
|
||
return value?.type === 'Plane' && value.value?.artifactId === artifactId
|
||
}
|
||
)
|
||
const offsetPlaneName = variableName?.[0]
|
||
return offsetPlaneName ? createLocalName(offsetPlaneName) : undefined
|
||
}
|
||
|
||
return undefined
|
||
}
|
||
|
||
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.name === 'circle'
|
||
)
|
||
return hasCircle
|
||
}
|