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' import { isCursorInSketchCommandRange } from 'lang/util' import { engineCommandManager } from 'lang/std/engineConnection' import { v4 as uuidv4 } from 'uuid' import { addStartSketch } from 'lang/modifyAst' import { roundOff } from 'lib/utils' import { recast, parse, Program, VariableDeclarator } from 'lang/wasm' 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' import { dispatchCodeMirrorCursor, setCodeMirrorCursor, useStore, } from 'useStore' type MachineContext = { state: StateFrom context: ContextFrom send: Prop, 'send'> } export const ModelingMachineContext = createContext( {} as MachineContext ) export const ModelingMachineProvider = ({ children, }: { children: React.ReactNode }) => { const { auth } = useGlobalStateContext() const token = auth?.context?.token const streamRef = useRef(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() }, 'create path': async () => { const sketchUuid = uuidv4() const proms = [ engineCommandManager.sendSceneCommand({ type: 'modeling_cmd_req', cmd_id: sketchUuid, cmd: { type: 'start_path', }, }), engineCommandManager.sendSceneCommand({ type: 'modeling_cmd_req', cmd_id: uuidv4(), cmd: { type: 'edit_mode_enter', target: sketchUuid, }, }), ] await Promise.all(proms) }, 'AST start new sketch': assign((_, { data: { coords, axis } }) => { // Something really weird must have happened for this to happen. if (!axis) { console.error('axis is undefined for starting a new sketch') return {} } 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) kclManager.executeAstMock(astWithUpdatedSource, true) return { sketchPathToNode: _pathToNode, } }), 'AST add line segment': ({ sketchPathToNode }, { data: { coords } }) => { if (!sketchPathToNode) return const lastCoord = coords[coords.length - 1] const { node: varDec } = getNodeFromPath( kclManager.ast, sketchPathToNode, 'VariableDeclarator' ) const variableName = varDec.id.name const sketchGroup = kclManager.programMemory.root[variableName] if (!sketchGroup || sketchGroup.type !== 'SketchGroup') return const initialCoords = sketchGroup.value[0].from const isClose = compareVec2Epsilon(initialCoords, [ lastCoord.x, lastCoord.y, ]) let _modifiedAst: Program if (!isClose) { _modifiedAst = addNewSketchLn({ node: kclManager.ast, programMemory: kclManager.programMemory, to: [lastCoord.x, lastCoord.y], fnName: 'line', pathToNode: sketchPathToNode, }).modifiedAst kclManager.executeAstMock(_modifiedAst, true) // kclManager.updateAst(_modifiedAst, false) } 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. const selectionRangeTypeMap = setCodeMirrorCursor({ codeSelection: setSelections.selection, currestSelections: selectionRanges, editorView, isShiftDown, }) return { selectionRangeTypeMap, } } // This DOES NOT set the `selectionRanges` in xstate context // same as comment above const selectionRangeTypeMap = dispatchCodeMirrorCursor({ selections: setSelections.selection, editorView, }) return { selectionRangeTypeMap, } }), }, 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 => { 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 => { 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 => { 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 => { const { modifiedAst, pathToNodeMap } = await applyConstraintAngleLength( { selectionRanges } ) await kclManager.updateAst(modifiedAst, true) return { selectionType: 'completeSelection', selection: pathMapToSelections( kclManager.ast, selectionRanges, pathToNodeMap ), } }, }, devTools: true, }) useEffect(() => { engineCommandManager.onPlaneSelected((plane_id: string) => { if (modelingState.nextEvents.includes('Select default plane')) { modelingSend({ type: 'Select default plane', data: { planeId: plane_id }, }) } }) }, [kclManager.defaultPlanes, modelingSend, modelingState.nextEvents]) // useStateMachineCommands({ // state: settingsState, // send: settingsSend, // commands, // owner: 'settings', // commandBarMeta: settingsCommandBarMeta, // }) return ( {/* 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 */}
{children}
) } export default ModelingMachineProvider