2023-10-11 13:36:54 +11:00
|
|
|
import { useMachine } from '@xstate/react'
|
|
|
|
import React, { createContext, useEffect, useRef } from 'react'
|
|
|
|
import {
|
|
|
|
AnyStateMachine,
|
|
|
|
ContextFrom,
|
|
|
|
InterpreterFrom,
|
|
|
|
Prop,
|
|
|
|
StateFrom,
|
|
|
|
assign,
|
|
|
|
} from 'xstate'
|
|
|
|
import { SetSelections, modelingMachine } from 'machines/modelingMachine'
|
|
|
|
import { useSetupEngineManager } from 'hooks/useSetupEngineManager'
|
|
|
|
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
2023-10-11 15:12:29 +11:00
|
|
|
import { isCursorInSketchCommandRange } from 'lang/util'
|
2023-10-11 13:36:54 +11:00
|
|
|
import { engineCommandManager } from 'lang/std/engineConnection'
|
|
|
|
import { v4 as uuidv4 } from 'uuid'
|
|
|
|
import { addStartSketch } from 'lang/modifyAst'
|
|
|
|
import { roundOff } from 'lib/utils'
|
2023-10-14 03:47:46 +11:00
|
|
|
import {
|
|
|
|
recast,
|
|
|
|
parse,
|
|
|
|
Program,
|
|
|
|
PipeExpression,
|
|
|
|
CallExpression,
|
|
|
|
} from 'lang/wasm'
|
2023-10-11 13:36:54 +11:00
|
|
|
import { getNodeFromPath } from 'lang/queryAst'
|
|
|
|
import {
|
|
|
|
addCloseToPipe,
|
|
|
|
addNewSketchLn,
|
|
|
|
compareVec2Epsilon,
|
|
|
|
} from 'lang/std/sketch'
|
|
|
|
import { kclManager } from 'lang/KclSinglton'
|
|
|
|
import { applyConstraintHorzVertDistance } from './Toolbar/SetHorzVertDistance'
|
|
|
|
import { applyConstraintAngleBetween } from './Toolbar/SetAngleBetween'
|
|
|
|
import { applyConstraintAngleLength } from './Toolbar/setAngleLength'
|
|
|
|
import { toast } from 'react-hot-toast'
|
|
|
|
import { pathMapToSelections } from 'lang/util'
|
2023-10-16 21:20:05 +11:00
|
|
|
import { useStore } from 'useStore'
|
|
|
|
import { handleSelectionBatch, handleSelectionWithShift } from 'lib/selections'
|
2023-10-16 08:54:38 +11:00
|
|
|
import { applyConstraintIntersect } from './Toolbar/Intersect'
|
2023-10-11 13:36:54 +11:00
|
|
|
|
|
|
|
type MachineContext<T extends AnyStateMachine> = {
|
|
|
|
state: StateFrom<T>
|
|
|
|
context: ContextFrom<T>
|
|
|
|
send: Prop<InterpreterFrom<T>, 'send'>
|
|
|
|
}
|
|
|
|
|
|
|
|
export const ModelingMachineContext = createContext(
|
|
|
|
{} as MachineContext<typeof modelingMachine>
|
|
|
|
)
|
|
|
|
|
|
|
|
export const ModelingMachineProvider = ({
|
|
|
|
children,
|
|
|
|
}: {
|
|
|
|
children: React.ReactNode
|
|
|
|
}) => {
|
|
|
|
const { auth } = useGlobalStateContext()
|
|
|
|
const token = auth?.context?.token
|
|
|
|
const streamRef = useRef<HTMLDivElement>(null)
|
|
|
|
useSetupEngineManager(streamRef, token)
|
|
|
|
|
|
|
|
const { isShiftDown, editorView } = useStore((s) => ({
|
|
|
|
isShiftDown: s.isShiftDown,
|
|
|
|
editorView: s.editorView,
|
|
|
|
}))
|
|
|
|
|
|
|
|
// const { commands } = useCommandsContext()
|
|
|
|
|
|
|
|
// Settings machine setup
|
|
|
|
// const retrievedSettings = useRef(
|
|
|
|
// localStorage?.getItem(MODELING_PERSIST_KEY) || '{}'
|
|
|
|
// )
|
|
|
|
|
|
|
|
// What should we persist from modeling state? Nothing?
|
|
|
|
// const persistedSettings = Object.assign(
|
|
|
|
// settingsMachine.initialState.context,
|
|
|
|
// JSON.parse(retrievedSettings.current) as Partial<
|
|
|
|
// (typeof settingsMachine)['context']
|
|
|
|
// >
|
|
|
|
// )
|
|
|
|
|
|
|
|
const [modelingState, modelingSend] = useMachine(modelingMachine, {
|
|
|
|
// context: persistedSettings,
|
|
|
|
actions: {
|
|
|
|
'Modify AST': () => {},
|
|
|
|
'Update code selection cursors': () => {},
|
|
|
|
'show default planes': () => {
|
|
|
|
kclManager.showPlanes()
|
|
|
|
},
|
2023-10-13 10:27:25 +11:00
|
|
|
'create path': assign({
|
|
|
|
sketchEnginePathId: () => {
|
|
|
|
const sketchUuid = uuidv4()
|
2023-10-11 13:36:54 +11:00
|
|
|
engineCommandManager.sendSceneCommand({
|
|
|
|
type: 'modeling_cmd_req',
|
|
|
|
cmd_id: sketchUuid,
|
|
|
|
cmd: {
|
|
|
|
type: 'start_path',
|
|
|
|
},
|
2023-10-13 10:27:25 +11:00
|
|
|
})
|
2023-10-11 13:36:54 +11:00
|
|
|
engineCommandManager.sendSceneCommand({
|
|
|
|
type: 'modeling_cmd_req',
|
|
|
|
cmd_id: uuidv4(),
|
|
|
|
cmd: {
|
|
|
|
type: 'edit_mode_enter',
|
|
|
|
target: sketchUuid,
|
|
|
|
},
|
2023-10-13 10:27:25 +11:00
|
|
|
})
|
|
|
|
return sketchUuid
|
|
|
|
},
|
|
|
|
}),
|
2023-10-14 03:47:46 +11:00
|
|
|
'AST start new sketch': assign(
|
|
|
|
({ sketchEnginePathId }, { data: { coords, axis, segmentId } }) => {
|
|
|
|
if (!axis) {
|
|
|
|
// Something really weird must have happened for this to happen.
|
|
|
|
console.error('axis is undefined for starting a new sketch')
|
|
|
|
return {}
|
|
|
|
}
|
|
|
|
if (!segmentId) {
|
|
|
|
// Something really weird must have happened for this to happen.
|
|
|
|
console.error('segmentId is undefined for starting a new sketch')
|
|
|
|
return {}
|
|
|
|
}
|
2023-10-11 13:36:54 +11:00
|
|
|
|
2023-10-14 03:47:46 +11:00
|
|
|
const _addStartSketch = addStartSketch(
|
|
|
|
kclManager.ast,
|
|
|
|
axis,
|
|
|
|
[roundOff(coords[0].x), roundOff(coords[0].y)],
|
|
|
|
[
|
|
|
|
roundOff(coords[1].x - coords[0].x),
|
|
|
|
roundOff(coords[1].y - coords[0].y),
|
|
|
|
]
|
|
|
|
)
|
|
|
|
const _modifiedAst = _addStartSketch.modifiedAst
|
|
|
|
const _pathToNode = _addStartSketch.pathToNode
|
|
|
|
const newCode = recast(_modifiedAst)
|
|
|
|
const astWithUpdatedSource = parse(newCode)
|
|
|
|
const updatedPipeNode = getNodeFromPath<PipeExpression>(
|
|
|
|
astWithUpdatedSource,
|
|
|
|
_pathToNode
|
|
|
|
).node
|
|
|
|
const startProfileAtCallExp = updatedPipeNode.body.find(
|
|
|
|
(exp) =>
|
|
|
|
exp.type === 'CallExpression' &&
|
|
|
|
exp.callee.name === 'startProfileAt'
|
|
|
|
)
|
|
|
|
if (startProfileAtCallExp)
|
|
|
|
engineCommandManager.artifactMap[sketchEnginePathId] = {
|
|
|
|
type: 'result',
|
|
|
|
range: [startProfileAtCallExp.start, startProfileAtCallExp.end],
|
2023-11-06 11:49:13 +11:00
|
|
|
commandType: 'start_path',
|
2023-10-14 03:47:46 +11:00
|
|
|
data: null,
|
|
|
|
raw: {} as any,
|
|
|
|
}
|
|
|
|
const lineCallExp = updatedPipeNode.body.find(
|
|
|
|
(exp) => exp.type === 'CallExpression' && exp.callee.name === 'line'
|
|
|
|
)
|
|
|
|
if (lineCallExp)
|
|
|
|
engineCommandManager.artifactMap[segmentId] = {
|
|
|
|
type: 'result',
|
|
|
|
range: [lineCallExp.start, lineCallExp.end],
|
|
|
|
commandType: 'extend_path',
|
|
|
|
parentId: sketchEnginePathId,
|
|
|
|
data: null,
|
|
|
|
raw: {} as any,
|
|
|
|
}
|
2023-10-11 13:36:54 +11:00
|
|
|
|
2023-10-14 03:47:46 +11:00
|
|
|
kclManager.executeAstMock(astWithUpdatedSource, true)
|
2023-10-11 13:36:54 +11:00
|
|
|
|
2023-10-14 03:47:46 +11:00
|
|
|
return {
|
|
|
|
sketchPathToNode: _pathToNode,
|
|
|
|
}
|
2023-10-11 13:36:54 +11:00
|
|
|
}
|
2023-10-14 03:47:46 +11:00
|
|
|
),
|
2023-10-18 07:30:03 +11:00
|
|
|
'AST add line segment': async (
|
2023-10-14 03:47:46 +11:00
|
|
|
{ sketchPathToNode, sketchEnginePathId },
|
|
|
|
{ data: { coords, segmentId } }
|
|
|
|
) => {
|
2023-10-11 13:36:54 +11:00
|
|
|
if (!sketchPathToNode) return
|
|
|
|
const lastCoord = coords[coords.length - 1]
|
|
|
|
|
2023-10-18 07:30:03 +11:00
|
|
|
const pathInfo = await engineCommandManager.sendSceneCommand({
|
|
|
|
type: 'modeling_cmd_req',
|
|
|
|
cmd_id: uuidv4(),
|
|
|
|
cmd: {
|
|
|
|
type: 'path_get_info',
|
|
|
|
path_id: sketchEnginePathId,
|
|
|
|
},
|
|
|
|
})
|
|
|
|
const firstSegment = pathInfo?.data?.data?.segments.find(
|
|
|
|
(seg: any) => seg.command === 'line_to'
|
2023-10-11 13:36:54 +11:00
|
|
|
)
|
2023-10-18 07:30:03 +11:00
|
|
|
const firstSegCoords = await engineCommandManager.sendSceneCommand({
|
|
|
|
type: 'modeling_cmd_req',
|
|
|
|
cmd_id: uuidv4(),
|
|
|
|
cmd: {
|
|
|
|
type: 'curve_get_control_points',
|
|
|
|
curve_id: firstSegment.command_id,
|
|
|
|
},
|
|
|
|
})
|
|
|
|
const startPathCoord = firstSegCoords?.data?.data?.control_points[0]
|
2023-10-11 13:36:54 +11:00
|
|
|
|
2023-10-18 07:30:03 +11:00
|
|
|
const isClose = compareVec2Epsilon(
|
|
|
|
[startPathCoord.x, startPathCoord.y],
|
|
|
|
[lastCoord.x, lastCoord.y]
|
|
|
|
)
|
2023-10-11 13:36:54 +11:00
|
|
|
|
|
|
|
let _modifiedAst: Program
|
|
|
|
if (!isClose) {
|
2023-10-14 03:47:46 +11:00
|
|
|
const newSketchLn = addNewSketchLn({
|
2023-10-11 13:36:54 +11:00
|
|
|
node: kclManager.ast,
|
|
|
|
programMemory: kclManager.programMemory,
|
|
|
|
to: [lastCoord.x, lastCoord.y],
|
2023-10-18 07:30:03 +11:00
|
|
|
from: [coords[0].x, coords[0].y],
|
2023-10-11 13:36:54 +11:00
|
|
|
fnName: 'line',
|
|
|
|
pathToNode: sketchPathToNode,
|
2023-10-14 03:47:46 +11:00
|
|
|
})
|
|
|
|
const _modifiedAst = newSketchLn.modifiedAst
|
|
|
|
kclManager.executeAstMock(_modifiedAst, true).then(() => {
|
|
|
|
const lineCallExp = getNodeFromPath<CallExpression>(
|
|
|
|
kclManager.ast,
|
|
|
|
newSketchLn.pathToNode
|
|
|
|
).node
|
|
|
|
if (segmentId)
|
|
|
|
engineCommandManager.artifactMap[segmentId] = {
|
|
|
|
type: 'result',
|
|
|
|
range: [lineCallExp.start, lineCallExp.end],
|
|
|
|
commandType: 'extend_path',
|
|
|
|
parentId: sketchEnginePathId,
|
|
|
|
data: null,
|
|
|
|
raw: {} as any,
|
|
|
|
}
|
|
|
|
})
|
2023-10-11 13:36:54 +11:00
|
|
|
} else {
|
|
|
|
_modifiedAst = addCloseToPipe({
|
|
|
|
node: kclManager.ast,
|
|
|
|
programMemory: kclManager.programMemory,
|
|
|
|
pathToNode: sketchPathToNode,
|
|
|
|
})
|
|
|
|
engineCommandManager.sendSceneCommand({
|
|
|
|
type: 'modeling_cmd_req',
|
|
|
|
cmd_id: uuidv4(),
|
|
|
|
cmd: { type: 'edit_mode_exit' },
|
|
|
|
})
|
|
|
|
engineCommandManager.sendSceneCommand({
|
|
|
|
type: 'modeling_cmd_req',
|
|
|
|
cmd_id: uuidv4(),
|
|
|
|
cmd: { type: 'default_camera_disable_sketch_mode' },
|
|
|
|
})
|
|
|
|
kclManager.executeAstMock(_modifiedAst, true)
|
|
|
|
// updateAst(_modifiedAst, true)
|
|
|
|
}
|
|
|
|
},
|
|
|
|
'sketch exit execute': () => {
|
|
|
|
kclManager.executeAst()
|
|
|
|
},
|
|
|
|
'set tool': () => {}, // TODO
|
|
|
|
'toast extrude failed': () => {
|
|
|
|
toast.error(
|
|
|
|
'Extrude failed, sketches need to be closed, or not already extruded'
|
|
|
|
)
|
|
|
|
},
|
|
|
|
'Set selection': assign(({ selectionRanges }, event) => {
|
|
|
|
if (event.type !== 'Set selection') return {} // this was needed for ts after adding 'Set selection' action to on done modal events
|
|
|
|
const setSelections = event.data
|
|
|
|
if (setSelections.selectionType === 'mirrorCodeMirrorSelections')
|
|
|
|
return { selectionRanges: setSelections.selection }
|
|
|
|
else if (setSelections.selectionType === 'otherSelection')
|
|
|
|
return {
|
|
|
|
selectionRanges: {
|
|
|
|
...selectionRanges,
|
|
|
|
otherSelections: [setSelections.selection],
|
|
|
|
},
|
|
|
|
}
|
|
|
|
else if (!editorView) return {}
|
|
|
|
else if (setSelections.selectionType === 'singleCodeCursor') {
|
|
|
|
// This DOES NOT set the `selectionRanges` in xstate context
|
|
|
|
// instead it updates/dispatches to the editor, which in turn updates the xstate context
|
|
|
|
// I've found this the best way to deal with the editor without causing an infinite loop
|
|
|
|
// and really we want the editor to be in charge of cursor positions and for `selectionRanges` mirror it
|
|
|
|
// because we want to respect the user manually placing the cursor too.
|
2023-10-16 21:20:05 +11:00
|
|
|
|
|
|
|
// for more details on how selections see `src/lib/selections.ts`.
|
|
|
|
const { codeMirrorSelection, selectionRangeTypeMap } =
|
|
|
|
handleSelectionWithShift({
|
|
|
|
codeSelection: setSelections.selection,
|
|
|
|
currestSelections: selectionRanges,
|
|
|
|
isShiftDown,
|
|
|
|
})
|
|
|
|
if (codeMirrorSelection) {
|
|
|
|
setTimeout(() => {
|
|
|
|
editorView.dispatch({
|
|
|
|
selection: codeMirrorSelection,
|
|
|
|
})
|
|
|
|
})
|
2023-10-11 13:36:54 +11:00
|
|
|
}
|
2023-10-16 21:20:05 +11:00
|
|
|
return { selectionRangeTypeMap }
|
2023-10-11 13:36:54 +11:00
|
|
|
}
|
|
|
|
// This DOES NOT set the `selectionRanges` in xstate context
|
|
|
|
// same as comment above
|
2023-10-16 21:20:05 +11:00
|
|
|
const { codeMirrorSelection, selectionRangeTypeMap } =
|
|
|
|
handleSelectionBatch({
|
|
|
|
selections: setSelections.selection,
|
|
|
|
})
|
|
|
|
if (codeMirrorSelection) {
|
|
|
|
setTimeout(() => {
|
|
|
|
editorView.dispatch({
|
|
|
|
selection: codeMirrorSelection,
|
|
|
|
})
|
|
|
|
})
|
2023-10-11 13:36:54 +11:00
|
|
|
}
|
2023-10-16 21:20:05 +11:00
|
|
|
return { selectionRangeTypeMap }
|
2023-10-11 13:36:54 +11:00
|
|
|
}),
|
|
|
|
},
|
|
|
|
guards: {
|
|
|
|
'Selection contains axis': () => true,
|
|
|
|
'Selection contains edge': () => true,
|
|
|
|
'Selection contains face': () => true,
|
|
|
|
'Selection contains line': () => true,
|
|
|
|
'Selection contains point': () => true,
|
|
|
|
'Selection is not empty': () => true,
|
|
|
|
'Selection is one face': ({ selectionRanges }) => {
|
|
|
|
return !!isCursorInSketchCommandRange(
|
|
|
|
engineCommandManager.artifactMap,
|
|
|
|
selectionRanges
|
|
|
|
)
|
|
|
|
},
|
|
|
|
},
|
|
|
|
services: {
|
|
|
|
'Get horizontal info': async ({
|
|
|
|
selectionRanges,
|
|
|
|
}): Promise<SetSelections> => {
|
|
|
|
const { modifiedAst, pathToNodeMap } =
|
|
|
|
await applyConstraintHorzVertDistance({
|
|
|
|
constraint: 'setHorzDistance',
|
|
|
|
selectionRanges,
|
|
|
|
})
|
|
|
|
await kclManager.updateAst(modifiedAst, true)
|
|
|
|
return {
|
|
|
|
selectionType: 'completeSelection',
|
|
|
|
selection: pathMapToSelections(
|
|
|
|
kclManager.ast,
|
|
|
|
selectionRanges,
|
|
|
|
pathToNodeMap
|
|
|
|
),
|
|
|
|
}
|
|
|
|
},
|
|
|
|
'Get vertical info': async ({
|
|
|
|
selectionRanges,
|
|
|
|
}): Promise<SetSelections> => {
|
|
|
|
const { modifiedAst, pathToNodeMap } =
|
|
|
|
await applyConstraintHorzVertDistance({
|
|
|
|
constraint: 'setVertDistance',
|
|
|
|
selectionRanges,
|
|
|
|
})
|
|
|
|
await kclManager.updateAst(modifiedAst, true)
|
|
|
|
return {
|
|
|
|
selectionType: 'completeSelection',
|
|
|
|
selection: pathMapToSelections(
|
|
|
|
kclManager.ast,
|
|
|
|
selectionRanges,
|
|
|
|
pathToNodeMap
|
|
|
|
),
|
|
|
|
}
|
|
|
|
},
|
|
|
|
'Get angle info': async ({ selectionRanges }): Promise<SetSelections> => {
|
|
|
|
const { modifiedAst, pathToNodeMap } =
|
|
|
|
await applyConstraintAngleBetween({
|
|
|
|
selectionRanges,
|
|
|
|
})
|
|
|
|
await kclManager.updateAst(modifiedAst, true)
|
|
|
|
return {
|
|
|
|
selectionType: 'completeSelection',
|
|
|
|
selection: pathMapToSelections(
|
|
|
|
kclManager.ast,
|
|
|
|
selectionRanges,
|
|
|
|
pathToNodeMap
|
|
|
|
),
|
|
|
|
}
|
|
|
|
},
|
|
|
|
'Get length info': async ({
|
|
|
|
selectionRanges,
|
|
|
|
}): Promise<SetSelections> => {
|
|
|
|
const { modifiedAst, pathToNodeMap } = await applyConstraintAngleLength(
|
|
|
|
{ selectionRanges }
|
|
|
|
)
|
|
|
|
await kclManager.updateAst(modifiedAst, true)
|
|
|
|
return {
|
|
|
|
selectionType: 'completeSelection',
|
|
|
|
selection: pathMapToSelections(
|
|
|
|
kclManager.ast,
|
|
|
|
selectionRanges,
|
|
|
|
pathToNodeMap
|
|
|
|
),
|
|
|
|
}
|
|
|
|
},
|
2023-10-16 08:54:38 +11:00
|
|
|
'Get perpendicular distance info': async ({
|
|
|
|
selectionRanges,
|
|
|
|
}): Promise<SetSelections> => {
|
|
|
|
const { modifiedAst, pathToNodeMap } = await applyConstraintIntersect({
|
|
|
|
selectionRanges,
|
|
|
|
})
|
|
|
|
await kclManager.updateAst(modifiedAst, true)
|
|
|
|
return {
|
|
|
|
selectionType: 'completeSelection',
|
|
|
|
selection: pathMapToSelections(
|
|
|
|
kclManager.ast,
|
|
|
|
selectionRanges,
|
|
|
|
pathToNodeMap
|
|
|
|
),
|
|
|
|
}
|
|
|
|
},
|
2023-10-11 13:36:54 +11:00
|
|
|
},
|
|
|
|
devTools: true,
|
|
|
|
})
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
engineCommandManager.onPlaneSelected((plane_id: string) => {
|
|
|
|
if (modelingState.nextEvents.includes('Select default plane')) {
|
|
|
|
modelingSend({
|
|
|
|
type: 'Select default plane',
|
|
|
|
data: { planeId: plane_id },
|
|
|
|
})
|
|
|
|
}
|
|
|
|
})
|
2023-10-14 03:47:46 +11:00
|
|
|
}, [modelingSend, modelingState.nextEvents])
|
2023-10-11 13:36:54 +11:00
|
|
|
|
2023-10-18 08:03:02 +11:00
|
|
|
useEffect(() => {
|
|
|
|
kclManager.registerExecuteCallback(() => {
|
|
|
|
modelingSend({ type: 'Re-execute' })
|
|
|
|
})
|
|
|
|
}, [modelingSend])
|
|
|
|
|
2023-10-11 13:36:54 +11:00
|
|
|
// useStateMachineCommands({
|
|
|
|
// state: settingsState,
|
|
|
|
// send: settingsSend,
|
|
|
|
// commands,
|
|
|
|
// owner: 'settings',
|
|
|
|
// commandBarMeta: settingsCommandBarMeta,
|
|
|
|
// })
|
|
|
|
|
|
|
|
return (
|
|
|
|
<ModelingMachineContext.Provider
|
|
|
|
value={{
|
|
|
|
state: modelingState,
|
|
|
|
context: modelingState.context,
|
|
|
|
send: modelingSend,
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
{/* TODO #818: maybe pass reff down to children/app.ts or render app.tsx directly?
|
|
|
|
since realistically it won't ever have generic children that isn't app.tsx */}
|
|
|
|
<div className="h-screen overflow-hidden select-none" ref={streamRef}>
|
|
|
|
{children}
|
|
|
|
</div>
|
|
|
|
</ModelingMachineContext.Provider>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
export default ModelingMachineProvider
|