Files
modeling-app/src/machines/modelingMachine.ts
Frank Noirot c9800a58d0 Update code mods for extrude so that new top-level constants are created (#2549)
* Make sketch and extrude produce separate top-level constants

* Fix most tests

* Add a breaking test for sketch on face AST mod

* Use `extrude` instead of `part`

* Implement @Irev-Dev's branch changes from https://github.com/KittyCAD/modeling-app/pull/2472

* Get extrude on face working

* Update incorrect sketch on face test

* Update unit tests

* Fix up E2E test changes

* fmt

* Fix a couple of goofed up test updates

* More specific names for paths to node sent to modelingMachine

* Bump down playwright workers for now

* Slightly more explicit type coercion

* Update snapshot tests

* Missed one other new flow test that wasn't updated to use "sketch001"

* Typo

* Damn missed one more sorry

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* Re-run CI

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* I think the multiple sketches test reverted from under me

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-06-04 13:57:01 -04:00

1342 lines
47 KiB
TypeScript

import { PathToNode, VariableDeclarator, parse, recast } from 'lang/wasm'
import { Axis, Selection, Selections, updateSelections } from 'lib/selections'
import { assign, createMachine } from 'xstate'
import {
isNodeSafeToReplacePath,
getNodePathFromSourceRange,
} from 'lang/queryAst'
import {
kclManager,
sceneInfra,
sceneEntitiesManager,
engineCommandManager,
editorManager,
} 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 { addStartProfileAt, extrudeSketch } from 'lang/modifyAst'
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 { Models } from '@kittycad/lib/dist/types/src'
import { ModelingCommandSchema } from 'lib/commandBarConfigs/modelingCommandConfig'
import { DefaultPlaneStr } from 'clientSideScene/sceneEntities'
import { Vector3 } from 'three'
import { quaternionFromUpNForward } from 'clientSideScene/helpers'
import { uuidv4 } from 'lib/utils'
import { Coords2d } from 'lang/std/sketch'
import { deleteSegment } from 'clientSideScene/ClientSideSceneComp'
export const MODELING_PERSIST_KEY = 'MODELING_PERSIST_KEY'
export type SetSelections =
| {
selectionType: 'singleCodeCursor'
selection?: Selection
}
| {
selectionType: 'otherSelection'
selection: Axis
}
| {
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 type SegmentOverlayPayload =
| {
type: 'set-one'
pathToNodeString: string
seg: SegmentOverlay
}
| {
type: 'delete-one'
pathToNodeString: string
}
| { type: 'clear' }
| {
type: 'set-many'
overlays: SegmentOverlays
}
export type ModelingMachineEvent =
| {
type: 'Enter sketch'
data?: {
forceNewSketch?: boolean
}
}
| { type: 'Sketch On Face' }
| {
type: 'Select default plane'
data: {
zAxis: [number, number, number]
yAxis: [number, number, number]
} & (
| {
type: 'defaultPlane'
plane: DefaultPlaneStr
planeId: string
}
| {
type: 'extrudeFace'
position: [number, number, number]
sketchPathToNode: PathToNode
extrudePathToNode: PathToNode
cap: 'start' | 'end' | 'none'
faceId: string
}
)
}
| {
type: 'Set selection'
data: SetSelections
}
| { 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: 'Extrude'; data?: ModelingCommandSchema['Extrude'] }
| { type: 'Equip Line tool' }
| { type: 'Equip tangential arc to' }
| { type: 'Equip rectangle tool' }
| {
type: 'Add rectangle origin'
data: [x: number, y: number]
}
| {
type: 'done.invoke.animate-to-face' | 'done.invoke.animate-to-sketch'
data: SketchDetails
}
| {
type: 'done.invoke.get-convert-to-variable-info'
data: PathToNode
}
| { type: 'Set mouse state'; data: MouseState }
| {
type: 'Set Segment Overlays'
data: SegmentOverlayPayload
}
| {
type: 'Delete segment'
data: PathToNode
}
| {
type: 'code edit during sketch'
}
| {
type: 'Convert to variable'
data: {
pathToNode: PathToNode
variableName: string
}
}
export type MoveDesc = { line: number; snippet: string }
export const modelingMachine = createMachine(
{
/** @xstate-layout N4IgpgJg5mDOIC5QFkD2EwBsCWA7KAxAMICGuAxlgNoAMAuoqAA6qzYAu2qujIAHogC0AdgCsAZgB04gEyjhADnEA2GgoUAWJQBoQAT0QBGGuICckmoZkbTM42YWKAvk91oMOfAQDKYdgAJYLDByTm5aBiQQFjYwniiBBEFTU2VJYWFxDUNDbPFxCQ1dAwQZO0lDW3EcuQzlBRoNFzd0LDxCXwCAW1QAVyDA9hJ2MAjeGI4ueNBE5JTJVUNNBUNlLQ05YsQsjQWNLVE5Yw0aZXFmkHc2r07-XygusFwAgHkANzAAJ0wSPVgxqITOK8RI0LYIGgXK6eKCSbAQTBgAgAUWeX0CAGs-OQABYA5isSbcEFCGTKYQVMmpLRWHLkor6RAyUy7DRZWR5GimBTKUxQ1owuEIpGokafTHYvGGSIE2JTElJaw0STZRaiNTiNQ0YTgk4ySS2YTKURWeqGcQrZT8jztIWIlF8difXoYfHRQnAhJCUTySTMmiiXmiDSiFbgnLqv08hQpdU2faGa3XWHw+3IvgsT7sN1A+VexXqKNKsq5Fmh8EZQwLMRZY0NGTCENJwXeLHsXHEMiUTCtyU5j15mZGJZpYyNrRasyycNLKs5dQ5TVLUPN2299s4ggAEWCI0CYAeT2z9HGA+J+bBjIhq-wknXHfIrX8kA4-ggvU+7QlG-7cvPQ4Qc0QwqfZjQtMpA32cEWTSDIyhUMl6gUCQb1he8cTvNtcQASWFAhkBILF90PZ5-A+LNsHIEhMF-Ilpn4IxmTnBQZE0I1NBoBtwzyaQaD4uQJAbJQmlcS4BTXLCMPQ3D7QIoighIgIcVQT8AC9uCGGiT0BM96MSKxGwWNRVBNAzyRkcNqirYN5EsM5jB9GRUMwyUXI3GSkSIbhYCdEg8H8ZS1I06i32wHyu1GbTZTohUrGDCwNk1bkslMJYFHBGQzDSAMfT4kNUvNZz0LcnC8K83AfM+PzcDIr5OCozBQvCihIpld0-z0xj6gNQ5kNWbI1A2DKGykQ5sjYuDjRElobVvYrpLK7zfP8gBBAAhbx-AADVoz0AKsZCVXEMRS2qZlMoy0x4qu8kGk40RTEyc5ROhCTXIW+1ysq6r-HWzaAE1dsHBjANY0Q-Q2VYjkDQMLKvTVC2EASTVURx9iKySSpxDziCWqr-LIKBESB-8QaAqQLXA5DFEMYRTAy9UKTkamMkDOnHox97JJxr7lpqxF8HYPEovamL83NY7pGNVRUtkapRAy5QlnSXlsjp-YlHJTmNyxnm8Z+pgvkN3AIEo3ofnFU3msoEnOtB1iVRSM6ThHU4MpWXZhEqGM4P2DQxG13FdcWiq+YClTsHU55qMwPR-Go7AoFwW3YrJfVkOWQpg1OcRwxRv0romhtA3yQOpO5kPvv88j6pjuOE6TlPxYbcw2VYumUbME1w2UORJFDR7uXqZlHGembkyx4PPv1-zYFwEgmH8dhUG2pv9uqKQyQtNRHpYswMsafUIOUE+TMcMoy6nzyZ5queF6XlfAZF3NSf0jeLCNaoMmO44MtyClgznwerTDQawx5iVmmhTGH1r6h3xjVMAABHXoIUBZQCFmvMmzJRr-zpsIAMchjrhkyMqNkJg+IBmlnyF64k5rQIrtPOBBsSBVUwIiLSbUX523NA2CwiElhlENHYcMLIKRKz4oYUM6gd6XxgbjJh-lPhgB6B8fwj4FHPH+M-XSqctALCsCGXumhJE6CvEsaoFhVi2EaPkYyTkaGQMnnIgASmAQQYA+AhF6CMTB+ler9zOMhAoJ8Sw6jMSYcGZDTAFB2ErWmsiGEimQdgReAAZPAYAH6oA4aeDqsUYxpAkCoGwV0sjqHBEocGjNrAQVAaZBJkocbImSYvIY+AjzYBCiw8gD9fFMjiiqLkAZSm1lzuEmMfofQqFOEPAM4DXp0K5o0vCzTegpP8Eo0IhNERZJyTpPJ+YRr6m5IfBMWRJFhJKHFBQKogL9RyJUU4DT3J4UfBgZ8psAjvk-Pgb8uI+mASyODXIIYxBrFYnDEo3Ibn5DyBGao3JpoQInvNRJ8ia4PzIiwzpAAjYm2iDnr0ONWJY6xxEsnBLIbUtztQxlsNyK6l8VoAHc-JKQjlHTSTU2mUH8HgAAZqgAgEBuBgDhLgN4qAsSSBgOwQQgVI7BUwIIAVqAAWZUaCqUBJxHpmEepkDK2RzDIQhdUXkcgtYOJRZjFlbLw5BWjo1K2PLMmqoIF8T4KlJBMB+OwQVnwugyr8PKjlSqVW4EFeqzUwKQWLhDKxR6GVwIJVOHxAoDYlZMtZa+GulEQrOoinyiNQqRW4DFXgSV0rZWCFzQ1cNkaCViwAplKwFRjorBMOSSoXErxlCUJIRwhxDgS1pvY8eLYbXZoCLW-NYUXVFsFe6z4nrPjet9f6wN1aZ3KtVVG1tDR5DALEJoYaGwKjS04haFi6h5m0Kga5W1r4-rbQXSW0V4rK1iurSQHFsBBB8HrWqxte0QYttgr3fihDyy9tSPqXIjR+qyF7pkLNdrn1bVfUulda7hgbqDXKn9f6AO7uA8DRILaKRsivQUUwAYtCXVWNIUMYFjiNmoeOt6OtH0BGff9TDpby0SqlV+4NhHBB6EA3utIBRD0PWwY2BW8NTiUYZeqUM0akULPvVxqdv0Nr+D426j1XqfW4ZUpu0Tv7xOSdI6-JkX8+GfyRifc5yhKX9QsLyEJRxjpjuRROh9untmuuLcK99FbhP4cEMFmznCdGHIc0uc0Q7YyGAZoGA00bLCSOYxkVDr5guYeM6u0zfrzNRZiyRuLhLQPVBufg7IrErA71pgzWmFRkv4LkAQy1HHFk6btWgoW-HwtCarcGobOJYu5KbbVyRR1MoshATITilymT1HBjnL+KhaZWHywESbRXl0mfXeV6tk3pv7Nm+Rs1FQ02hlWGxSF62z0hK0KI+QYL9v+ENp8Y2ptyDmxYdywtbqBMfsi9W37-2zYW0EAWlql3oogfI2nHqShrC9zJCxRTJQIXgx-qA40sZCHfeh08AHQPLZztB6F4rOGysBqi+Tk2sOWHw5p4jqrM2UdMjR4XDkWPh646ZBkCkhOT4PSuqTq1AWdbpLLbslELSl6Ew6V0z4PTl7qrpmkbk-8bHRLpmMqFihBnVC0MaZzSxL4K8ycvbJyu1mL02W0om9vUDZJ17IA0DQNhnNVOGFb5hyS8jNTBJQtuMm7MkNhXAHACDqsymkJQfUGgsjkPTK8Zo-QWmibR46dNMpR8Vw7zAsf4-sET9KHnZG+dI37pxS0jWZkVjpiqM4EhbCBlowGEvHvsmSAAHIrwAAqoDwOwWABAVoQAgIMFhAQWCT-VSfXYtGYYZ4aGscEJ9W5mDwbS1P-eY8j-8OPyf0-SAtT2cjuvgFNDmBMJlFiXIDjlKvPgqsNglbRLOjyW9RxYqOPBPAFYwRjA4fBaZQ4N-IPfIDvI0MkVNMkbIS+EAqvKgGvK7XnQCAMKsGMIZUBKlSwBkK5JidIIJUBU0GwFQS+AAFTV2eE6Uam6Qfid3WTt12VX32COlpQenVF7mglAXSCRkylslOFAWEEvl6BNhXnCizHQheFwDCzLQh2lRWm8DoMEBkJFUEHkPYEUOTls24XyBuUOEyAKn2AKDUHBFDGVFSEeTYkpmOkvlcS2XwB2TL0kG4y-AjigDwBnznw2RCDdx2T8LwABSshTxc1o2ZCumyG4l5AHRSEsDpmfzrEvnIERDIECF9SRDAOyF2BMh9BNVyCKRnHNAsFOGMF3j1T21l04yDm4zUW4AxWXixU-B-XtHBwi3GzlXURrkEGXhrWxS6LcW52wPvzKDEA60aCNw7SVhFwQFkEqAqFSBlnewMVEG+wGLqkxTeFGLxSRHp1Kzw2rV2KzCGNQBGM6KOKR1FhwL7QpEsBNXyF2x9CWNsQpisRSFYlRjUB2JvmCJUUyXUSrk0RUME0-UkF0LBL5kECURBMEDhPgWPGq2u3r12CnDZFnEbF7lIKMBYk9imlAXUBWw2D8y00nmaJRJ+gVU5TrkhLULFVhJvhDQdU0ljijUsH7lSCuhZBMDyEsh5Iaw1HJB9C80BI0VqgogaljiZN6JZOuNpLwBrTqjzTYT0G5PMHOmsCWyWEbHSjMT1RVD7RjDKDyEjwaP6yaN0xVJqnpKVVjnjhwCTgVLGyVORLZMdMdVjmi1dKMPRMePOn7hywtU7S9jzjUANFAmOkARWFYilPBJlNrk1JdMTmUJ6I9JhOVLZO3T9IbkDNrzs1KAbFZGDH6goTWDSzMVUCxLOBUDGkNNSiTLDjvlaRXi2ndOhNZI0T0PniYCuP-W5OVE7y9gKAMhDAPgKEpGsEMSnAIVbPgUCAHMxX+m7Mi17PBP7IXiHK1OMIVA1TSH9gNMnCoL-jJGkDKHOhpHWKXJ+iQRQUakmw3OlS3PhMfOokEEmyT1SgNGsBOhsEUGQjczMS9lHAELBlyHbkAOtUCztXtJ+xYRjiwFfM9PtMECYGQrYWoAPIS2jNo1umRgyCNKuWiRuUykyDRhy2XDLn8FwBXn5RIEoB8GCFCDfDACYvNiXx+DLQBUEDsGYhZEL0tzYyWN7hTysHZHZiNBsGcjIGwC6GGC-HaNMzLTQskAUqUpGCHKYptjwoAh9GYlpUaEg21F3wdjOlsQTUNIUHkvj20pUpXg8TnS-FgEkg0q0uGDcWGPcr7AMpBl7k2zTjphWGDF83cxuRPiotSHUFETsouAYowHgCiC02LLtkEAaCrFkHkE1jUGWBNyEEcEdhSFjDZEQO5GclTDAHSoVEEDWDMPJF1U0FDH9gqRWH7gRkkSsssHY380aJxFqvzFipVGpk7QIVgN7RMANF-3nC7QGm2OtO0yDhgSGv2hOE3ken9lsBatCpnBZBAikq2ygNgrl1tLtR9K5QR15VVTWrJmjC1S5EUC9gbC6yTVsGkBDE1FOHNS9m+23RBxalfTuv0jJPPQkRiV7lSmGkjE4nGlyElxNG+3Q2BsmJLKWGiQsCUCsGiV7mMC5EugsUMUUEWDPkpLvWpN0141Rrv3RvNP7lYhxL92e2WNOHBggh9DEGhWZG+0K1urRu4RjHBjODynyG5EOGiQZmjL3mQkDG1F7gKG+0O35tpsFto3NxhhWFxpPkVhUHSFSm5CNHVhLjJyNgpzZ2p2thC0FRBsJIy113VA4h-3yAyhpnSG9mAU5tSBPzL1ttKHsA-lMODGhXqFsI6vz1TxSD1FsB9s93L3QL9rsBNEDotGDvUFDuzw+tWDOFjNo1AT6qpOKk4K8IYoDWokTqApVG-n9AaFkBZq-0dl-zeL+NkFjsHzPwv00QrokCjAtw7TljOArF7ijAeTq1kpWDQMrz9ochDxDH9xMCUFSgJMAlnA6wXHNBeJXCWsngYPaSYI1y11QETtDGePAn9wkoRvDDZr2EegekcG1F+OkNkIXwUMkiUOnvqHMAkEARLnU2ZFsOEKoNsBDDZhAtcJCMK19oFsPLqF91WDSI+I5EshnL1R60ZvU3AfcPdxjx8N+SCHUXn0fE+DLU+ETqJLgaapykEmXv6nLJUCyuQnRm3uKjcNCIH3L1wagH8HCPoi4UPJAvSBsBWxbniJrKuXzuSNo1pgLzJC3r62WowiyLAByNgDyOnqnH7g2BSCRjblsCvrOBmoIUZisSlLaJXgONuMRGnoEbuhDCUHlm7nhjKGVBiWlzLF-wSvkcpoQqBMRNQFUXtKn2sbZGSLOEopukHrMQTINGsTITWH9jOHvP8kurrmCakAKTlg7hcxZoOnMGiQDDBR1UylOoGu8LtKBPzJKFVvyRCYyfCcDDCcskegHTKPqH2C6xcOYcnR8elJSbTMLLSdCZo1VAcFAvEead21NAtUZikK6fgtfEQsqfTKTkGYyYehGehTzg+oHj3jhqxxKZtIwhpKBPbMxS2msYyzugbBsskR3ymsyk+pjHZH9iSdvlXPaP+guZT34iRh9lueXrsEspWPwWznUGQleefGQVQSeHQUGugfFk0HSdSGGdWFGZ7mabIRk1kEcGNEWq8eKmOelKwtYXYQueBSsKKUHjEaMFsGPLWEvUUEaDAbmY3HosYuYpqvhYAgEvgLrGEY2HBU4l1EyEmShuQ0-miXssUuUt+VUt4s5eqfFgMcel5DmPkGazGcQAkosCvSNEAQRlma8a8s4Flecr4FcrwckmnuabBSt04lyARRZtYlHNRliJOB5ETJcCcCAA */
id: 'Modeling',
tsTypes: {} as import('./modelingMachine.typegen').Typegen0,
predictableActionArguments: true,
preserveActionOrder: true,
context: {
guiMode: 'default',
tool: null as Models['SceneToolType_type'] | null,
selection: [] as string[],
selectionRanges: {
otherSelections: [],
codeBasedSelections: [],
} as Selections,
sketchDetails: {
sketchPathToNode: [],
zAxis: [0, 0, 1],
yAxis: [0, 1, 0],
origin: [0, 0, 0],
} as null | SketchDetails,
sketchPlaneId: '' as string,
sketchEnginePathId: '' as string,
moveDescs: [] as MoveDesc[],
mouseState: { type: 'idle' } as MouseState,
segmentOverlays: {} as SegmentOverlays,
segmentHoverMap: {} as { [pathToNodeString: string]: number },
},
schema: {
events: {} as ModelingMachineEvent,
},
states: {
idle: {
on: {
'Enter sketch': [
{
target: 'animating to existing sketch',
cond: 'Selection is on face',
},
'Sketch no face',
],
Extrude: {
target: 'idle',
cond: 'has valid extrude selection',
actions: ['AST extrude'],
internal: true,
},
Export: {
target: 'idle',
internal: true,
cond: 'Has exportable geometry',
actions: 'Engine export',
},
},
entry: 'reset client scene mouse handlers',
},
Sketch: {
states: {
SketchIdle: {
on: {
'Make segment vertical': {
cond: 'Can make selection vertical',
target: 'Await constrain vertically',
},
'Make segment horizontal': {
cond: 'Can make selection horizontal',
target: 'Await constrain horizontally',
},
'Constrain horizontal distance': {
target: 'Await horizontal distance info',
cond: 'Can constrain horizontal distance',
},
'Constrain vertical distance': {
target: 'Await vertical distance info',
cond: 'Can constrain vertical distance',
},
'Constrain ABS X': {
target: 'Await ABS X info',
cond: 'Can constrain ABS X',
},
'Constrain ABS Y': {
target: 'Await ABS Y info',
cond: 'Can constrain ABS Y',
},
'Constrain angle': {
target: 'Await angle info',
cond: 'Can constrain angle',
},
'Constrain length': {
target: 'Await length info',
cond: 'Can constrain length',
},
'Constrain perpendicular distance': {
target: 'Await perpendicular distance info',
cond: 'Can constrain perpendicular distance',
},
'Constrain horizontally align': {
cond: 'Can constrain horizontally align',
target: 'Await constrain horizontally align',
},
'Constrain vertically align': {
cond: 'Can constrain vertically align',
target: 'Await constrain vertically align',
},
'Constrain snap to X': {
cond: 'Can constrain snap to X',
target: 'Await constrain snap to X',
},
'Constrain snap to Y': {
cond: 'Can constrain snap to Y',
target: 'Await constrain snap to Y',
},
'Constrain equal length': {
cond: 'Can constrain equal length',
target: 'Await constrain equal length',
},
'Constrain parallel': {
target: 'Await constrain parallel',
cond: 'Can canstrain parallel',
},
'Constrain remove constraints': {
cond: 'Can constrain remove constraints',
target: 'Await constrain remove constraints',
},
'Re-execute': {
target: 'SketchIdle',
internal: true,
actions: ['set sketchMetadata from pathToNode'],
},
'Equip Line tool': 'Line tool',
'Equip tangential arc to': {
target: 'Tangential arc to',
cond: 'is editing existing sketch',
},
'Equip rectangle tool': {
target: 'Rectangle tool',
cond: 'Sketch is empty',
},
'code edit during sketch': 'clean slate',
'Convert to variable': {
target: 'Await convert to variable',
cond: 'Can convert to variable',
},
},
entry: 'setup client side sketch segments',
},
'Await horizontal distance info': {
invoke: {
src: 'Get horizontal info',
id: 'get-horizontal-info',
onDone: {
target: 'SketchIdle',
actions: 'Set selection',
},
onError: 'SketchIdle',
},
},
'Await vertical distance info': {
invoke: {
src: 'Get vertical info',
id: 'get-vertical-info',
onDone: {
target: 'SketchIdle',
actions: 'Set selection',
},
onError: 'SketchIdle',
},
},
'Await ABS X info': {
invoke: {
src: 'Get ABS X info',
id: 'get-abs-x-info',
onDone: {
target: 'SketchIdle',
actions: 'Set selection',
},
onError: 'SketchIdle',
},
},
'Await ABS Y info': {
invoke: {
src: 'Get ABS Y info',
id: 'get-abs-y-info',
onDone: {
target: 'SketchIdle',
actions: 'Set selection',
},
onError: 'SketchIdle',
},
},
'Await angle info': {
invoke: {
src: 'Get angle info',
id: 'get-angle-info',
onDone: {
target: 'SketchIdle',
actions: 'Set selection',
},
onError: 'SketchIdle',
},
},
'Await length info': {
invoke: {
src: 'Get length info',
id: 'get-length-info',
onDone: {
target: 'SketchIdle',
actions: 'Set selection',
},
onError: 'SketchIdle',
},
},
'Await perpendicular distance info': {
invoke: {
src: 'Get perpendicular distance info',
id: 'get-perpendicular-distance-info',
onDone: {
target: 'SketchIdle',
actions: 'Set selection',
},
onError: 'SketchIdle',
},
},
'Line tool': {
exit: [],
on: {
'Equip tangential arc to': {
target: 'Tangential arc to',
cond: 'is editing existing sketch',
},
'Equip rectangle tool': {
target: 'Rectangle tool',
cond: 'Sketch is empty',
},
},
states: {
Init: {
always: [
{
target: 'normal',
cond: 'is editing existing sketch',
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',
},
Init: {
always: [
{
target: 'SketchIdle',
cond: 'is editing existing sketch',
},
'Line tool',
],
},
'Tangential arc to': {
entry: 'set up draft arc',
on: {
'Equip Line tool': 'Line tool',
},
},
'undo startSketchOn': {
invoke: {
src: 'AST-undo-startSketchOn',
id: 'AST-undo-startSketchOn',
onDone: '#Modeling.idle',
},
},
'Rectangle tool': {
entry: ['listen for rectangle origin'],
states: {
'Awaiting second corner': {},
'Awaiting origin': {
on: {
'Add rectangle origin': {
target: 'Awaiting second corner',
actions: 'set up draft rectangle',
},
},
},
},
initial: 'Awaiting origin',
},
'clean slate': {
always: 'SketchIdle',
},
'Await convert to variable': {
invoke: {
src: 'Get convert to variable info',
id: 'get-convert-to-variable-info',
onError: 'SketchIdle',
onDone: {
target: 'SketchIdle',
actions: 'Set sketchDetails',
},
},
},
'Await constrain remove constraints': {
invoke: {
src: 'do-constrain-remove-constraint',
id: 'do-constrain-remove-constraint',
onDone: {
target: 'SketchIdle',
actions: 'Set selection',
},
},
},
'Await constrain horizontally': {
invoke: {
src: 'do-constrain-horizontally',
id: 'do-constrain-horizontally',
onDone: {
target: 'SketchIdle',
actions: 'Set selection',
},
},
},
'Await constrain vertically': {
invoke: {
src: 'do-constrain-vertically',
id: 'do-constrain-vertically',
onDone: {
target: 'SketchIdle',
actions: 'Set selection',
},
},
},
'Await constrain horizontally align': {
invoke: {
src: 'do-constrain-horizontally-align',
id: 'do-constrain-horizontally-align',
onDone: {
target: 'SketchIdle',
actions: 'Set selection',
},
},
},
'Await constrain vertically align': {
invoke: {
src: 'do-constrain-vertically-align',
id: 'do-constrain-vertically-align',
onDone: {
target: 'SketchIdle',
actions: 'Set selection',
},
},
},
'Await constrain snap to X': {
invoke: {
src: 'do-constrain-snap-to-x',
id: 'do-constrain-snap-to-x',
onDone: {
target: 'SketchIdle',
actions: 'Set selection',
},
},
},
'Await constrain snap to Y': {
invoke: {
src: 'do-constrain-snap-to-y',
id: 'do-constrain-snap-to-y',
onDone: {
target: 'SketchIdle',
actions: 'Set selection',
},
},
},
'Await constrain equal length': {
invoke: {
src: 'do-constrain-equal-length',
id: 'do-constrain-equal-length',
onDone: {
target: 'SketchIdle',
actions: 'Set selection',
},
},
},
'Await constrain parallel': {
invoke: {
src: 'do-constrain-parallel',
id: 'do-constrain-parallel',
onDone: {
target: 'SketchIdle',
actions: 'Set selection',
},
},
},
},
initial: 'Init',
on: {
CancelSketch: '.SketchIdle',
'Delete segment': {
internal: true,
actions: 'Delete segment',
},
'code edit during sketch': '.clean slate',
},
exit: [
'sketch exit execute',
'animate after sketch',
'tear down client sketch',
'remove sketch grid',
'engineToClient cam sync direction',
'Reset Segment Overlays',
],
entry: [
'add axis n grid',
'conditionally equip line tool',
'clientToEngine cam sync direction',
],
},
'Sketch no face': {
entry: ['show default planes', 'set selection filter to faces only'],
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',
onDone: {
target: 'Sketch',
actions: 'set new sketch metadata',
},
},
},
'animating to existing sketch': {
invoke: [
{
src: 'animate-to-sketch',
id: 'animate-to-sketch',
onDone: {
target: 'Sketch',
actions: 'set new sketch metadata',
},
},
],
},
},
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'],
},
'Set selection': {
internal: true,
actions: 'Set selection',
},
'Set mouse state': {
internal: true,
actions: 'Set mouse state',
},
'Set Segment Overlays': {
internal: true,
actions: 'Set Segment Overlays',
},
},
},
{
guards: {
'is editing existing sketch': ({ sketchDetails }) => {
// 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'
).node
if (variableDeclaration.type !== 'VariableDeclarator') return false
const pipeExpression = variableDeclaration.init
if (pipeExpression.type !== 'PipeExpression') return false
const hasStartProfileAt = pipeExpression.body.some(
(item) =>
item.type === 'CallExpression' &&
item.callee.name === 'startProfileAt'
)
return hasStartProfileAt && pipeExpression.body.length > 2
},
'Can make selection horizontal': ({ selectionRanges }) =>
horzVertInfo(selectionRanges, 'horizontal').enabled,
'Can make selection vertical': ({ selectionRanges }) =>
horzVertInfo(selectionRanges, 'vertical').enabled,
'Can constrain horizontal distance': ({ selectionRanges }) =>
horzVertDistanceInfo({ selectionRanges, constraint: 'setHorzDistance' })
.enabled,
'Can constrain vertical distance': ({ selectionRanges }) =>
horzVertDistanceInfo({ selectionRanges, constraint: 'setVertDistance' })
.enabled,
'Can constrain ABS X': ({ selectionRanges }) =>
absDistanceInfo({ selectionRanges, constraint: 'xAbs' }).enabled,
'Can constrain ABS Y': ({ selectionRanges }) =>
absDistanceInfo({ selectionRanges, constraint: 'yAbs' }).enabled,
'Can constrain angle': ({ selectionRanges }) =>
angleBetweenInfo({ selectionRanges }).enabled ||
angleLengthInfo({ selectionRanges, angleOrLength: 'setAngle' }).enabled,
'Can constrain length': ({ selectionRanges }) =>
angleLengthInfo({ selectionRanges }).enabled,
'Can constrain perpendicular distance': ({ selectionRanges }) =>
intersectInfo({ selectionRanges }).enabled,
'Can constrain horizontally align': ({ selectionRanges }) =>
horzVertDistanceInfo({ selectionRanges, constraint: 'setHorzDistance' })
.enabled,
'Can constrain vertically align': ({ selectionRanges }) =>
horzVertDistanceInfo({ selectionRanges, constraint: 'setHorzDistance' })
.enabled,
'Can constrain snap to X': ({ selectionRanges }) =>
absDistanceInfo({ selectionRanges, constraint: 'snapToXAxis' }).enabled,
'Can constrain snap to Y': ({ selectionRanges }) =>
absDistanceInfo({ selectionRanges, constraint: 'snapToYAxis' }).enabled,
'Can constrain equal length': ({ selectionRanges }) =>
setEqualLengthInfo({ selectionRanges }).enabled,
'Can canstrain parallel': ({ selectionRanges }) =>
equalAngleInfo({ selectionRanges }).enabled,
'Can constrain remove constraints': ({ selectionRanges }, { data }) =>
removeConstrainingValuesInfo({
selectionRanges,
pathToNodes: data && [data],
}).enabled,
'Can convert to variable': (_, { data }) => {
if (!data) return false
return isNodeSafeToReplacePath(
parse(recast(kclManager.ast)),
data.pathToNode
).isSafe
},
},
// end guards
actions: {
'set sketchMetadata from pathToNode': assign(({ sketchDetails }) => {
if (!sketchDetails?.sketchPathToNode || !sketchDetails) return {}
return {
sketchDetails: {
...sketchDetails,
sketchPathToNode: sketchDetails.sketchPathToNode,
},
}
}),
'hide default planes': () => {
sceneInfra.removeDefaultPlanes()
kclManager.hidePlanes()
},
'reset sketch metadata': assign({
sketchDetails: null,
sketchEnginePathId: '',
sketchPlaneId: '',
}),
'set new sketch metadata': assign((_, { data }) => ({
sketchDetails: data,
})),
'AST extrude': async (_, event) => {
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.codeBasedSelections[0].range
)
const { modifiedAst, pathToExtrudeArg } = extrudeSketch(
ast,
pathToNode,
false,
'variableName' in distance
? distance.variableIdentifierAst
: distance.valueAst
)
const selections = await kclManager.updateAst(modifiedAst, true, {
focusPath: pathToExtrudeArg,
})
if (selections) {
editorManager.selectRange(selections)
}
},
'conditionally equip line tool': (_, { type }) => {
if (type === 'done.invoke.animate-to-face') {
sceneInfra.modelingSend('Equip Line tool')
}
},
'setup client side sketch segments': ({ sketchDetails }) => {
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,
})
sceneInfra.resetMouseListeners()
sceneEntitiesManager.setupSketchIdleCallbacks({
pathToNode: sketchDetails?.sketchPathToNode || [],
forward: sketchDetails.zAxis,
up: sketchDetails.yAxis,
position: sketchDetails.origin,
})
})()
},
'animate after sketch': () => {
engineCommandManager
.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'sketch_mode_disable',
},
})
.then(async () => {
// there doesn't appear to be an animation, but if there was one we could add a wait here
await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_set_perspective',
},
})
sceneInfra.camControls.syncDirection = 'engineToClient'
await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_set_perspective',
},
})
const center = { x: 0, y: 0, z: 0 }
const camPos = sceneInfra.camControls.camera.position
if (camPos.x === 0 && camPos.y === 0) {
// looking straight up or down is going to cause issues with the engine
// tweaking the center to be a little off center
// TODO come up with a proper fix
center.y = 0.05
}
await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
center,
vantage: sceneInfra.camControls.camera.position,
up: { x: 0, y: 0, z: 1 },
},
})
await engineCommandManager.sendSceneCommand({
// CameraControls subscribes to default_camera_get_settings response events
// firing this at connection ensure the camera's are synced initially
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
})
})
},
'tear down client sketch': () => {
if (sceneEntitiesManager.activeSegments) {
sceneEntitiesManager.tearDownSketch({ removeAxis: false })
}
},
'remove sketch grid': () => sceneEntitiesManager.removeSketchGrid(),
'set up draft line': ({ sketchDetails }) => {
if (!sketchDetails) return
sceneEntitiesManager.setUpDraftSegment(
sketchDetails.sketchPathToNode,
sketchDetails.zAxis,
sketchDetails.yAxis,
sketchDetails.origin,
'line'
)
},
'set up draft arc': ({ sketchDetails }) => {
if (!sketchDetails) return
sceneEntitiesManager.setUpDraftSegment(
sketchDetails.sketchPathToNode,
sketchDetails.zAxis,
sketchDetails.yAxis,
sketchDetails.origin,
'tangentialArcTo'
)
},
'listen for rectangle origin': ({ sketchDetails }) => {
if (!sketchDetails) return
sceneEntitiesManager.setupRectangleOriginListener()
},
'set up draft rectangle': ({ sketchDetails }, { data }) => {
if (!sketchDetails || !data) return
sceneEntitiesManager.setupDraftRectangle(
sketchDetails.sketchPathToNode,
sketchDetails.zAxis,
sketchDetails.yAxis,
sketchDetails.origin,
data
)
},
'set up draft line without teardown': ({ sketchDetails }) => {
if (!sketchDetails) return
sceneEntitiesManager.setUpDraftSegment(
sketchDetails.sketchPathToNode,
sketchDetails.zAxis,
sketchDetails.yAxis,
sketchDetails.origin,
'line',
false
)
},
'show default planes': () => {
sceneInfra.showDefaultPlanes()
sceneEntitiesManager.setupDefaultPlaneHover()
kclManager.showPlanes()
},
'setup noPoints onClick listener': ({ sketchDetails }) => {
if (!sketchDetails) return
sceneEntitiesManager.createIntersectionPlane()
const quaternion = quaternionFromUpNForward(
new Vector3(...sketchDetails.yAxis),
new Vector3(...sketchDetails.zAxis)
)
sceneEntitiesManager.intersectionPlane &&
sceneEntitiesManager.intersectionPlane.setRotationFromQuaternion(
quaternion
)
sceneEntitiesManager.intersectionPlane &&
sceneEntitiesManager.intersectionPlane.position.copy(
new Vector3(...(sketchDetails?.origin || [0, 0, 0]))
)
sceneInfra.setCallbacks({
onClick: async (args) => {
if (!args) return
if (args.mouseEvent.which !== 1) return
const { intersectionPoint } = args
if (!intersectionPoint?.twoD || !sketchDetails?.sketchPathToNode)
return
const { modifiedAst } = addStartProfileAt(
kclManager.ast,
sketchDetails.sketchPathToNode,
[intersectionPoint.twoD.x, intersectionPoint.twoD.y]
)
await kclManager.updateAst(modifiedAst, false)
sceneEntitiesManager.removeIntersectionPlane()
sceneInfra.modelingSend('Add start point')
},
})
},
'add axis n grid': ({ sketchDetails }) => {
if (!sketchDetails) return
sceneEntitiesManager.createSketchAxis(
sketchDetails.sketchPathToNode || [],
sketchDetails.zAxis,
sketchDetails.yAxis,
sketchDetails.origin
)
},
'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'
},
'set selection filter to faces only': () =>
engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'set_selection_filter',
filter: ['face', 'plane'],
},
}),
'set selection filter to defaults': () =>
kclManager.defaultSelectionFilter(),
'Delete segment': ({ sketchDetails }, { data: pathToNode }) =>
deleteSegment({ pathToNode, sketchDetails }),
'Reset Segment Overlays': () => sceneEntitiesManager.resetOverlays(),
},
// end actions
services: {
'do-constrain-remove-constraint': async (
{ selectionRanges, sketchDetails },
{ data }
) => {
const { modifiedAst, pathToNodeMap } = applyRemoveConstrainingValues({
selectionRanges,
pathToNodes: data && [data],
})
if (!sketchDetails) return
sceneEntitiesManager.updateAstAndRejigSketch(
sketchDetails?.sketchPathToNode || [],
modifiedAst,
sketchDetails.zAxis,
sketchDetails.yAxis,
sketchDetails.origin
)
if (!sketchDetails) return
await sceneEntitiesManager.updateAstAndRejigSketch(
sketchDetails.sketchPathToNode,
modifiedAst,
sketchDetails.zAxis,
sketchDetails.yAxis,
sketchDetails.origin
)
return {
selectionType: 'completeSelection',
selection: updateSelections(
pathToNodeMap,
selectionRanges,
parse(recast(modifiedAst))
),
}
},
'do-constrain-horizontally': async ({
selectionRanges,
sketchDetails,
}) => {
const { modifiedAst, pathToNodeMap } = applyConstraintHorzVert(
selectionRanges,
'horizontal',
kclManager.ast,
kclManager.programMemory
)
if (!sketchDetails) return
await sceneEntitiesManager.updateAstAndRejigSketch(
sketchDetails.sketchPathToNode,
modifiedAst,
sketchDetails.zAxis,
sketchDetails.yAxis,
sketchDetails.origin
)
return {
selectionType: 'completeSelection',
selection: updateSelections(
pathToNodeMap,
selectionRanges,
parse(recast(modifiedAst))
),
}
},
'do-constrain-vertically': async ({ selectionRanges, sketchDetails }) => {
const { modifiedAst, pathToNodeMap } = applyConstraintHorzVert(
selectionRanges,
'vertical',
kclManager.ast,
kclManager.programMemory
)
if (!sketchDetails) return
await sceneEntitiesManager.updateAstAndRejigSketch(
sketchDetails.sketchPathToNode || [],
modifiedAst,
sketchDetails.zAxis,
sketchDetails.yAxis,
sketchDetails.origin
)
return {
selectionType: 'completeSelection',
selection: updateSelections(
pathToNodeMap,
selectionRanges,
parse(recast(modifiedAst))
),
}
},
'do-constrain-horizontally-align': async ({
selectionRanges,
sketchDetails,
}) => {
const { modifiedAst, pathToNodeMap } = applyConstraintHorzVertAlign({
selectionRanges,
constraint: 'setVertDistance',
})
if (!sketchDetails) return
await sceneEntitiesManager.updateAstAndRejigSketch(
sketchDetails?.sketchPathToNode || [],
modifiedAst,
sketchDetails.zAxis,
sketchDetails.yAxis,
sketchDetails.origin
)
const updatedSelectionRanges = updateSelections(
pathToNodeMap,
selectionRanges,
parse(recast(modifiedAst))
)
return {
selectionType: 'completeSelection',
selection: updatedSelectionRanges,
}
},
'do-constrain-vertically-align': async ({
selectionRanges,
sketchDetails,
}) => {
const { modifiedAst, pathToNodeMap } = applyConstraintHorzVertAlign({
selectionRanges,
constraint: 'setHorzDistance',
})
if (!sketchDetails) return
await sceneEntitiesManager.updateAstAndRejigSketch(
sketchDetails?.sketchPathToNode || [],
modifiedAst,
sketchDetails.zAxis,
sketchDetails.yAxis,
sketchDetails.origin
)
const updatedSelectionRanges = updateSelections(
pathToNodeMap,
selectionRanges,
parse(recast(modifiedAst))
)
return {
selectionType: 'completeSelection',
selection: updatedSelectionRanges,
}
},
'do-constrain-snap-to-x': async ({ selectionRanges, sketchDetails }) => {
const { modifiedAst, pathToNodeMap } = applyConstraintAxisAlign({
selectionRanges,
constraint: 'snapToXAxis',
})
if (!sketchDetails) return
await sceneEntitiesManager.updateAstAndRejigSketch(
sketchDetails?.sketchPathToNode || [],
modifiedAst,
sketchDetails.zAxis,
sketchDetails.yAxis,
sketchDetails.origin
)
const updatedSelectionRanges = updateSelections(
pathToNodeMap,
selectionRanges,
parse(recast(modifiedAst))
)
return {
selectionType: 'completeSelection',
selection: updatedSelectionRanges,
}
},
'do-constrain-snap-to-y': async ({ selectionRanges, sketchDetails }) => {
const { modifiedAst, pathToNodeMap } = applyConstraintAxisAlign({
selectionRanges,
constraint: 'snapToYAxis',
})
if (!sketchDetails) return
await sceneEntitiesManager.updateAstAndRejigSketch(
sketchDetails?.sketchPathToNode || [],
modifiedAst,
sketchDetails.zAxis,
sketchDetails.yAxis,
sketchDetails.origin
)
const updatedSelectionRanges = updateSelections(
pathToNodeMap,
selectionRanges,
parse(recast(modifiedAst))
)
return {
selectionType: 'completeSelection',
selection: updatedSelectionRanges,
}
},
'do-constrain-parallel': async ({ selectionRanges, sketchDetails }) => {
const { modifiedAst, pathToNodeMap } = applyConstraintEqualAngle({
selectionRanges,
})
if (!sketchDetails) throw new Error('No sketch details')
await sceneEntitiesManager.updateAstAndRejigSketch(
sketchDetails?.sketchPathToNode || [],
parse(recast(modifiedAst)),
sketchDetails.zAxis,
sketchDetails.yAxis,
sketchDetails.origin
)
const updatedSelectionRanges = updateSelections(
pathToNodeMap,
selectionRanges,
parse(recast(modifiedAst))
)
return {
selectionType: 'completeSelection',
selection: updatedSelectionRanges,
}
},
'do-constrain-equal-length': async ({
selectionRanges,
sketchDetails,
}) => {
const { modifiedAst, pathToNodeMap } = applyConstraintEqualLength({
selectionRanges,
})
if (!sketchDetails) return
await sceneEntitiesManager.updateAstAndRejigSketch(
sketchDetails?.sketchPathToNode || [],
modifiedAst,
sketchDetails.zAxis,
sketchDetails.yAxis,
sketchDetails.origin
)
const updatedSelectionRanges = updateSelections(
pathToNodeMap,
selectionRanges,
parse(recast(modifiedAst))
)
return {
selectionType: 'completeSelection',
selection: updatedSelectionRanges,
}
},
},
// end services
}
)