2521 lines
82 KiB
TypeScript
2521 lines
82 KiB
TypeScript
import {
|
|
PathToNode,
|
|
ProgramMemory,
|
|
VariableDeclaration,
|
|
VariableDeclarator,
|
|
parse,
|
|
recast,
|
|
resultIsOk,
|
|
} from 'lang/wasm'
|
|
import {
|
|
Axis,
|
|
DefaultPlaneSelection,
|
|
Selections,
|
|
Selection,
|
|
updateSelections,
|
|
} from 'lib/selections'
|
|
import { assign, fromPromise, setup } from 'xstate'
|
|
import { SidebarType } from 'components/ModelingSidebar/ModelingPanes'
|
|
import {
|
|
isNodeSafeToReplacePath,
|
|
getNodePathFromSourceRange,
|
|
} from 'lang/queryAst'
|
|
import {
|
|
kclManager,
|
|
sceneInfra,
|
|
sceneEntitiesManager,
|
|
engineCommandManager,
|
|
editorManager,
|
|
codeManager,
|
|
} from 'lib/singletons'
|
|
import {
|
|
horzVertInfo,
|
|
applyConstraintHorzVert,
|
|
} from 'components/Toolbar/HorzVert'
|
|
import {
|
|
applyConstraintHorzVertAlign,
|
|
horzVertDistanceInfo,
|
|
} from 'components/Toolbar/SetHorzVertDistance'
|
|
import { angleBetweenInfo } from 'components/Toolbar/SetAngleBetween'
|
|
import { angleLengthInfo } from 'components/Toolbar/setAngleLength'
|
|
import {
|
|
applyConstraintEqualLength,
|
|
setEqualLengthInfo,
|
|
} from 'components/Toolbar/EqualLength'
|
|
import {
|
|
addOffsetPlane,
|
|
deleteFromSelection,
|
|
extrudeSketch,
|
|
loftSketches,
|
|
revolveSketch,
|
|
} from 'lang/modifyAst'
|
|
import {
|
|
applyEdgeTreatmentToSelection,
|
|
EdgeTreatmentType,
|
|
FilletParameters,
|
|
} from 'lang/modifyAst/addEdgeTreatment'
|
|
import { getNodeFromPath } from '../lang/queryAst'
|
|
import {
|
|
applyConstraintEqualAngle,
|
|
equalAngleInfo,
|
|
} from 'components/Toolbar/EqualAngle'
|
|
import {
|
|
applyRemoveConstrainingValues,
|
|
removeConstrainingValuesInfo,
|
|
} from 'components/Toolbar/RemoveConstrainingValues'
|
|
import { intersectInfo } from 'components/Toolbar/Intersect'
|
|
import {
|
|
absDistanceInfo,
|
|
applyConstraintAxisAlign,
|
|
} from 'components/Toolbar/SetAbsDistance'
|
|
import { ModelingCommandSchema } from 'lib/commandBarConfigs/modelingCommandConfig'
|
|
import { err, reportRejection, trap } from 'lib/trap'
|
|
import { DefaultPlaneStr, getFaceDetails } from 'clientSideScene/sceneEntities'
|
|
import { uuidv4 } from 'lib/utils'
|
|
import { Coords2d } from 'lang/std/sketch'
|
|
import { deleteSegment } from 'clientSideScene/ClientSideSceneComp'
|
|
import { executeAst } from 'lang/langHelpers'
|
|
import toast from 'react-hot-toast'
|
|
import { ToolbarModeName } from 'lib/toolbar'
|
|
import { quaternionFromUpNForward } from 'clientSideScene/helpers'
|
|
import { Vector3 } from 'three'
|
|
import { MachineManager } from 'components/MachineManagerProvider'
|
|
|
|
export const MODELING_PERSIST_KEY = 'MODELING_PERSIST_KEY'
|
|
|
|
export type SetSelections =
|
|
| {
|
|
selectionType: 'singleCodeCursor'
|
|
selection?: Selection
|
|
}
|
|
| {
|
|
selectionType: 'axisSelection'
|
|
selection: Axis
|
|
}
|
|
| {
|
|
selectionType: 'defaultPlaneSelection'
|
|
selection: DefaultPlaneSelection
|
|
}
|
|
| {
|
|
selectionType: 'completeSelection'
|
|
selection: Selections
|
|
updatedPathToNode?: 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 {
|
|
sketchPathToNode: PathToNode
|
|
zAxis: [number, number, number]
|
|
yAxis: [number, number, number]
|
|
origin: [number, number, number]
|
|
}
|
|
|
|
export interface SegmentOverlay {
|
|
windowCoords: Coords2d
|
|
angle: number
|
|
group: any
|
|
pathToNode: PathToNode
|
|
visible: boolean
|
|
}
|
|
|
|
export interface SegmentOverlays {
|
|
[pathToNodeString: string]: SegmentOverlay
|
|
}
|
|
|
|
export interface EdgeCutInfo {
|
|
type: 'edgeCut'
|
|
tagName: string
|
|
subType: 'base' | 'opposite' | 'adjacent'
|
|
}
|
|
|
|
export interface CapInfo {
|
|
type: 'cap'
|
|
subType: 'start' | 'end'
|
|
}
|
|
|
|
export type ExtrudeFacePlane = {
|
|
type: 'extrudeFace'
|
|
position: [number, number, number]
|
|
sketchPathToNode: PathToNode
|
|
extrudePathToNode: PathToNode
|
|
faceInfo:
|
|
| {
|
|
type: 'wall'
|
|
}
|
|
| CapInfo
|
|
| EdgeCutInfo
|
|
faceId: string
|
|
zAxis: [number, number, number]
|
|
yAxis: [number, number, number]
|
|
}
|
|
|
|
export type DefaultPlane = {
|
|
type: 'defaultPlane'
|
|
plane: DefaultPlaneStr
|
|
planeId: string
|
|
zAxis: [number, number, number]
|
|
yAxis: [number, number, number]
|
|
}
|
|
|
|
export type OffsetPlane = {
|
|
type: 'offsetPlane'
|
|
position: [number, number, number]
|
|
planeId: string
|
|
pathToNode: PathToNode
|
|
zAxis: [number, number, number]
|
|
yAxis: [number, number, number]
|
|
}
|
|
|
|
export type SegmentOverlayPayload =
|
|
| {
|
|
type: 'set-one'
|
|
pathToNodeString: string
|
|
seg: SegmentOverlay
|
|
}
|
|
| {
|
|
type: 'delete-one'
|
|
pathToNodeString: string
|
|
}
|
|
| { type: 'clear' }
|
|
| {
|
|
type: 'set-many'
|
|
overlays: SegmentOverlays
|
|
}
|
|
|
|
export interface Store {
|
|
videoElement?: HTMLVideoElement
|
|
openPanes: SidebarType[]
|
|
}
|
|
|
|
export type SketchTool =
|
|
| 'line'
|
|
| 'tangentialArc'
|
|
| 'rectangle'
|
|
| 'center rectangle'
|
|
| 'circle'
|
|
| 'none'
|
|
|
|
export type ModelingMachineEvent =
|
|
| {
|
|
type: 'Enter sketch'
|
|
data?: {
|
|
forceNewSketch?: boolean
|
|
}
|
|
}
|
|
| { type: 'Sketch On Face' }
|
|
| {
|
|
type: 'Select default plane'
|
|
data: DefaultPlane | ExtrudeFacePlane | OffsetPlane
|
|
}
|
|
| {
|
|
type: 'Set selection'
|
|
data: SetSelections
|
|
}
|
|
| {
|
|
type: 'Delete selection'
|
|
}
|
|
| { type: 'Sketch no face' }
|
|
| { type: 'Toggle gui mode' }
|
|
| { type: 'Cancel' }
|
|
| { type: 'CancelSketch' }
|
|
| { type: 'Add start point' }
|
|
| { 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' }
|
|
| { type: 'Constrain equal length' }
|
|
| { type: 'Constrain parallel' }
|
|
| { type: 'Constrain remove constraints'; data?: PathToNode }
|
|
| { type: 'Re-execute' }
|
|
| { type: 'Export'; data: ModelingCommandSchema['Export'] }
|
|
| { type: 'Make'; data: ModelingCommandSchema['Make'] }
|
|
| { type: 'Extrude'; data?: ModelingCommandSchema['Extrude'] }
|
|
| { type: 'Loft'; data?: ModelingCommandSchema['Loft'] }
|
|
| { type: 'Revolve'; data?: ModelingCommandSchema['Revolve'] }
|
|
| { type: 'Fillet'; data?: ModelingCommandSchema['Fillet'] }
|
|
| { type: 'Offset plane'; data: ModelingCommandSchema['Offset plane'] }
|
|
| { type: 'Text-to-CAD'; data: ModelingCommandSchema['Text-to-CAD'] }
|
|
| {
|
|
type: 'Add rectangle origin'
|
|
data: [x: number, y: number]
|
|
}
|
|
| {
|
|
type: 'Add center rectangle origin'
|
|
data: [x: number, y: number]
|
|
}
|
|
| {
|
|
type: 'Add circle origin'
|
|
data: [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: '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: 'Convert to variable'
|
|
data: {
|
|
pathToNode: PathToNode
|
|
variableName: string
|
|
}
|
|
}
|
|
| {
|
|
type: 'change tool'
|
|
data: {
|
|
tool: SketchTool
|
|
}
|
|
}
|
|
| { type: 'Finish rectangle' }
|
|
| { type: 'Finish center rectangle' }
|
|
| { type: 'Finish circle' }
|
|
| { type: 'Artifact graph populated' }
|
|
| { type: 'Artifact graph emptied' }
|
|
|
|
export type MoveDesc = { line: number; snippet: string }
|
|
|
|
export const PERSIST_MODELING_CONTEXT = 'persistModelingContext'
|
|
interface PersistedModelingContext {
|
|
openPanes: Store['openPanes']
|
|
}
|
|
|
|
type PersistedKeys = keyof PersistedModelingContext
|
|
export const PersistedValues: PersistedKeys[] = ['openPanes']
|
|
|
|
export const getPersistedContext = (): Partial<PersistedModelingContext> => {
|
|
const c = (typeof window !== 'undefined' &&
|
|
JSON.parse(localStorage.getItem(PERSIST_MODELING_CONTEXT) || '{}')) || {
|
|
openPanes: ['code'],
|
|
}
|
|
return c
|
|
}
|
|
|
|
export interface ModelingMachineContext {
|
|
currentMode: ToolbarModeName
|
|
currentTool: SketchTool
|
|
machineManager: MachineManager
|
|
selection: string[]
|
|
selectionRanges: Selections
|
|
sketchDetails: SketchDetails | null
|
|
sketchPlaneId: string
|
|
sketchEnginePathId: string
|
|
moveDescs: MoveDesc[]
|
|
mouseState: MouseState
|
|
segmentOverlays: SegmentOverlays
|
|
segmentHoverMap: { [pathToNodeString: string]: number }
|
|
store: Store
|
|
}
|
|
export const modelingMachineDefaultContext: ModelingMachineContext = {
|
|
currentMode: 'modeling',
|
|
currentTool: 'none',
|
|
machineManager: {
|
|
machines: [],
|
|
machineApiIp: null,
|
|
currentMachine: null,
|
|
setCurrentMachine: () => {},
|
|
noMachinesReason: () => undefined,
|
|
},
|
|
selection: [],
|
|
selectionRanges: {
|
|
otherSelections: [],
|
|
graphSelections: [],
|
|
},
|
|
sketchDetails: {
|
|
sketchPathToNode: [],
|
|
zAxis: [0, 0, 1],
|
|
yAxis: [0, 1, 0],
|
|
origin: [0, 0, 0],
|
|
},
|
|
sketchPlaneId: '',
|
|
sketchEnginePathId: '',
|
|
moveDescs: [],
|
|
mouseState: { type: 'idle' },
|
|
segmentOverlays: {},
|
|
segmentHoverMap: {},
|
|
store: {
|
|
openPanes: getPersistedContext().openPanes || ['code'],
|
|
},
|
|
}
|
|
|
|
export const modelingMachine = setup({
|
|
types: {
|
|
context: {} as ModelingMachineContext,
|
|
events: {} as ModelingMachineEvent,
|
|
input: {} as ModelingMachineContext,
|
|
},
|
|
guards: {
|
|
'Selection is on face': () => false,
|
|
'has valid sweep selection': () => false,
|
|
'has valid loft selection': () => false,
|
|
'has valid edge treatment selection': () => false,
|
|
'Has exportable geometry': () => false,
|
|
'has valid selection for deletion': () => false,
|
|
'has made first point': ({ context }) => {
|
|
if (!context.sketchDetails?.sketchPathToNode) return false
|
|
const variableDeclaration = getNodeFromPath<VariableDeclarator>(
|
|
kclManager.ast,
|
|
context.sketchDetails.sketchPathToNode,
|
|
'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 hasStartSketchOn = pipeExpression.body.some(
|
|
(item) =>
|
|
item.type === 'CallExpression' && item.callee.name === 'startSketchOn'
|
|
)
|
|
return hasStartSketchOn && pipeExpression.body.length > 1
|
|
},
|
|
'is editing existing sketch': ({ context: { sketchDetails } }) =>
|
|
isEditingExistingSketch({ sketchDetails }),
|
|
'Can make selection horizontal': ({ context: { selectionRanges } }) => {
|
|
const info = horzVertInfo(selectionRanges, 'horizontal')
|
|
if (trap(info)) return false
|
|
return info.enabled
|
|
},
|
|
'Can make selection vertical': ({ context: { selectionRanges } }) => {
|
|
const info = horzVertInfo(selectionRanges, 'vertical')
|
|
if (trap(info)) return false
|
|
return info.enabled
|
|
},
|
|
'Can constrain horizontal distance': ({ context: { selectionRanges } }) => {
|
|
const info = horzVertDistanceInfo({
|
|
selectionRanges: selectionRanges,
|
|
constraint: 'setHorzDistance',
|
|
})
|
|
if (trap(info)) return false
|
|
return info.enabled
|
|
},
|
|
'Can constrain vertical distance': ({ context: { selectionRanges } }) => {
|
|
const info = horzVertDistanceInfo({
|
|
selectionRanges: selectionRanges,
|
|
constraint: 'setVertDistance',
|
|
})
|
|
if (trap(info)) return false
|
|
return info.enabled
|
|
},
|
|
'Can constrain ABS X': ({ context: { selectionRanges } }) => {
|
|
const info = absDistanceInfo({
|
|
selectionRanges,
|
|
constraint: 'xAbs',
|
|
})
|
|
if (trap(info)) return false
|
|
return info.enabled
|
|
},
|
|
'Can constrain ABS Y': ({ context: { selectionRanges } }) => {
|
|
const info = absDistanceInfo({
|
|
selectionRanges,
|
|
constraint: 'yAbs',
|
|
})
|
|
if (trap(info)) return false
|
|
return info.enabled
|
|
},
|
|
'Can constrain angle': ({ context: { selectionRanges } }) => {
|
|
const angleBetween = angleBetweenInfo({
|
|
selectionRanges,
|
|
})
|
|
if (trap(angleBetween)) return false
|
|
const angleLength = angleLengthInfo({
|
|
selectionRanges,
|
|
angleOrLength: 'setAngle',
|
|
})
|
|
if (trap(angleLength)) return false
|
|
return angleBetween.enabled || angleLength.enabled
|
|
},
|
|
'Can constrain length': ({ context: { selectionRanges } }) => {
|
|
const angleLength = angleLengthInfo({
|
|
selectionRanges,
|
|
})
|
|
if (trap(angleLength)) return false
|
|
return angleLength.enabled
|
|
},
|
|
'Can constrain perpendicular distance': ({
|
|
context: { selectionRanges },
|
|
}) => {
|
|
const info = intersectInfo({ selectionRanges })
|
|
if (trap(info)) return false
|
|
return info.enabled
|
|
},
|
|
'Can constrain horizontally align': ({ context: { selectionRanges } }) => {
|
|
const info = horzVertDistanceInfo({
|
|
selectionRanges: selectionRanges,
|
|
constraint: 'setHorzDistance',
|
|
})
|
|
if (trap(info)) return false
|
|
return info.enabled
|
|
},
|
|
'Can constrain vertically align': ({ context: { selectionRanges } }) => {
|
|
const info = horzVertDistanceInfo({
|
|
selectionRanges: selectionRanges,
|
|
constraint: 'setHorzDistance',
|
|
})
|
|
if (trap(info)) return false
|
|
return info.enabled
|
|
},
|
|
'Can constrain snap to X': ({ context: { selectionRanges } }) => {
|
|
const info = absDistanceInfo({
|
|
selectionRanges,
|
|
constraint: 'snapToXAxis',
|
|
})
|
|
if (trap(info)) return false
|
|
return info.enabled
|
|
},
|
|
'Can constrain snap to Y': ({ context: { selectionRanges } }) => {
|
|
const info = absDistanceInfo({
|
|
selectionRanges,
|
|
constraint: 'snapToYAxis',
|
|
})
|
|
if (trap(info)) return false
|
|
return info.enabled
|
|
},
|
|
'Can constrain equal length': ({ context: { selectionRanges } }) => {
|
|
const info = setEqualLengthInfo({
|
|
selectionRanges,
|
|
})
|
|
if (trap(info)) return false
|
|
return info.enabled
|
|
},
|
|
'Can canstrain parallel': ({ context: { selectionRanges } }) => {
|
|
const info = equalAngleInfo({
|
|
selectionRanges,
|
|
})
|
|
if (err(info)) return false
|
|
return info.enabled
|
|
},
|
|
'Can constrain remove constraints': ({
|
|
context: { selectionRanges },
|
|
event,
|
|
}) => {
|
|
if (event.type !== 'Constrain remove constraints') return false
|
|
const info = removeConstrainingValuesInfo({
|
|
selectionRanges,
|
|
pathToNodes: event.data && [event.data],
|
|
})
|
|
if (trap(info)) return false
|
|
return info.enabled
|
|
},
|
|
'Can convert to variable': ({ event }) => {
|
|
if (event.type !== 'Convert to variable') 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.pathToNode
|
|
)
|
|
if (err(isSafeRetVal)) return false
|
|
return isSafeRetVal.isSafe
|
|
},
|
|
'next is tangential arc': ({ context: { sketchDetails, currentTool } }) =>
|
|
currentTool === 'tangentialArc' &&
|
|
isEditingExistingSketch({ sketchDetails }),
|
|
|
|
'next is rectangle': ({ context: { sketchDetails, currentTool } }) =>
|
|
currentTool === 'rectangle' &&
|
|
canRectangleOrCircleTool({ sketchDetails }),
|
|
'next is center rectangle': ({ context: { sketchDetails, currentTool } }) =>
|
|
currentTool === 'center rectangle' &&
|
|
canRectangleOrCircleTool({ sketchDetails }),
|
|
'next is circle': ({ context: { sketchDetails, currentTool } }) =>
|
|
currentTool === 'circle' && canRectangleOrCircleTool({ sketchDetails }),
|
|
'next is line': ({ context }) => context.currentTool === 'line',
|
|
'next is none': ({ context }) => context.currentTool === 'none',
|
|
},
|
|
// end guards
|
|
actions: {
|
|
'assign tool in context': assign({
|
|
currentTool: ({ event }) =>
|
|
'data' in event && event.data && 'tool' in event.data
|
|
? event.data.tool
|
|
: 'none',
|
|
}),
|
|
'reset selections': assign({
|
|
selectionRanges: { graphSelections: [], otherSelections: [] },
|
|
}),
|
|
'enter sketching mode': assign({ currentMode: 'sketching' }),
|
|
'enter modeling mode': assign({ currentMode: 'modeling' }),
|
|
'set sketchMetadata from pathToNode': assign(
|
|
({ context: { sketchDetails } }) => {
|
|
if (!sketchDetails?.sketchPathToNode || !sketchDetails) return {}
|
|
return {
|
|
sketchDetails: {
|
|
...sketchDetails,
|
|
sketchPathToNode: sketchDetails.sketchPathToNode,
|
|
},
|
|
}
|
|
}
|
|
),
|
|
'hide default planes': () => {
|
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
kclManager.hidePlanes()
|
|
},
|
|
'reset sketch metadata': assign({
|
|
sketchDetails: null,
|
|
sketchEnginePathId: '',
|
|
sketchPlaneId: '',
|
|
}),
|
|
'reset camera position': () => {
|
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
engineCommandManager.sendSceneCommand({
|
|
type: 'modeling_cmd_req',
|
|
cmd_id: uuidv4(),
|
|
cmd: {
|
|
type: 'default_camera_look_at',
|
|
center: { x: 0, y: 0, z: 0 },
|
|
vantage: { x: 0, y: -1250, z: 580 },
|
|
up: { x: 0, y: 0, z: 1 },
|
|
},
|
|
})
|
|
},
|
|
'set new sketch metadata': assign(({ event }) => {
|
|
if (
|
|
event.type !== 'xstate.done.actor.animate-to-sketch' &&
|
|
event.type !== 'xstate.done.actor.animate-to-face'
|
|
)
|
|
return {}
|
|
return {
|
|
sketchDetails: event.output,
|
|
}
|
|
}),
|
|
'AST extrude': ({ context: { store }, event }) => {
|
|
if (event.type !== 'Extrude') return
|
|
;(async () => {
|
|
if (!event.data) return
|
|
const { selection, distance } = event.data
|
|
let ast = kclManager.ast
|
|
if (
|
|
'variableName' in distance &&
|
|
distance.variableName &&
|
|
distance.insertIndex !== undefined
|
|
) {
|
|
const newBody = [...ast.body]
|
|
newBody.splice(
|
|
distance.insertIndex,
|
|
0,
|
|
distance.variableDeclarationAst
|
|
)
|
|
ast.body = newBody
|
|
}
|
|
const pathToNode = getNodePathFromSourceRange(
|
|
ast,
|
|
selection.graphSelections[0]?.codeRef.range
|
|
)
|
|
const extrudeSketchRes = extrudeSketch(
|
|
ast,
|
|
pathToNode,
|
|
false,
|
|
'variableName' in distance
|
|
? distance.variableIdentifierAst
|
|
: distance.valueAst
|
|
)
|
|
if (trap(extrudeSketchRes)) return
|
|
const { modifiedAst, pathToExtrudeArg } = extrudeSketchRes
|
|
|
|
const updatedAst = await kclManager.updateAst(modifiedAst, true, {
|
|
focusPath: [pathToExtrudeArg],
|
|
zoomToFit: true,
|
|
zoomOnRangeAndType: {
|
|
range: selection.graphSelections[0]?.codeRef.range,
|
|
type: 'path',
|
|
},
|
|
})
|
|
|
|
await codeManager.updateEditorWithAstAndWriteToFile(updatedAst.newAst)
|
|
|
|
if (updatedAst?.selections) {
|
|
editorManager.selectRange(updatedAst?.selections)
|
|
}
|
|
})().catch(reportRejection)
|
|
},
|
|
'AST revolve': ({ context: { store }, event }) => {
|
|
if (event.type !== 'Revolve') return
|
|
;(async () => {
|
|
if (!event.data) return
|
|
const { selection, angle } = event.data
|
|
let ast = kclManager.ast
|
|
if (
|
|
'variableName' in angle &&
|
|
angle.variableName &&
|
|
angle.insertIndex !== undefined
|
|
) {
|
|
const newBody = [...ast.body]
|
|
newBody.splice(angle.insertIndex, 0, angle.variableDeclarationAst)
|
|
ast.body = newBody
|
|
}
|
|
const pathToNode = getNodePathFromSourceRange(
|
|
ast,
|
|
selection.graphSelections[0]?.codeRef.range
|
|
)
|
|
const revolveSketchRes = revolveSketch(
|
|
ast,
|
|
pathToNode,
|
|
false,
|
|
'variableName' in angle ? angle.variableIdentifierAst : angle.valueAst
|
|
)
|
|
if (trap(revolveSketchRes)) return
|
|
const { modifiedAst, pathToRevolveArg } = revolveSketchRes
|
|
|
|
const updatedAst = await kclManager.updateAst(modifiedAst, true, {
|
|
focusPath: [pathToRevolveArg],
|
|
zoomToFit: true,
|
|
zoomOnRangeAndType: {
|
|
range: selection.graphSelections[0]?.codeRef.range,
|
|
type: 'path',
|
|
},
|
|
})
|
|
|
|
await codeManager.updateEditorWithAstAndWriteToFile(updatedAst.newAst)
|
|
|
|
if (updatedAst?.selections) {
|
|
editorManager.selectRange(updatedAst?.selections)
|
|
}
|
|
})().catch(reportRejection)
|
|
},
|
|
'AST delete selection': ({ context: { selectionRanges } }) => {
|
|
;(async () => {
|
|
let ast = kclManager.ast
|
|
|
|
const modifiedAst = await deleteFromSelection(
|
|
ast,
|
|
selectionRanges.graphSelections[0],
|
|
kclManager.programMemory,
|
|
getFaceDetails
|
|
)
|
|
if (err(modifiedAst)) return
|
|
|
|
const testExecute = await executeAst({
|
|
ast: modifiedAst,
|
|
engineCommandManager,
|
|
// We make sure to send an empty program memory to denote we mean mock mode.
|
|
programMemoryOverride: ProgramMemory.empty(),
|
|
})
|
|
if (testExecute.errors.length) {
|
|
toast.error('Unable to delete part')
|
|
return
|
|
}
|
|
|
|
await kclManager.updateAst(modifiedAst, true)
|
|
await codeManager.updateEditorWithAstAndWriteToFile(modifiedAst)
|
|
})().catch(reportRejection)
|
|
},
|
|
'AST fillet': ({ event }) => {
|
|
if (event.type !== 'Fillet') return
|
|
if (!event.data) return
|
|
|
|
// Extract inputs
|
|
const ast = kclManager.ast
|
|
const { selection, radius } = event.data
|
|
const parameters: FilletParameters = {
|
|
type: EdgeTreatmentType.Fillet,
|
|
radius,
|
|
}
|
|
|
|
// Apply fillet to selection
|
|
const applyEdgeTreatmentToSelectionResult = applyEdgeTreatmentToSelection(
|
|
ast,
|
|
selection,
|
|
parameters
|
|
)
|
|
if (err(applyEdgeTreatmentToSelectionResult))
|
|
return applyEdgeTreatmentToSelectionResult
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
|
|
},
|
|
'set selection filter to curves only': () => {
|
|
;(async () => {
|
|
await engineCommandManager.sendSceneCommand({
|
|
type: 'modeling_cmd_req',
|
|
cmd_id: uuidv4(),
|
|
cmd: {
|
|
type: 'set_selection_filter',
|
|
filter: ['curve'],
|
|
},
|
|
})
|
|
})().catch(reportRejection)
|
|
},
|
|
'setup client side sketch segments': ({
|
|
context: { sketchDetails, selectionRanges },
|
|
}) => {
|
|
if (!sketchDetails) return
|
|
;(async () => {
|
|
if (Object.keys(sceneEntitiesManager.activeSegments).length > 0) {
|
|
await sceneEntitiesManager.tearDownSketch({ removeAxis: false })
|
|
}
|
|
sceneInfra.resetMouseListeners()
|
|
await sceneEntitiesManager.setupSketch({
|
|
sketchPathToNode: sketchDetails?.sketchPathToNode || [],
|
|
forward: sketchDetails.zAxis,
|
|
up: sketchDetails.yAxis,
|
|
position: sketchDetails.origin,
|
|
maybeModdedAst: kclManager.ast,
|
|
selectionRanges,
|
|
})
|
|
sceneInfra.resetMouseListeners()
|
|
sceneEntitiesManager.setupSketchIdleCallbacks({
|
|
pathToNode: sketchDetails?.sketchPathToNode || [],
|
|
forward: sketchDetails.zAxis,
|
|
up: sketchDetails.yAxis,
|
|
position: sketchDetails.origin,
|
|
})
|
|
})().catch(reportRejection)
|
|
},
|
|
'tear down client sketch': () => {
|
|
if (sceneEntitiesManager.activeSegments) {
|
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
sceneEntitiesManager.tearDownSketch({ removeAxis: false })
|
|
}
|
|
},
|
|
'remove sketch grid': () => sceneEntitiesManager.removeSketchGrid(),
|
|
'set up draft line': ({ context: { sketchDetails } }) => {
|
|
if (!sketchDetails) return
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
sceneEntitiesManager
|
|
.setupDraftSegment(
|
|
sketchDetails.sketchPathToNode,
|
|
sketchDetails.zAxis,
|
|
sketchDetails.yAxis,
|
|
sketchDetails.origin,
|
|
'line'
|
|
)
|
|
.then(() => {
|
|
return codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
|
|
})
|
|
},
|
|
'set up draft arc': ({ context: { sketchDetails } }) => {
|
|
if (!sketchDetails) return
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
sceneEntitiesManager
|
|
.setupDraftSegment(
|
|
sketchDetails.sketchPathToNode,
|
|
sketchDetails.zAxis,
|
|
sketchDetails.yAxis,
|
|
sketchDetails.origin,
|
|
'tangentialArcTo'
|
|
)
|
|
.then(() => {
|
|
return codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
|
|
})
|
|
},
|
|
'listen for rectangle origin': ({ context: { sketchDetails } }) => {
|
|
if (!sketchDetails) return
|
|
sceneEntitiesManager.setupNoPointsListener({
|
|
sketchDetails,
|
|
afterClick: (args) => {
|
|
const twoD = args.intersectionPoint?.twoD
|
|
if (twoD) {
|
|
sceneInfra.modelingSend({
|
|
type: 'Add rectangle origin',
|
|
data: [twoD.x, twoD.y],
|
|
})
|
|
} else {
|
|
console.error('No intersection point found')
|
|
}
|
|
},
|
|
})
|
|
},
|
|
|
|
'listen for center rectangle origin': ({ context: { sketchDetails } }) => {
|
|
if (!sketchDetails) return
|
|
// setupNoPointsListener has the code for startProfileAt onClick
|
|
sceneEntitiesManager.setupNoPointsListener({
|
|
sketchDetails,
|
|
afterClick: (args) => {
|
|
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
|
|
if (sceneEntitiesManager.intersectionPlane) {
|
|
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 || !sketchDetails?.sketchPathToNode)
|
|
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')
|
|
}
|
|
},
|
|
})
|
|
},
|
|
'set up draft rectangle': ({ context: { sketchDetails }, event }) => {
|
|
if (event.type !== 'Add rectangle origin') return
|
|
if (!sketchDetails || !event.data) return
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
sceneEntitiesManager
|
|
.setupDraftRectangle(
|
|
sketchDetails.sketchPathToNode,
|
|
sketchDetails.zAxis,
|
|
sketchDetails.yAxis,
|
|
sketchDetails.origin,
|
|
event.data
|
|
)
|
|
.then(() => {
|
|
return codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
|
|
})
|
|
},
|
|
'set up draft center rectangle': ({
|
|
context: { sketchDetails },
|
|
event,
|
|
}) => {
|
|
if (event.type !== 'Add center rectangle origin') return
|
|
if (!sketchDetails || !event.data) return
|
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
sceneEntitiesManager.setupDraftCenterRectangle(
|
|
sketchDetails.sketchPathToNode,
|
|
sketchDetails.zAxis,
|
|
sketchDetails.yAxis,
|
|
sketchDetails.origin,
|
|
event.data
|
|
)
|
|
},
|
|
'set up draft circle': ({ context: { sketchDetails }, event }) => {
|
|
if (event.type !== 'Add circle origin') return
|
|
if (!sketchDetails || !event.data) return
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
sceneEntitiesManager
|
|
.setupDraftCircle(
|
|
sketchDetails.sketchPathToNode,
|
|
sketchDetails.zAxis,
|
|
sketchDetails.yAxis,
|
|
sketchDetails.origin,
|
|
event.data
|
|
)
|
|
.then(() => {
|
|
return codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
|
|
})
|
|
},
|
|
'set up draft line without teardown': ({ context: { sketchDetails } }) => {
|
|
if (!sketchDetails) return
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
sceneEntitiesManager
|
|
.setupDraftSegment(
|
|
sketchDetails.sketchPathToNode,
|
|
sketchDetails.zAxis,
|
|
sketchDetails.yAxis,
|
|
sketchDetails.origin,
|
|
'line',
|
|
false
|
|
)
|
|
.then(() => {
|
|
return codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
|
|
})
|
|
},
|
|
'show default planes': () => {
|
|
console.log('show default planes')
|
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
kclManager.showPlanes()
|
|
},
|
|
'setup noPoints onClick listener': ({ context: { sketchDetails } }) => {
|
|
if (!sketchDetails) return
|
|
sceneEntitiesManager.setupNoPointsListener({
|
|
sketchDetails,
|
|
afterClick: () => sceneInfra.modelingSend({ type: 'Add start point' }),
|
|
})
|
|
},
|
|
'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.sketchPathToNode || [],
|
|
sketchDetails.zAxis,
|
|
sketchDetails.yAxis,
|
|
sketchDetails.origin
|
|
)
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
|
|
},
|
|
'reset client scene mouse handlers': () => {
|
|
// when not in sketch mode we don't need any mouse listeners
|
|
// (note the orbit controls are always active though)
|
|
sceneInfra.resetMouseListeners()
|
|
},
|
|
'clientToEngine cam sync direction': () => {
|
|
sceneInfra.camControls.syncDirection = 'clientToEngine'
|
|
},
|
|
'engineToClient cam sync direction': () => {
|
|
sceneInfra.camControls.syncDirection = 'engineToClient'
|
|
},
|
|
/** TODO: this action is hiding unawaited asynchronous code */
|
|
'set selection filter to faces only': () => {
|
|
kclManager.setSelectionFilter(['face', 'object'])
|
|
},
|
|
/** TODO: this action is hiding unawaited asynchronous code */
|
|
'set selection filter to defaults': () =>
|
|
kclManager.defaultSelectionFilter(),
|
|
'Delete segment': ({ context: { sketchDetails }, event }) => {
|
|
if (event.type !== 'Delete segment') return
|
|
if (!sketchDetails || !event.data) return
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
deleteSegment({
|
|
pathToNode: event.data,
|
|
sketchDetails,
|
|
}).then(() => {
|
|
return codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
|
|
})
|
|
},
|
|
'Reset Segment Overlays': () => sceneEntitiesManager.resetOverlays(),
|
|
'Set context': assign({
|
|
store: ({ context: { store }, event }) => {
|
|
if (event.type !== 'Set context') return store
|
|
if (!event.data) return store
|
|
|
|
const result = {
|
|
...store,
|
|
...event.data,
|
|
}
|
|
const persistedContext: Partial<PersistedModelingContext> = {}
|
|
for (const key of PersistedValues) {
|
|
persistedContext[key] = result[key]
|
|
}
|
|
if (typeof window !== 'undefined') {
|
|
window.localStorage.setItem(
|
|
PERSIST_MODELING_CONTEXT,
|
|
JSON.stringify(persistedContext)
|
|
)
|
|
}
|
|
return result
|
|
},
|
|
}),
|
|
Make: () => {},
|
|
'enable copilot': () => {},
|
|
'disable copilot': () => {},
|
|
'Set selection': () => {},
|
|
'Set mouse state': () => {},
|
|
'Set Segment Overlays': () => {},
|
|
'Center camera on selection': () => {},
|
|
'Engine export': () => {},
|
|
'Submit to Text-to-CAD API': () => {},
|
|
'Set sketchDetails': () => {},
|
|
'sketch exit execute': () => {},
|
|
},
|
|
// end actions
|
|
actors: {
|
|
'do-constrain-remove-constraint': fromPromise(
|
|
async ({
|
|
input: { selectionRanges, sketchDetails, data },
|
|
}: {
|
|
input: Pick<
|
|
ModelingMachineContext,
|
|
'selectionRanges' | 'sketchDetails'
|
|
> & { data?: PathToNode }
|
|
}) => {
|
|
const constraint = applyRemoveConstrainingValues({
|
|
selectionRanges,
|
|
pathToNodes: data && [data],
|
|
})
|
|
if (trap(constraint)) return
|
|
const { pathToNodeMap } = constraint
|
|
if (!sketchDetails) return
|
|
let updatedAst = await sceneEntitiesManager.updateAstAndRejigSketch(
|
|
pathToNodeMap[0],
|
|
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.programMemory
|
|
)
|
|
if (trap(constraint)) return false
|
|
const { modifiedAst, pathToNodeMap } = constraint
|
|
if (!sketchDetails) return
|
|
const updatedAst = await sceneEntitiesManager.updateAstAndRejigSketch(
|
|
sketchDetails.sketchPathToNode,
|
|
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.programMemory
|
|
)
|
|
if (trap(constraint)) return false
|
|
const { modifiedAst, pathToNodeMap } = constraint
|
|
if (!sketchDetails) return
|
|
const updatedAst = await sceneEntitiesManager.updateAstAndRejigSketch(
|
|
sketchDetails.sketchPathToNode || [],
|
|
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?.sketchPathToNode || [],
|
|
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?.sketchPathToNode || [],
|
|
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?.sketchPathToNode || [],
|
|
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?.sketchPathToNode || [],
|
|
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?.sketchPathToNode || [],
|
|
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?.sketchPathToNode || [],
|
|
modifiedAst,
|
|
sketchDetails.zAxis,
|
|
sketchDetails.yAxis,
|
|
sketchDetails.origin
|
|
)
|
|
if (trap(updatedAst, { suppress: true })) return
|
|
if (!updatedAst) return
|
|
await codeManager.updateEditorWithAstAndWriteToFile(updatedAst.newAst)
|
|
const updatedSelectionRanges = updateSelections(
|
|
pathToNodeMap,
|
|
selectionRanges,
|
|
updatedAst.newAst
|
|
)
|
|
return {
|
|
selectionType: 'completeSelection',
|
|
selection: updatedSelectionRanges,
|
|
}
|
|
}
|
|
),
|
|
'Get vertical info': fromPromise(
|
|
async (_: {
|
|
input: Pick<ModelingMachineContext, 'selectionRanges' | 'sketchDetails'>
|
|
}) => {
|
|
return {} as SetSelections
|
|
}
|
|
),
|
|
'Get ABS X info': fromPromise(
|
|
async (_: {
|
|
input: Pick<ModelingMachineContext, 'selectionRanges' | 'sketchDetails'>
|
|
}) => {
|
|
return {} as SetSelections
|
|
}
|
|
),
|
|
'Get ABS Y info': fromPromise(
|
|
async (_: {
|
|
input: Pick<ModelingMachineContext, 'selectionRanges' | 'sketchDetails'>
|
|
}) => {
|
|
return {} as SetSelections
|
|
}
|
|
),
|
|
'Get angle info': fromPromise(
|
|
async (_: {
|
|
input: Pick<ModelingMachineContext, 'selectionRanges' | 'sketchDetails'>
|
|
}) => {
|
|
return {} as SetSelections
|
|
}
|
|
),
|
|
'Get perpendicular distance info': fromPromise(
|
|
async (_: {
|
|
input: Pick<ModelingMachineContext, 'selectionRanges' | 'sketchDetails'>
|
|
}) => {
|
|
return {} as SetSelections
|
|
}
|
|
),
|
|
'AST-undo-startSketchOn': fromPromise(
|
|
async (_: { input: Pick<ModelingMachineContext, 'sketchDetails'> }) => {
|
|
return undefined
|
|
}
|
|
),
|
|
'animate-to-face': fromPromise(
|
|
async (_: { input?: ExtrudeFacePlane | DefaultPlane | OffsetPlane }) => {
|
|
return {} as
|
|
| undefined
|
|
| {
|
|
sketchPathToNode: PathToNode
|
|
zAxis: [number, number, number]
|
|
yAxis: [number, number, number]
|
|
origin: [number, number, number]
|
|
}
|
|
}
|
|
),
|
|
'animate-to-sketch': fromPromise(
|
|
async (_: { input: Pick<ModelingMachineContext, 'selectionRanges'> }) => {
|
|
return {} as {
|
|
sketchPathToNode: PathToNode
|
|
zAxis: [number, number, number]
|
|
yAxis: [number, number, number]
|
|
origin: [number, number, number]
|
|
}
|
|
}
|
|
),
|
|
'Get horizontal info': fromPromise(
|
|
async (_: {
|
|
input: Pick<ModelingMachineContext, 'sketchDetails' | 'selectionRanges'>
|
|
}) => {
|
|
return {} as SetSelections
|
|
}
|
|
),
|
|
'Get length info': fromPromise(
|
|
async (_: {
|
|
input: Pick<ModelingMachineContext, 'sketchDetails' | 'selectionRanges'>
|
|
}) => {
|
|
return {} as SetSelections
|
|
}
|
|
),
|
|
'Get convert to variable info': fromPromise(
|
|
async (_: {
|
|
input: Pick<
|
|
ModelingMachineContext,
|
|
'sketchDetails' | 'selectionRanges'
|
|
> & {
|
|
data?: {
|
|
variableName: string
|
|
pathToNode: PathToNode
|
|
}
|
|
}
|
|
}) => {
|
|
return {} as SetSelections
|
|
}
|
|
),
|
|
offsetPlaneAstMod: fromPromise(
|
|
async ({
|
|
input,
|
|
}: {
|
|
input: ModelingCommandSchema['Offset plane'] | undefined
|
|
}) => {
|
|
if (!input) return new Error('No input provided')
|
|
// Extract inputs
|
|
const ast = kclManager.ast
|
|
const { plane: selection, distance } = input
|
|
|
|
// Extract the default plane from selection
|
|
const plane = selection.otherSelections[0]
|
|
if (!(plane && plane instanceof Object && 'name' in plane))
|
|
return trap('No plane selected')
|
|
|
|
// Insert the distance variable if it exists
|
|
if (
|
|
'variableName' in distance &&
|
|
distance.variableName &&
|
|
distance.insertIndex !== undefined
|
|
) {
|
|
const newBody = [...ast.body]
|
|
newBody.splice(
|
|
distance.insertIndex,
|
|
0,
|
|
distance.variableDeclarationAst
|
|
)
|
|
ast.body = newBody
|
|
}
|
|
|
|
// Get the default plane name from the selection
|
|
|
|
const offsetPlaneResult = addOffsetPlane({
|
|
node: ast,
|
|
defaultPlane: plane.name,
|
|
offset:
|
|
'variableName' in distance
|
|
? distance.variableIdentifierAst
|
|
: distance.valueAst,
|
|
})
|
|
|
|
const updateAstResult = await kclManager.updateAst(
|
|
offsetPlaneResult.modifiedAst,
|
|
true,
|
|
{
|
|
focusPath: [offsetPlaneResult.pathToNode],
|
|
}
|
|
)
|
|
|
|
await codeManager.updateEditorWithAstAndWriteToFile(
|
|
updateAstResult.newAst
|
|
)
|
|
|
|
if (updateAstResult?.selections) {
|
|
editorManager.selectRange(updateAstResult?.selections)
|
|
}
|
|
}
|
|
),
|
|
loftAstMod: fromPromise(
|
|
async ({
|
|
input,
|
|
}: {
|
|
input: ModelingCommandSchema['Loft'] | undefined
|
|
}) => {
|
|
if (!input) return new Error('No input provided')
|
|
// Extract inputs
|
|
const ast = kclManager.ast
|
|
const { selection } = input
|
|
const declarators = selection.graphSelections.flatMap((s) => {
|
|
const path = getNodePathFromSourceRange(ast, s?.codeRef.range)
|
|
const nodeFromPath = getNodeFromPath<VariableDeclarator>(
|
|
ast,
|
|
path,
|
|
'VariableDeclarator'
|
|
)
|
|
return err(nodeFromPath) ? [] : nodeFromPath.node
|
|
})
|
|
|
|
// TODO: add better validation on selection
|
|
if (!(declarators && declarators.length > 1)) {
|
|
trap('Not enough sketches selected')
|
|
}
|
|
|
|
// Perform the loft
|
|
const loftSketchesRes = loftSketches(ast, declarators)
|
|
const updateAstResult = await kclManager.updateAst(
|
|
loftSketchesRes.modifiedAst,
|
|
true,
|
|
{
|
|
focusPath: [loftSketchesRes.pathToNode],
|
|
}
|
|
)
|
|
|
|
await codeManager.updateEditorWithAstAndWriteToFile(
|
|
updateAstResult.newAst
|
|
)
|
|
|
|
if (updateAstResult?.selections) {
|
|
editorManager.selectRange(updateAstResult?.selections)
|
|
}
|
|
}
|
|
),
|
|
},
|
|
// end services
|
|
}).createMachine({
|
|
/** @xstate-layout N4IgpgJg5mDOIC5QFkD2EwBsCWA7KAxAMICGuAxlgNoAMAuoqAA6qzYAu2qujIAHogC0ANhoBWAHQAOAMwB2KQEY5AFgCcGqWqkAaEAE9Ew0RLEqa64TIBMKmTUXCAvk71oMOfAQDKYdgAJYLDByTm5aBiQQFjYwniiBBEEpYSkJOUUaOWsxeylrWzk9QwQClQkrVOsZNWExFItnVxB3LDxCXwCAW1QAVyDA9hJ2MAjeGI4ueNBE5KkaCWspZfMM+XE1YsQZMQWxNQtxao0ZeRc3dDavTv9ybhG+djGoibjeWZSZCRUxJa1flTCNTWLYIYS2CT2RSKdRyGhZawWc4tS6eDp+fy+KBdMC4AIAeQAbmAAE6YEj6WDPZisSbcd5CYHCSFqOQ1NSKWRiMRA0HqSTKblSX48+pKZGtNHEXEjEm3Eg4kkkfzcQLBUJTanRWlvKIlQQ86zSKwcmjCOS7FTLPSJGQpRQSTkHVkOLL7CWo9oSbAQTBgAgAUTxpMCAGs-OQABZa15TBlJHIO9QyYSZTm2YFiUHZNTpKQKfMKYQqRQ5D0eL0+v2B4Ny2Dh9hRqiKSI02JxhJCMp56xVRSs5Z1EEGIyp0zc6HFsRybQc8tXKDe33+gOPEm9DAxnUdmZCadGuTgqfwtQl06gwVpHaibTzApu+dopfVgBKYEJqEwxK37fpnaS04Or8+TzFaKgKDIF7CmkuxqDscFSHY1SPpWy4EAAYtgmB+k89DjNuf67gBbISKeyxmua9g0AUF4qOY6Q5FkMilnayHNJKqHVquLAkrhrbar+0z8HuqiQoo042OCNT2MOJSOPUiw0FoM6IjYGiKCh+DPv6yAkOGP50kJszTpIPxWqyvyiNykEjgg4mAhUqhWGaPzyKommLlW-oACLBCMap+hq4R4S8BFGSJXxKPCh5WNCoEXmoCkqNYcGHoiNDAnBHnaQQAAqYCPII7CoIIRAAILeQZupEQaokKNoig1HRMjJReLULFomb1DydEltlXkEPiABmQ1BAETDkrgowhW2hnxrVkg0PY052FokkqKCiLgtIsjmEpwr9o1-XLhIkY+mAAAKk1wAQZW8dgQ0kKE-hQEqTCRv4LBML05IjBAVU7sJSRWRUVpZOBFj5NkF4uZCqQKDCuSprYx1+hIsCRqgADuV1kDdd2cI9z2vSQ73+GAXRMJwkAA4RQMGto6QrdYcgZL2cKyYgpachUFiJcC7Nsk0FwVlp3gNlGxBkJQmDixG0YzQJc3-ol5TGCWCgZPUGSbLZpbVBUiJCzsBR1DI2Vy42kYEL5OFgGq2IyrT4UIEoXwFKzh7mMlHI2SUBwSA44EZt7+TFhbEvW3cGDkxAHD+BAvQku0Yby878bQo1pHw8x4iZMWoKskayMaEtShWtYGnsZ6YuRxIltRgAkmhunhg7OJ4v4xL3eQJCYOn-6ZPs6Q1NOsj9rs060dy0gOIl4jjwcUgR-L9eR831at-bQSO53mPJwAXvcfcD0RmR2I6Zs7Ko+SAm1ZiB+BR6pDCR4r1ba-yxv-pENwsDsEqPA-h97YCPniPuCdsD-2ltNfisY6aJEyAUUwpdvZwhqBaTaq1xyqByLBX4S135Rk-lbb+xA-4AJIEA7unBe6YEgdAigsD8KCQzktOQpgMr1A9nUKwm1mLMmoqmDKUNKJEMjCQpuaFf64H-oA3A-gyoACFvD+AABqnyBkPL4Ghko-B5OaDIm0NAcJ+PmcQWQrDzDkOIyRkYyEyLkVQhRyjVEAE1NGIPEF8Gcyw4LiWhDsUEzFmKLBZk-OoHIxBHWrqLRcDcJEJIcRQ+R-gyBQD9J4rmmdSLmG5MCRqdRWabTMLmQ8FilgIiyLYpJ0iUnOP8H6fA7AFZwLChnIepECinFZIYhCm1jAOhZo1H2iVbAqBqevOpsjKFAKYKSeZuA47kB+iQOUcdGGUCyQgExgceQWHsPIHpG1bJLGnGE9MLMb7ckmV-aZTigEgLAUMbC+g0k4CgLgbZmRQkll+OJbQ+0sx6yFOkVkrMrkzjhDY2JC47F2OSTM1JNDsB0MwG8vu2BPnfIsBw2cwiAn5hqBeYwaQMrKGqGyY5MKRZwoSQi+5syFGwFwKTfwxV1E4qUNIREuRaiNQyAoTaSlyjZAtByXhSxbmkMZakllbKOUeMVvAl2mRuVWHsFaeQOQQKbX7AsCxdQMrmnzMoaVUjqyOKZeTAAjr0CBTSoAtJxdtLIASlqrBsCcuSELSI2AKNEs0vThYojifC2plr6lzLWX3P0-dlXtMHlw0wsgWqDnArBBKAdM6ngynaSwEzYVPnpRGn+UaFEkgpqgYktxy3sCpAm1hSblCXw5CkDktR5BQRItElYZhdoIXNfYtCb5BAFRCL0EY3z2rfD5pycSWQKIw0QoHUeuwWqNW5CGjitdV6loINHe2kB46J2TvgVOVtvnQnKEtbQcJqKWlLDDFMFQbDGzsIK7dNd4l133TImh7LUBdzWdgEgAAjTJjblZn25mE4UM5jX7mBSUL1uYOTDPUPMGSX6w0lqmdWKM6T7bFU-NsgoEIMyIXAvo2QF5OSSCsJXJaCJshWFsWVLGVCAhPOPvQjZQwmH+DwENVABAIDcDAN6XAH5wwSBgOwQQPHwGYEEMJ1AZGmKByWjCdhZgeRYO2jsaj+R+zq3Y5x+OSmXkMIE5QITuARMEFJCSVAJIJATWGCJkkXQ5N+EU650BvHVMOfU1B6qQMjZfHNNe-s1jUjesQLYCEZgcg5HMAIxC5muNd1JLQiB-GYH2cc+JqaUmZOSfk4IFFdDgsiY0-MCop4zCZCEVrfhvZISs2UBkLkM4xBZfjtV-LUDbP2zU05kkLm3MefYF5nzlWhsqbU-Vh0KZbBaCUI1fsm0lhq3NL8VQdogTTgGwEVx6iiuiZK5JvA5XfMKfA7AQQfBauhbaU2oiiJhRiSJX8A43VgnwjVslDdWqPU0tDXSuuHHsvnbUZdibU33O-Tm-dwQj3nuvfq5IUuqZkq9iEchxLiU0gNDHuJaJPVTuKJUf4NxCPrtldQLJyrGP9BY7C4DRIX3yjoatLwq0uRgnQgNfCfMPJTiAhhNT879PxvOdc8jzzrn5t+bZxz970GItKUkPUOw5oDjpiWsErapgtZEocKzLKRavT0ph-HdJfoGcSaZyztX+A-Qa5YVr7nOvpBjzqP8H2uhTn7FzEhZK4IzQs36zb3dH97cBEd2NkLiPFczdR6zj3YAvehQ+9r-MixRDQQtFYOERPSj5IqPZRKVE4KFtpcW6HFmAiOpac70rt3mcVb823yMufZrhd97IUiWhSxg1qEoCvLMWamCHHo7mE9qd94Rwr6bKOVdo77wPpWQ-EsioqNOfaWHEqKAGd9xwdFEa9k5ObOPP7V6J8+gs3EyzVnrJG4V8bjOu9u4U-MkkRZN-ckEkQQArJhHfFVeMVSICBdbkHVRwRqTaFIDhIlGoS-E4FmanAAoA1Fd-GzL-VPNfJXWbTfSrHA1-PAkAsAz-CA5bTnBBffUJSncQEUJQVMP2RLJQXMNAiVdQOCLA+-eFAAGTwGI1QE-APUjCI0A1IwYJdmqFn1qGMBSnakymEELldTDnPhakY2XiEPpVEKmlkMwAkEblwA4AIA03EBZABHzBSjZBDxKGyB8XMBSDcO4O0FsSMPEM-DMIsPYCsJbG9z31KAcC+BqFUgNw2xolslkD2FOEty4QtEIQMLrh8JMIkAADkgMLpUA8B61boIAIBBg1lxp8i8QyMSwHQXDEYmpLQNDbI4RBFlBA8zAgRRAG9Icm9V4MiSNTCcj-A8iCjYApYmF41NdQjECOF6gORSw2Qlp5AK9DtSIGpb9GNoVbFzDLDtkFBFo9EWpbAzRlgz89YY9s5EpokRdotY9G9bc65tjAjmxJiudEB8gFg6JDwTZzQrQ2oWpFh7CMhWjDdbFcoiM8RQN6E1lyBAMpCZD+jtlDiOFDx6gMo3R1hOCEAVJFJmJWYlIPYq47j49iFeglkgNoFeIEl8RcAxMXdf9JMypvBcpBBSTxNBAKT2AqSvl5CM4UwgJCxK4zEw8EssTolL4zJ4NEIZwuid0H8P43xQhk8TC4T8BfCJiQjXjSgTMzduQ+tKJO0YYrBHRVBgQQIUpOQIdZT4UFSBMMk1SJBE8U4gg7glla0SQpoSQMI8AoEPpK1FTs9ESYQb1HCeRxVDxTj-YbCTiDk7RmtbEbSlT+iHSW8U4AsoA8AiiSi-TbSnc0y8BAyDZlAUxwVjhlBswL5n4lhWRg99CiS5TiEEzs9MjMILCMZIB-BGy7SrCeTB5koajHBUhxcrlOYdlWRvgUjUxUgdcfhbEiAZQQxOynd+iVSYATDNF9RGpIpjBlDCghRGjEh9FViCF8xzBqg6JZz5y5RFz7THTz07h3TSQvTWyPpKBax-Bszk9ESoQWRK5RQIUl0QUYR0go8viHBiwpU0jV45y3zrzMjbyoAVRk50yaSypijbhLz3yQgcz7Y8zuSXjGCEA1JJBulchHANArlaJjBgKjZnJL8IK6z4VoLZQOysLEyJDTCWyfT2ymKFzWLs9uz8KFDiwHRwQtBVh0scgLxoVZ43V10WZjBbFyA-QyBAhfp-Qr0R9HACgx8eR-EnDskZ5dgsMtU7BtAcModH8W9a1pNctANgNk5wNqwf9pNu80cXSaEioSpCQQNHKc96DBLoCDkKhzQygSw6hTyTcxwgRhQWo+VchrBqd3LbKOVvKHKIN-RiCM8yC-MkreJPKqsfL0rIDE1PsNhV06JyMt0KdglTzIQUoYz7R8hEry1MKega0XSHk8RRjnK7s2SOqmVBBK02qc9+r5E+INSCLERkpSJdgtKfgUoSwRzORzQi82QdYvVMNmqkUGkrNY19BaTO8XLZM+ry1-ND5eN0UNM4RHQkYDgy4tAlqWpygtoxdsh9hWYtrOqcse49qDqbsjrJMTrtq8AqtctUU9qrrUDzJ5hcUdggk9Yr8ukrQYRwl2jPrrVdrXl3ksUaSerXKgbOqzrAtlN0V0cPk8KJqFCzQHQEQcgpIORYQLwJxvhARDQw97wErIKE8rLRqGlFt0VsbPk-rXdAaSpeaQb+b2dMVsUezSqL5XQxLKhoRWQSUHIUhfgDgHCtA6h0a5VWUmA7K1Fhb6SJACaBr5UmB8q+ANMgRHRgReoYR8hbBhVWZDYP1jjWQYkGK7ceaWqLa7K3FjaAbTaxbTqLb8r9ArrFolhRAYQ7RTw9VUgeU+yLQbAjlbjuj7jLLstxaFEwA7UHVcQnVrY8bjrQ7gbcAx0C6VM+8baPjy4TNTgUw6IpKFIhxrIeogRwJdaGkmAY1sIsAg7ery7Ca+6lQB71S88fd99XbAQdgtt0Mp49ZAUR5-FbBIlexZzpDVT-Bcp2KBLKboDAQjQ7ASwwLoQ2Y6MchFg4J9hrEA02Jva64iBt7Vy97JDnjD7-xewX15IQqx47wr7iLb7AVshUs78n6oLX77Z37MArDrAArv7zEzdEJfgagTNdY5JSxgH4J77ExazM7iSJEX6ZDYGrCZBEGiIelcxJxRBwRuo2QgGb7cHaa0Gt7SH96qAVBKGIssNIRXIzQ4RnQUw6N8gmY1gdYx53IubiESGd6yGqAxAeHuc+HYr7BzQ0S4JGi5IjiwlBHYoBDN6ZHiHsASQlK1SVy1TtlDyNBjzVBqInrQRBB51jQOjwQM16hzKeiP4iBTHzG4KUzz1cLMzbg-HcykL8zZagY8hddolDteotAshhVGYLTwRgQtYIJZywmbzAmEKXwSA45+gnyfTQmzHINlHtg81Fg5LGochMFTleVViUohw4RAkM6rT6VfGyn7TOK2ySiunzGD6p7Qi6aHQOQZxVYet1GpKsgZKtYlp5KvGs6rZ-BcAgMiZ-RfBAoAgMBHofpxprptlnHEpxwODahWbBSBlmR2YdhuR-kWYUxsoyBsAuhhgU4OUPMpoh7XLnnXmRh8qNnrGUhSJMhxndEJ5MT2ZgK1rsHc5w4hDfm3nz0Pnro0919ldvMJBEX-nipBBAWonEFr7exwIFAy4FrDxgkxyrAKrYthkjGGLsX3mgMCoRsnTI5vnZNsWc9cX6w04CXEB+cQWuEopxJtztHRwjRDholERUt5h2nv0HSmAJp9BUyRoxpPpUXS7JNUA1W-BcYpoyp-53AjnEDvgCcZxUg21qIs0RLSxtMgRqJAQlmtIyolX0VVXRoMRPmMrJt08N9MWdXPX2B9WwBDX2BjX+WEArRFhr4FnEIMFMGuZ1Jq8YCHX1679mg1mMB4AohZSv6apy9A44J0H0p-t9KAIV1TU850EbBMshCvJ836Yiki2el3i0T6hQQx9vhEYCFZxAQCGrSvJTpzoQ2c3B9NSRAMoW2S2TwF46N+xpAVJUggQlhGo1BUZJMMZsZR3G3ZgTY8xdEy57AJmYYiWp9qIXRNZObIGrZd2BXZnbGvt7GzyRTBBYQKhZB5hoR0oMpEoh1v4727JlAHRhRkpf30pPGLxqhmRixmIt1EIRcB2FWfbstMaCDBM1NAPHAX0TEhG00MwsF-ipSA0dYTVnX6yJEn9Ft0O7NMPhnNT5IFgMgfiNZgSigGngRTAb8zRoljs62b3iEn84dLssPBlX1tAw8LILXjESIScgSKJWbyP4UhPac5cQtROxwdc5qagTV2OUNMhkTwkw51BoPqclS6Px2CKBzyhlrWZ0s9py28FVs6n5hA9tNl8i728LPd8GPA9s4L2pwwIhdTkDdmH0McxPhLTkPm9ssKClkqC1kaOU8RNRPr64bjhDgKrkCKNK4FAwOo8IHCGKOJA+j2KsPTVX0HBECWpdC9OjAeZ4jVhO1wLr2iuRCxDMjHisOO1KvJwRlavsx-jSVexJ8mtzzjGSuOuky1nvM+5APTZBF2p3rxIYbxWsSL5NA4oUw31qkJvSu-DBjhiur5uCgQP0HNY7Q2s4iLA8xDl8g7RWClP6Uuv6OCKQJFJfzARHC6u7JulDYmIrBfhexchQTwTOAIFoTANAOnrTIilNAJVORQRKIwloP+w07TNbFWTyShhKTI5qSsOzEi3TvwQ1hr1QQxRs581ag3V-F4y+K7STD5uIIb6ChTwfip8RSbwmZahK4qh9gQeJvYKkz4K1QXSSj7yPT5vx4WeFr2f7JO2fgWbbAWIYbla6f-SGfhfcnEKsU8ApejTxA6gkFSyfuVjKzlIaz1fsLmzvS+mWKNe-QpeZ4i4UDoVqhjdbJc0zXolThdgf65wJueKrz6elyyvXuFC71HQddewtpAk-ijR-hsHg9HWLyYKQ+cmuMU4JfSRoeloBRo+o9UwkDALRUQLaLwKkPcNn6MKhf2LkzM+gmImhIoD-xDl8-hQY-qbi+5Jixcw0t0MqJlBK+LKfGa-0+bfnzuKx+HewBc+dgo+O-C+4+9ZmfVA7P8g70va2v6VzGVLYA1KsOtKi2i5NZCwLA6MzRvh3HZr7xqytqAMUrCrHfw+M56HIRAI-ENGHDIqE+hQrdqfBCAnSjr7QrqtVq09sXOvWlE6lhvguQcQM1BiwV56MpkOCJPmvQrAe6jyALM8j2rQCT6cAswHYGvSPUbA3wGwLsCDSblCS2-GLvHFzrfU8sryPAbAOWgICUatEeiAImTCwgiBmAhRJjQFrS1m+JVLRMjBYFok84hYJmjUEcjLRGolA+VlX2zp0CWqktQWsIPzyIIxBAjO6r2lZhrdsOi0cZmDBSKFcOmtAgIPQP9oco1EqXSQLGzz6SQTYwqOoP7h1yoD7Gw-bxoJxAFfVrBQGNxHYPf73p0ugPEcrlw6hqF8kpYWoHwNtT2p6EfeZgToPhB6CfuRZNDP6iwwzgKU67Cbk-noFj1Y0WAVLj4gVrpZTgywdIZPmzj7ArQ8RNaOw3kZh9LOChT4tnBUL14MozTURjgzvqsNH6NAqCtk0Z4v9v6nsMgZRD6wUpjACUYsHVSyBKBZAqYMwGYOi7DDumATBvghVwpS9r6+uNaqflYxrdEQKTfMGk2GRjw1hSgnxiMK17bCOyBTbAP0D2GmQUwhwuJr2BOGKEP2jQA7FVx2BZNNhSZXppGG4rZNXhkwj4TMJOHvDjSiRUsMu3UDiJVm6zJ6LPzGE1RUswVftLglUhwRKWW5eKkcH7DkVFBcKRlsiyAzetAOheKSOCDjqpA1skLVwcmHtoaAikgIJ5hYT+ZMtyYfAVluel5a3tMRQMGcGCn0Skpa2YVJHsC3vAsRVggSbKK62VYet1WNI0UcVCYD74H4bIFmLUBrYd89AfoIaOwCECOA0gy7Fbp2hFCnEwMqAdgMVC6CJYkEI8fURo0qG-A9ASFSMGaKSDRsrRsxMIZmhABYwfQLSLmIiD0BgisUvogViHlmAWiP2Kw7WibGiTCpdRb6A0YsQ74uAXAQAA */
|
|
id: 'Modeling',
|
|
|
|
context: ({ input }) => ({
|
|
...modelingMachineDefaultContext,
|
|
...input,
|
|
}),
|
|
|
|
states: {
|
|
idle: {
|
|
on: {
|
|
'Enter sketch': [
|
|
{
|
|
target: 'animating to existing sketch',
|
|
guard: 'Selection is on face',
|
|
},
|
|
'Sketch no face',
|
|
],
|
|
|
|
Extrude: {
|
|
target: 'idle',
|
|
guard: 'has valid sweep selection',
|
|
actions: ['AST extrude'],
|
|
reenter: false,
|
|
},
|
|
|
|
Revolve: {
|
|
target: 'idle',
|
|
guard: 'has valid sweep selection',
|
|
actions: ['AST revolve'],
|
|
reenter: false,
|
|
},
|
|
|
|
Loft: {
|
|
target: 'Applying loft',
|
|
reenter: true,
|
|
},
|
|
|
|
Fillet: {
|
|
target: 'idle',
|
|
guard: 'has valid edge treatment selection',
|
|
actions: ['AST fillet'],
|
|
reenter: false,
|
|
},
|
|
|
|
Export: {
|
|
target: 'idle',
|
|
reenter: false,
|
|
guard: 'Has exportable geometry',
|
|
actions: 'Engine export',
|
|
},
|
|
|
|
Make: {
|
|
target: 'idle',
|
|
reenter: false,
|
|
guard: 'Has exportable geometry',
|
|
actions: 'Make',
|
|
},
|
|
|
|
'Delete selection': {
|
|
target: 'idle',
|
|
guard: 'has valid selection for deletion',
|
|
actions: ['AST delete selection'],
|
|
reenter: false,
|
|
},
|
|
|
|
'Text-to-CAD': {
|
|
target: 'idle',
|
|
reenter: false,
|
|
actions: ['Submit to Text-to-CAD API'],
|
|
},
|
|
|
|
'Offset plane': {
|
|
target: 'Applying offset plane',
|
|
reenter: true,
|
|
},
|
|
},
|
|
|
|
entry: 'reset client scene mouse handlers',
|
|
|
|
states: {
|
|
hidePlanes: {
|
|
on: {
|
|
'Artifact graph populated': 'showPlanes',
|
|
},
|
|
|
|
entry: 'hide default planes',
|
|
},
|
|
|
|
showPlanes: {
|
|
reenter: true,
|
|
on: {
|
|
'Artifact graph emptied': 'hidePlanes',
|
|
'Artifact graph populated': 'showPlanes',
|
|
},
|
|
|
|
entry: [
|
|
'show default planes',
|
|
'reset camera position',
|
|
'set selection filter to curves only',
|
|
],
|
|
description: `We want to disable selections and hover highlights here, because users can't do anything with that information until they actually add something to the scene. The planes are just for orientation here.`,
|
|
exit: 'set selection filter to defaults',
|
|
},
|
|
},
|
|
|
|
initial: 'hidePlanes',
|
|
},
|
|
|
|
Sketch: {
|
|
states: {
|
|
SketchIdle: {
|
|
on: {
|
|
'Make segment vertical': {
|
|
guard: 'Can make selection vertical',
|
|
target: 'Await constrain vertically',
|
|
},
|
|
|
|
'Make segment horizontal': {
|
|
guard: 'Can make selection horizontal',
|
|
target: 'Await constrain horizontally',
|
|
},
|
|
|
|
'Constrain horizontal distance': {
|
|
target: 'Await horizontal distance info',
|
|
guard: 'Can constrain horizontal distance',
|
|
},
|
|
|
|
'Constrain vertical distance': {
|
|
target: 'Await vertical distance info',
|
|
guard: 'Can constrain vertical distance',
|
|
},
|
|
|
|
'Constrain ABS X': {
|
|
target: 'Await ABS X info',
|
|
guard: 'Can constrain ABS X',
|
|
},
|
|
|
|
'Constrain ABS Y': {
|
|
target: 'Await ABS Y info',
|
|
guard: 'Can constrain ABS Y',
|
|
},
|
|
|
|
'Constrain angle': {
|
|
target: 'Await angle info',
|
|
guard: 'Can constrain angle',
|
|
},
|
|
|
|
'Constrain length': {
|
|
target: 'Await length info',
|
|
guard: 'Can constrain length',
|
|
},
|
|
|
|
'Constrain perpendicular distance': {
|
|
target: 'Await perpendicular distance info',
|
|
guard: 'Can constrain perpendicular distance',
|
|
},
|
|
|
|
'Constrain horizontally align': {
|
|
guard: 'Can constrain horizontally align',
|
|
target: 'Await constrain horizontally align',
|
|
},
|
|
|
|
'Constrain vertically align': {
|
|
guard: 'Can constrain vertically align',
|
|
target: 'Await constrain vertically align',
|
|
},
|
|
|
|
'Constrain snap to X': {
|
|
guard: 'Can constrain snap to X',
|
|
target: 'Await constrain snap to X',
|
|
},
|
|
|
|
'Constrain snap to Y': {
|
|
guard: 'Can constrain snap to Y',
|
|
target: 'Await constrain snap to Y',
|
|
},
|
|
|
|
'Constrain equal length': {
|
|
guard: 'Can constrain equal length',
|
|
target: 'Await constrain equal length',
|
|
},
|
|
|
|
'Constrain parallel': {
|
|
target: 'Await constrain parallel',
|
|
guard: 'Can canstrain parallel',
|
|
},
|
|
|
|
'Constrain remove constraints': {
|
|
guard: 'Can constrain remove constraints',
|
|
target: 'Await constrain remove constraints',
|
|
},
|
|
|
|
'Re-execute': {
|
|
target: 'SketchIdle',
|
|
reenter: false,
|
|
actions: ['set sketchMetadata from pathToNode'],
|
|
},
|
|
|
|
'code edit during sketch': 'clean slate',
|
|
|
|
'Convert to variable': {
|
|
target: 'Await convert to variable',
|
|
guard: 'Can convert to variable',
|
|
},
|
|
|
|
'change tool': {
|
|
target: 'Change Tool',
|
|
},
|
|
},
|
|
|
|
entry: ['setup client side sketch segments'],
|
|
},
|
|
|
|
'Await horizontal distance info': {
|
|
invoke: {
|
|
src: 'Get horizontal info',
|
|
id: 'get-horizontal-info',
|
|
input: ({ context: { selectionRanges, sketchDetails } }) => ({
|
|
selectionRanges,
|
|
sketchDetails,
|
|
}),
|
|
onDone: {
|
|
target: 'SketchIdle',
|
|
actions: 'Set selection',
|
|
},
|
|
onError: 'SketchIdle',
|
|
},
|
|
},
|
|
|
|
'Await vertical distance info': {
|
|
invoke: {
|
|
src: 'Get vertical info',
|
|
id: 'get-vertical-info',
|
|
input: ({ context: { selectionRanges, sketchDetails } }) => ({
|
|
selectionRanges,
|
|
sketchDetails,
|
|
}),
|
|
onDone: {
|
|
target: 'SketchIdle',
|
|
actions: 'Set selection',
|
|
},
|
|
onError: 'SketchIdle',
|
|
},
|
|
},
|
|
|
|
'Await ABS X info': {
|
|
invoke: {
|
|
src: 'Get ABS X info',
|
|
id: 'get-abs-x-info',
|
|
input: ({ context: { selectionRanges, sketchDetails } }) => ({
|
|
selectionRanges,
|
|
sketchDetails,
|
|
}),
|
|
onDone: {
|
|
target: 'SketchIdle',
|
|
actions: 'Set selection',
|
|
},
|
|
onError: 'SketchIdle',
|
|
},
|
|
},
|
|
|
|
'Await ABS Y info': {
|
|
invoke: {
|
|
src: 'Get ABS Y info',
|
|
id: 'get-abs-y-info',
|
|
input: ({ context: { selectionRanges, sketchDetails } }) => ({
|
|
selectionRanges,
|
|
sketchDetails,
|
|
}),
|
|
onDone: {
|
|
target: 'SketchIdle',
|
|
actions: 'Set selection',
|
|
},
|
|
onError: 'SketchIdle',
|
|
},
|
|
},
|
|
|
|
'Await angle info': {
|
|
invoke: {
|
|
src: 'Get angle info',
|
|
id: 'get-angle-info',
|
|
input: ({ context: { selectionRanges, sketchDetails } }) => ({
|
|
selectionRanges,
|
|
sketchDetails,
|
|
}),
|
|
onDone: {
|
|
target: 'SketchIdle',
|
|
actions: 'Set selection',
|
|
},
|
|
onError: 'SketchIdle',
|
|
},
|
|
},
|
|
|
|
'Await length info': {
|
|
invoke: {
|
|
src: 'Get length info',
|
|
id: 'get-length-info',
|
|
input: ({ context: { selectionRanges, sketchDetails } }) => ({
|
|
selectionRanges,
|
|
sketchDetails,
|
|
}),
|
|
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: {
|
|
always: [
|
|
{
|
|
target: 'normal',
|
|
guard: 'has made first point',
|
|
actions: 'set up draft line',
|
|
},
|
|
'No Points',
|
|
],
|
|
},
|
|
|
|
normal: {},
|
|
|
|
'No Points': {
|
|
entry: 'setup noPoints onClick listener',
|
|
|
|
on: {
|
|
'Add start point': {
|
|
target: 'normal',
|
|
actions: 'set up draft line without teardown',
|
|
},
|
|
|
|
Cancel: '#Modeling.Sketch.undo startSketchOn',
|
|
},
|
|
},
|
|
},
|
|
|
|
initial: 'Init',
|
|
|
|
on: {
|
|
'change tool': {
|
|
target: 'Change Tool',
|
|
},
|
|
},
|
|
},
|
|
|
|
Init: {
|
|
always: [
|
|
{
|
|
target: 'SketchIdle',
|
|
guard: 'is editing existing sketch',
|
|
},
|
|
'Line tool',
|
|
],
|
|
},
|
|
|
|
'Tangential arc to': {
|
|
entry: 'set up draft arc',
|
|
|
|
on: {
|
|
'change tool': {
|
|
target: 'Change Tool',
|
|
},
|
|
},
|
|
},
|
|
|
|
'undo startSketchOn': {
|
|
invoke: {
|
|
src: 'AST-undo-startSketchOn',
|
|
id: 'AST-undo-startSketchOn',
|
|
input: ({ context: { sketchDetails } }) => ({ sketchDetails }),
|
|
onDone: {
|
|
target: '#Modeling.idle',
|
|
actions: 'enter modeling mode',
|
|
},
|
|
},
|
|
},
|
|
|
|
'Rectangle tool': {
|
|
entry: ['listen for rectangle origin'],
|
|
|
|
states: {
|
|
'Awaiting second corner': {
|
|
on: {
|
|
'Finish rectangle': 'Finished Rectangle',
|
|
},
|
|
},
|
|
|
|
'Awaiting origin': {
|
|
on: {
|
|
'Add rectangle origin': {
|
|
target: 'Awaiting second corner',
|
|
actions: 'set up draft rectangle',
|
|
},
|
|
},
|
|
},
|
|
|
|
'Finished Rectangle': {
|
|
always: '#Modeling.Sketch.SketchIdle',
|
|
},
|
|
},
|
|
|
|
initial: 'Awaiting origin',
|
|
|
|
on: {
|
|
'change tool': {
|
|
target: 'Change Tool',
|
|
},
|
|
},
|
|
},
|
|
|
|
'Center Rectangle tool': {
|
|
entry: ['listen for center rectangle origin'],
|
|
|
|
states: {
|
|
'Awaiting corner': {
|
|
on: {
|
|
'Finish center rectangle': 'Finished Center Rectangle',
|
|
},
|
|
},
|
|
|
|
'Awaiting origin': {
|
|
on: {
|
|
'Add center rectangle origin': {
|
|
target: 'Awaiting corner',
|
|
// TODO
|
|
actions: 'set up draft center rectangle',
|
|
},
|
|
},
|
|
},
|
|
|
|
'Finished Center Rectangle': {
|
|
always: '#Modeling.Sketch.SketchIdle',
|
|
},
|
|
},
|
|
|
|
initial: 'Awaiting origin',
|
|
|
|
on: {
|
|
'change tool': {
|
|
target: 'Change Tool',
|
|
},
|
|
},
|
|
},
|
|
|
|
'clean slate': {
|
|
always: 'SketchIdle',
|
|
},
|
|
|
|
'Await convert to variable': {
|
|
invoke: {
|
|
src: 'Get convert to variable info',
|
|
id: 'get-convert-to-variable-info',
|
|
input: ({ context: { selectionRanges, sketchDetails }, event }) => {
|
|
if (event.type !== 'Convert to variable') {
|
|
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': {
|
|
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',
|
|
},
|
|
],
|
|
|
|
entry: ['assign tool in context', 'reset selections'],
|
|
},
|
|
'Circle tool': {
|
|
on: {
|
|
'change tool': 'Change Tool',
|
|
},
|
|
|
|
states: {
|
|
'Awaiting origin': {
|
|
on: {
|
|
'Add circle origin': {
|
|
target: 'Awaiting Radius',
|
|
actions: 'set up draft circle',
|
|
},
|
|
},
|
|
},
|
|
|
|
'Awaiting Radius': {
|
|
on: {
|
|
'Finish circle': 'Finished Circle',
|
|
},
|
|
},
|
|
|
|
'Finished Circle': {
|
|
always: '#Modeling.Sketch.SketchIdle',
|
|
},
|
|
},
|
|
|
|
initial: 'Awaiting origin',
|
|
entry: 'listen for circle origin',
|
|
},
|
|
},
|
|
|
|
initial: 'Init',
|
|
|
|
on: {
|
|
CancelSketch: '.SketchIdle',
|
|
|
|
'Delete segment': {
|
|
reenter: false,
|
|
actions: ['Delete segment', 'Set sketchDetails'],
|
|
},
|
|
'code edit during sketch': '.clean slate',
|
|
},
|
|
|
|
exit: [
|
|
'sketch exit execute',
|
|
'tear down client sketch',
|
|
'remove sketch grid',
|
|
'engineToClient cam sync direction',
|
|
'Reset Segment Overlays',
|
|
'enable copilot',
|
|
],
|
|
|
|
entry: ['add axis n grid', 'clientToEngine cam sync direction'],
|
|
},
|
|
|
|
'Sketch no face': {
|
|
entry: [
|
|
'disable copilot',
|
|
'show default planes',
|
|
'set selection filter to faces only',
|
|
'enter sketching mode',
|
|
],
|
|
|
|
exit: ['hide default planes', 'set selection filter to defaults'],
|
|
on: {
|
|
'Select default plane': {
|
|
target: 'animating to plane',
|
|
actions: ['reset sketch metadata'],
|
|
},
|
|
},
|
|
},
|
|
|
|
'animating to plane': {
|
|
invoke: {
|
|
src: 'animate-to-face',
|
|
id: 'animate-to-face',
|
|
|
|
input: ({ event }) => {
|
|
if (event.type !== 'Select default plane') return undefined
|
|
return event.data
|
|
},
|
|
|
|
onDone: {
|
|
target: 'Sketch',
|
|
actions: 'set new sketch metadata',
|
|
},
|
|
|
|
onError: 'Sketch no face',
|
|
},
|
|
},
|
|
|
|
'animating to existing sketch': {
|
|
invoke: {
|
|
src: 'animate-to-sketch',
|
|
id: 'animate-to-sketch',
|
|
input: ({ context }) => ({
|
|
selectionRanges: context.selectionRanges,
|
|
sketchDetails: context.sketchDetails,
|
|
}),
|
|
onDone: {
|
|
target: 'Sketch',
|
|
actions: [
|
|
'disable copilot',
|
|
'set new sketch metadata',
|
|
'enter sketching mode',
|
|
],
|
|
},
|
|
},
|
|
},
|
|
|
|
'Applying offset plane': {
|
|
invoke: {
|
|
src: 'offsetPlaneAstMod',
|
|
id: 'offsetPlaneAstMod',
|
|
input: ({ event }) => {
|
|
if (event.type !== 'Offset plane') return undefined
|
|
return event.data
|
|
},
|
|
onDone: ['idle'],
|
|
onError: ['idle'],
|
|
},
|
|
},
|
|
|
|
'Applying loft': {
|
|
invoke: {
|
|
src: 'loftAstMod',
|
|
id: 'loftAstMod',
|
|
input: ({ event }) => {
|
|
if (event.type !== 'Loft') return undefined
|
|
return event.data
|
|
},
|
|
onDone: ['idle'],
|
|
onError: ['idle'],
|
|
},
|
|
},
|
|
},
|
|
|
|
initial: 'idle',
|
|
|
|
on: {
|
|
Cancel: {
|
|
target: '.idle',
|
|
// TODO what if we're existing extrude equipped, should these actions still be fired?
|
|
// maybe cancel needs to have a guard for if else logic?
|
|
actions: [
|
|
'reset sketch metadata',
|
|
'enable copilot',
|
|
'enter modeling mode',
|
|
],
|
|
},
|
|
|
|
'Set selection': {
|
|
reenter: false,
|
|
actions: 'Set selection',
|
|
},
|
|
|
|
'Set mouse state': {
|
|
reenter: false,
|
|
actions: 'Set mouse state',
|
|
},
|
|
'Set context': {
|
|
reenter: false,
|
|
actions: 'Set context',
|
|
},
|
|
'Set Segment Overlays': {
|
|
reenter: false,
|
|
actions: 'Set Segment Overlays',
|
|
},
|
|
'Center camera on selection': {
|
|
reenter: false,
|
|
actions: 'Center camera on selection',
|
|
},
|
|
},
|
|
})
|
|
|
|
export function isEditingExistingSketch({
|
|
sketchDetails,
|
|
}: {
|
|
sketchDetails: SketchDetails | null
|
|
}): boolean {
|
|
// should check that the variable declaration is a pipeExpression
|
|
// and that the pipeExpression contains a "startProfileAt" callExpression
|
|
if (!sketchDetails?.sketchPathToNode) return false
|
|
const variableDeclaration = getNodeFromPath<VariableDeclarator>(
|
|
kclManager.ast,
|
|
sketchDetails.sketchPathToNode,
|
|
'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 hasStartProfileAt = pipeExpression.body.some(
|
|
(item) =>
|
|
item.type === 'CallExpression' && item.callee.name === 'startProfileAt'
|
|
)
|
|
const hasCircle = pipeExpression.body.some(
|
|
(item) => item.type === 'CallExpression' && item.callee.name === 'circle'
|
|
)
|
|
return (hasStartProfileAt && pipeExpression.body.length > 2) || hasCircle
|
|
}
|
|
export function pipeHasCircle({
|
|
sketchDetails,
|
|
}: {
|
|
sketchDetails: SketchDetails | null
|
|
}): boolean {
|
|
if (!sketchDetails?.sketchPathToNode) return false
|
|
const variableDeclaration = getNodeFromPath<VariableDeclarator>(
|
|
kclManager.ast,
|
|
sketchDetails.sketchPathToNode,
|
|
'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 === 'CallExpression' && item.callee.name === 'circle'
|
|
)
|
|
return hasCircle
|
|
}
|
|
|
|
export function canRectangleOrCircleTool({
|
|
sketchDetails,
|
|
}: {
|
|
sketchDetails: SketchDetails | null
|
|
}): boolean {
|
|
const node = getNodeFromPath<VariableDeclaration>(
|
|
kclManager.ast,
|
|
sketchDetails?.sketchPathToNode || [],
|
|
'VariableDeclaration'
|
|
)
|
|
// This should not be returning false, and it should be caught
|
|
// but we need to simulate old behavior to move on.
|
|
if (err(node)) return false
|
|
return node.node?.declarations?.[0]?.init.type !== 'PipeExpression'
|
|
}
|
|
|
|
/** If the sketch contains `close` or `circle` stdlib functions it must be closed */
|
|
export function isClosedSketch({
|
|
sketchDetails,
|
|
}: {
|
|
sketchDetails: SketchDetails | null
|
|
}): boolean {
|
|
const node = getNodeFromPath<VariableDeclaration>(
|
|
kclManager.ast,
|
|
sketchDetails?.sketchPathToNode || [],
|
|
'VariableDeclaration'
|
|
)
|
|
// This should not be returning false, and it should be caught
|
|
// but we need to simulate old behavior to move on.
|
|
if (err(node)) return false
|
|
if (node.node?.declarations?.[0]?.init.type !== 'PipeExpression') return false
|
|
return node.node.declarations[0].init.body.some(
|
|
(node) =>
|
|
node.type === 'CallExpression' &&
|
|
(node.callee.name === 'close' || node.callee.name === 'circle')
|
|
)
|
|
}
|