Pass the ?pool query param through to the backend. This will slice off the ?pool= param and pass it to the WebSocket request, which requests that the Zoo API use a particular pool of engines. This isn't something any users of the zoo api require; but it's needed for the internal engine Zoo development workflow. This may be used in the future, but for now this'll be always enabled. Passing any value in the production servers will result in a "no backend" error for now.
557 lines
18 KiB
TypeScript
557 lines
18 KiB
TypeScript
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 { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
|
import { isCursorInSketchCommandRange } from 'lang/util'
|
|
import {
|
|
kclManager,
|
|
sceneInfra,
|
|
engineCommandManager,
|
|
codeManager,
|
|
editorManager,
|
|
} from 'lib/singletons'
|
|
import { applyConstraintHorzVertDistance } from './Toolbar/SetHorzVertDistance'
|
|
import {
|
|
angleBetweenInfo,
|
|
applyConstraintAngleBetween,
|
|
} from './Toolbar/SetAngleBetween'
|
|
import { applyConstraintAngleLength } from './Toolbar/setAngleLength'
|
|
import { pathMapToSelections } from 'lang/util'
|
|
import { useStore } from 'useStore'
|
|
import {
|
|
Selections,
|
|
canExtrudeSelection,
|
|
handleSelectionBatch,
|
|
isSelectionLastLine,
|
|
isSketchPipe,
|
|
} from 'lib/selections'
|
|
import { applyConstraintIntersect } from './Toolbar/Intersect'
|
|
import { applyConstraintAbsDistance } from './Toolbar/SetAbsDistance'
|
|
import useStateMachineCommands from 'hooks/useStateMachineCommands'
|
|
import { modelingMachineConfig } from 'lib/commandBarConfigs/modelingCommandConfig'
|
|
import {
|
|
getSketchOrientationDetails,
|
|
getSketchQuaternion,
|
|
} from 'clientSideScene/sceneEntities'
|
|
import { sketchOnExtrudedFace, startSketchOnDefault } from 'lang/modifyAst'
|
|
import { Program, VariableDeclaration, coreDump } from 'lang/wasm'
|
|
import {
|
|
getNodeFromPath,
|
|
getNodePathFromSourceRange,
|
|
isSingleCursorInPipe,
|
|
} from 'lang/queryAst'
|
|
import { TEST } from 'env'
|
|
import { exportFromEngine } from 'lib/exportFromEngine'
|
|
import { Models } from '@kittycad/lib/dist/types/src'
|
|
import toast from 'react-hot-toast'
|
|
import { EditorSelection } from '@uiw/react-codemirror'
|
|
import { CoreDumpManager } from 'lib/coredump'
|
|
import { useHotkeys } from 'react-hotkeys-hook'
|
|
import { useSearchParams } from 'react-router-dom'
|
|
import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls'
|
|
|
|
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,
|
|
settings: {
|
|
context: {
|
|
app: { theme, enableSSAO },
|
|
modeling: { defaultUnit, highlightEdges },
|
|
},
|
|
},
|
|
} = useSettingsAuthContext()
|
|
const token = auth?.context?.token
|
|
const streamRef = useRef<HTMLDivElement>(null)
|
|
|
|
let [searchParams] = useSearchParams()
|
|
const pool = searchParams.get('pool')
|
|
|
|
useSetupEngineManager(streamRef, token, {
|
|
pool: pool,
|
|
theme: theme.current,
|
|
highlightEdges: highlightEdges.current,
|
|
enableSSAO: enableSSAO.current,
|
|
})
|
|
const { htmlRef } = useStore((s) => ({
|
|
htmlRef: s.htmlRef,
|
|
}))
|
|
const coreDumpManager = new CoreDumpManager(
|
|
engineCommandManager,
|
|
htmlRef,
|
|
token
|
|
)
|
|
useHotkeys('meta + shift + .', () => coreDump(coreDumpManager, true))
|
|
|
|
// 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, modelingActor] = useMachine(
|
|
modelingMachine,
|
|
{
|
|
actions: {
|
|
'sketch exit execute': () => {
|
|
kclManager.executeCode(true)
|
|
},
|
|
'Set mouse state': assign({
|
|
mouseState: (_, event) => event.data,
|
|
}),
|
|
'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 (!editorManager.editorView) return {}
|
|
const dispatchSelection = (selection?: EditorSelection) => {
|
|
if (!selection) return // TODO less of hack for the below please
|
|
editorManager.lastSelectionEvent = Date.now()
|
|
setTimeout(() => {
|
|
if (editorManager.editorView) {
|
|
editorManager.editorView.dispatch({ selection })
|
|
}
|
|
})
|
|
}
|
|
let selections: Selections = {
|
|
codeBasedSelections: [],
|
|
otherSelections: [],
|
|
}
|
|
if (setSelections.selectionType === 'singleCodeCursor') {
|
|
if (!setSelections.selection && editorManager.isShiftDown) {
|
|
} else if (!setSelections.selection && !editorManager.isShiftDown) {
|
|
selections = {
|
|
codeBasedSelections: [],
|
|
otherSelections: [],
|
|
}
|
|
} else if (setSelections.selection && !editorManager.isShiftDown) {
|
|
selections = {
|
|
codeBasedSelections: [setSelections.selection],
|
|
otherSelections: [],
|
|
}
|
|
} else if (setSelections.selection && editorManager.isShiftDown) {
|
|
selections = {
|
|
codeBasedSelections: [
|
|
...selectionRanges.codeBasedSelections,
|
|
setSelections.selection,
|
|
],
|
|
otherSelections: selectionRanges.otherSelections,
|
|
}
|
|
}
|
|
|
|
const {
|
|
engineEvents,
|
|
codeMirrorSelection,
|
|
updateSceneObjectColors,
|
|
} = handleSelectionBatch({
|
|
selections,
|
|
})
|
|
codeMirrorSelection && dispatchSelection(codeMirrorSelection)
|
|
engineEvents &&
|
|
engineEvents.forEach((event) =>
|
|
engineCommandManager.sendSceneCommand(event)
|
|
)
|
|
updateSceneObjectColors()
|
|
|
|
return {
|
|
selectionRanges: selections,
|
|
}
|
|
}
|
|
|
|
if (setSelections.selectionType === 'mirrorCodeMirrorSelections') {
|
|
return {
|
|
selectionRanges: setSelections.selection,
|
|
}
|
|
}
|
|
|
|
if (setSelections.selectionType === 'otherSelection') {
|
|
if (editorManager.isShiftDown) {
|
|
selections = {
|
|
codeBasedSelections: selectionRanges.codeBasedSelections,
|
|
otherSelections: [setSelections.selection],
|
|
}
|
|
} else {
|
|
selections = {
|
|
codeBasedSelections: [],
|
|
otherSelections: [setSelections.selection],
|
|
}
|
|
}
|
|
const { engineEvents, updateSceneObjectColors } =
|
|
handleSelectionBatch({
|
|
selections,
|
|
})
|
|
engineEvents &&
|
|
engineEvents.forEach((event) =>
|
|
engineCommandManager.sendSceneCommand(event)
|
|
)
|
|
updateSceneObjectColors()
|
|
return {
|
|
selectionRanges: selections,
|
|
}
|
|
}
|
|
|
|
return {}
|
|
}),
|
|
'Engine export': (_, event) => {
|
|
if (event.type !== 'Export' || TEST) return
|
|
const format = {
|
|
...event.data,
|
|
} as Partial<Models['OutputFormat_type']>
|
|
|
|
// Set all the un-configurable defaults here.
|
|
if (format.type === 'gltf') {
|
|
format.presentation = 'pretty'
|
|
}
|
|
|
|
if (
|
|
format.type === 'obj' ||
|
|
format.type === 'ply' ||
|
|
format.type === 'step' ||
|
|
format.type === 'stl'
|
|
) {
|
|
// Set the default coords.
|
|
// In the future we can make this configurable.
|
|
// But for now, its probably best to keep it consistent with the
|
|
// UI.
|
|
format.coords = {
|
|
forward: {
|
|
axis: 'y',
|
|
direction: 'negative',
|
|
},
|
|
up: {
|
|
axis: 'z',
|
|
direction: 'positive',
|
|
},
|
|
}
|
|
}
|
|
|
|
if (
|
|
format.type === 'obj' ||
|
|
format.type === 'stl' ||
|
|
format.type === 'ply'
|
|
) {
|
|
format.units = defaultUnit.current
|
|
}
|
|
|
|
if (format.type === 'ply' || format.type === 'stl') {
|
|
format.selection = { type: 'default_scene' }
|
|
}
|
|
|
|
exportFromEngine({
|
|
format: format as Models['OutputFormat_type'],
|
|
}).catch((e) => toast.error('Error while exporting', e)) // TODO I think we need to throw the error from engineCommandManager
|
|
},
|
|
},
|
|
guards: {
|
|
'has valid extrude selection': ({ selectionRanges }) => {
|
|
// A user can begin extruding if they either have 1+ faces selected or nothing selected
|
|
// TODO: I believe this guard only allows for extruding a single face at a time
|
|
const isPipe = isSketchPipe(selectionRanges)
|
|
|
|
if (
|
|
selectionRanges.codeBasedSelections.length === 0 ||
|
|
isSelectionLastLine(selectionRanges, codeManager.code)
|
|
)
|
|
return true
|
|
if (!isPipe) return false
|
|
|
|
return canExtrudeSelection(selectionRanges)
|
|
},
|
|
'Sketch is empty': ({ sketchDetails }) =>
|
|
getNodeFromPath<VariableDeclaration>(
|
|
kclManager.ast,
|
|
sketchDetails?.sketchPathToNode || [],
|
|
'VariableDeclaration'
|
|
)?.node?.declarations[0]?.init.type !== 'PipeExpression',
|
|
'Selection is on face': ({ selectionRanges }, { data }) => {
|
|
if (data?.forceNewSketch) return false
|
|
if (!isSingleCursorInPipe(selectionRanges, kclManager.ast))
|
|
return false
|
|
return !!isCursorInSketchCommandRange(
|
|
engineCommandManager.artifactMap,
|
|
selectionRanges
|
|
)
|
|
},
|
|
'Has exportable geometry': () =>
|
|
kclManager.kclErrors.length === 0 && kclManager.ast.body.length > 0,
|
|
},
|
|
services: {
|
|
'AST-undo-startSketchOn': async ({ sketchDetails }) => {
|
|
if (!sketchDetails) return
|
|
const newAst: Program = JSON.parse(JSON.stringify(kclManager.ast))
|
|
const varDecIndex = sketchDetails.sketchPathToNode[1][0]
|
|
// remove body item at varDecIndex
|
|
newAst.body = newAst.body.filter((_, i) => i !== varDecIndex)
|
|
await kclManager.executeAstMock(newAst)
|
|
sceneInfra.setCallbacks({
|
|
onClick: () => {},
|
|
onDrag: () => {},
|
|
})
|
|
},
|
|
'animate-to-face': async (_, { data }) => {
|
|
if (data.type === 'extrudeFace') {
|
|
const { modifiedAst, pathToNode: pathToNewSketchNode } =
|
|
sketchOnExtrudedFace(
|
|
kclManager.ast,
|
|
data.extrudeSegmentPathToNode,
|
|
kclManager.programMemory,
|
|
data.cap
|
|
)
|
|
await kclManager.executeAstMock(modifiedAst)
|
|
|
|
await letEngineAnimateAndSyncCamAfter(
|
|
engineCommandManager,
|
|
data.faceId
|
|
)
|
|
|
|
return {
|
|
sketchPathToNode: pathToNewSketchNode,
|
|
zAxis: data.zAxis,
|
|
yAxis: data.yAxis,
|
|
origin: data.position,
|
|
}
|
|
}
|
|
const { modifiedAst, pathToNode } = startSketchOnDefault(
|
|
kclManager.ast,
|
|
data.plane
|
|
)
|
|
await kclManager.updateAst(modifiedAst, false)
|
|
sceneInfra.camControls.syncDirection = 'clientToEngine'
|
|
const quat = await getSketchQuaternion(pathToNode, data.zAxis)
|
|
await sceneInfra.camControls.tweenCameraToQuaternion(quat)
|
|
return {
|
|
sketchPathToNode: pathToNode,
|
|
zAxis: data.zAxis,
|
|
yAxis: data.yAxis,
|
|
origin: [0, 0, 0],
|
|
}
|
|
},
|
|
'animate-to-sketch': async ({ selectionRanges }) => {
|
|
const sourceRange = selectionRanges.codeBasedSelections[0].range
|
|
const sketchPathToNode = getNodePathFromSourceRange(
|
|
kclManager.ast,
|
|
sourceRange
|
|
)
|
|
const info = await getSketchOrientationDetails(sketchPathToNode || [])
|
|
await letEngineAnimateAndSyncCamAfter(
|
|
engineCommandManager,
|
|
info?.sketchDetails?.faceId || ''
|
|
)
|
|
return {
|
|
sketchPathToNode: sketchPathToNode || [],
|
|
zAxis: info.sketchDetails.zAxis || null,
|
|
yAxis: info.sketchDetails.yAxis || null,
|
|
origin: info.sketchDetails.origin.map(
|
|
(a) => a / sceneInfra._baseUnitMultiplier
|
|
) as [number, number, number],
|
|
}
|
|
},
|
|
'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 (angleBetweenInfo({
|
|
selectionRanges,
|
|
}).enabled
|
|
? applyConstraintAngleBetween({
|
|
selectionRanges,
|
|
})
|
|
: applyConstraintAngleLength({
|
|
selectionRanges,
|
|
angleOrLength: 'setAngle',
|
|
}))
|
|
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
|
|
),
|
|
}
|
|
},
|
|
'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
|
|
),
|
|
}
|
|
},
|
|
'Get ABS X info': async ({
|
|
selectionRanges,
|
|
}): Promise<SetSelections> => {
|
|
const { modifiedAst, pathToNodeMap } =
|
|
await applyConstraintAbsDistance({
|
|
constraint: 'xAbs',
|
|
selectionRanges,
|
|
})
|
|
await kclManager.updateAst(modifiedAst, true)
|
|
return {
|
|
selectionType: 'completeSelection',
|
|
selection: pathMapToSelections(
|
|
kclManager.ast,
|
|
selectionRanges,
|
|
pathToNodeMap
|
|
),
|
|
}
|
|
},
|
|
'Get ABS Y info': async ({
|
|
selectionRanges,
|
|
}): Promise<SetSelections> => {
|
|
const { modifiedAst, pathToNodeMap } =
|
|
await applyConstraintAbsDistance({
|
|
constraint: 'yAbs',
|
|
selectionRanges,
|
|
})
|
|
await kclManager.updateAst(modifiedAst, true)
|
|
return {
|
|
selectionType: 'completeSelection',
|
|
selection: pathMapToSelections(
|
|
kclManager.ast,
|
|
selectionRanges,
|
|
pathToNodeMap
|
|
),
|
|
}
|
|
},
|
|
},
|
|
devTools: true,
|
|
}
|
|
)
|
|
|
|
useEffect(() => {
|
|
kclManager.registerExecuteCallback(() => {
|
|
modelingSend({ type: 'Re-execute' })
|
|
})
|
|
}, [modelingSend])
|
|
|
|
// Give the state back to the editorManager.
|
|
useEffect(() => {
|
|
editorManager.modelingSend = modelingSend
|
|
}, [modelingSend])
|
|
|
|
useEffect(() => {
|
|
editorManager.modelingEvent = modelingState.event
|
|
}, [modelingState.event])
|
|
|
|
useEffect(() => {
|
|
editorManager.selectionRanges = modelingState.context.selectionRanges
|
|
}, [modelingState.context.selectionRanges])
|
|
|
|
useStateMachineCommands({
|
|
machineId: 'modeling',
|
|
state: modelingState,
|
|
send: modelingSend,
|
|
actor: modelingActor,
|
|
commandBarConfig: modelingMachineConfig,
|
|
allCommandsRequireNetwork: true,
|
|
onCancel: () => modelingSend({ type: 'Cancel' }),
|
|
})
|
|
|
|
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
|