* chore: saving off skeleton * fix: saving skeleton * chore: skeleton for loading projects from project directory path * chore: cleaning up useless state transition to be an on event direct to action state * fix: new structure for web vs desktop vs react machine provider code * chore: saving off skeleton * fix: skeleton logic for react? going to move it from a string to obj.string * fix: trying to prevent error element unmount on global react components. This is bricking JS state * fix: we are so back * chore: implemented navigating to specfic KCL file * chore: implementing renaming project * chore: deleting project * fix: auto fixes * fix: old debug/testing file oops * chore: generic create new file * chore: skeleton for web create file provide * chore: basic machine vitest... need to figure out how to get window.electron implemented in vitest? * chore: save off progress before deleting other project implementation, a few missing features still * chore: trying a different init skeleton? most likely will migrate * chore: first attempt of purging projects context provider * chore: enabling toast for some machine state * chore: enabling more toast success and error * chore: writing read write state to the system io based on the project path * fix: tsc fixes * fix: use file system watcher, navigate to project after creation via the requestProjectName * chore: open project command, hooks vs snapshot context helpers * chore: implemented open and create project for e2e testing. They are hard coded in poor spot for now. * fix: codespell fixes * chore: implementing more project commands * chore: PR improvements for root.tsx * chore: leaving comment about new Router.tsx layout * fix: removing debugging code * fix: rewriting component for readability * fix: improving web initialization * chore: implementing import file from url which is not actually that? * fix: clearing search params on import file from url * fix: fixed two e2e tests, forgot needsReview when making new command * fix: fixing some import from url business logic to pass e2e tests * chore: script for diffing circular deps +/- * fix: formatting * fix: massive fix for circular depsga! * fix: trying to fix some errors and auto fmt * fix: updating deps * fix: removing debugging code * fix: big clean up * fix: more deletion * fix: tsc cleanup * fix: TSC TSC TSC TSC! * fix: typo fix * fix: clear query params on web only, desktop not required * fix: removing unused code * fmt * Bring back `trap` removed in merge * Use explicit types instead of `any`s on arg configs * Add project commands directly to command palette * fix: deleting debugging code, from PR review * fix: this got added back(?) * fix: using referred type * fix: more PR clean up * fix: big block comment for xstate architecture decision * fix: more pr comment fixes * fix: merge conflict just added them back why dude * fix: more PR comments * fix: big ciruclar deps fix, commandBarActor in appActor --------- Co-authored-by: Frank Noirot <frankjohnson1993@gmail.com>
2030 lines
73 KiB
TypeScript
2030 lines
73 KiB
TypeScript
import { useMachine, useSelector } from '@xstate/react'
|
||
import React, {
|
||
createContext,
|
||
useContext,
|
||
useEffect,
|
||
useMemo,
|
||
useRef,
|
||
} from 'react'
|
||
import toast from 'react-hot-toast'
|
||
import { useHotkeys } from 'react-hotkeys-hook'
|
||
import { useLoaderData, useNavigate } from 'react-router-dom'
|
||
import type { Actor, ContextFrom, Prop, SnapshotFrom, StateFrom } from 'xstate'
|
||
import { assign, fromPromise } from 'xstate'
|
||
|
||
import type {
|
||
OutputFormat3d,
|
||
Point3d,
|
||
} from '@rust/kcl-lib/bindings/ModelingCmd'
|
||
import type { Node } from '@rust/kcl-lib/bindings/Node'
|
||
import type { Plane } from '@rust/kcl-lib/bindings/Plane'
|
||
|
||
import { useAppState } from '@src/AppState'
|
||
import { letEngineAnimateAndSyncCamAfter } from '@src/clientSideScene/CameraControls'
|
||
import {
|
||
SEGMENT_BODIES,
|
||
getParentGroup,
|
||
} from '@src/clientSideScene/sceneConstants'
|
||
import type { MachineManager } from '@src/components/MachineManagerProvider'
|
||
import { MachineManagerContext } from '@src/components/MachineManagerProvider'
|
||
import type { SidebarType } from '@src/components/ModelingSidebar/ModelingPanes'
|
||
import { applyConstraintIntersect } from '@src/components/Toolbar/Intersect'
|
||
import { applyConstraintAbsDistance } from '@src/components/Toolbar/SetAbsDistance'
|
||
import {
|
||
angleBetweenInfo,
|
||
applyConstraintAngleBetween,
|
||
} from '@src/components/Toolbar/SetAngleBetween'
|
||
import { applyConstraintHorzVertDistance } from '@src/components/Toolbar/SetHorzVertDistance'
|
||
import {
|
||
applyConstraintAngleLength,
|
||
applyConstraintLength,
|
||
} from '@src/components/Toolbar/setAngleLength'
|
||
import { useFileContext } from '@src/hooks/useFileContext'
|
||
import {
|
||
useMenuListener,
|
||
useSketchModeMenuEnableDisable,
|
||
} from '@src/hooks/useMenu'
|
||
import { useNetworkContext } from '@src/hooks/useNetworkContext'
|
||
import useStateMachineCommands from '@src/hooks/useStateMachineCommands'
|
||
import { useKclContext } from '@src/lang/KclProvider'
|
||
import { updateModelingState } from '@src/lang/modelingWorkflows'
|
||
import {
|
||
insertNamedConstant,
|
||
replaceValueAtNodePath,
|
||
sketchOnExtrudedFace,
|
||
sketchOnOffsetPlane,
|
||
splitPipedProfile,
|
||
startSketchOnDefault,
|
||
} from '@src/lang/modifyAst'
|
||
import {
|
||
artifactIsPlaneWithPaths,
|
||
doesSketchPipeNeedSplitting,
|
||
getNodeFromPath,
|
||
isCursorInFunctionDefinition,
|
||
traverse,
|
||
} from '@src/lang/queryAst'
|
||
import { getNodePathFromSourceRange } from '@src/lang/queryAstNodePathUtils'
|
||
import {
|
||
getFaceCodeRef,
|
||
getPathsFromArtifact,
|
||
getPlaneFromArtifact,
|
||
} from '@src/lang/std/artifactGraph'
|
||
import {
|
||
EngineConnectionEvents,
|
||
EngineConnectionStateType,
|
||
} from '@src/lang/std/engineConnection'
|
||
import {
|
||
crossProduct,
|
||
isCursorInSketchCommandRange,
|
||
updateSketchDetailsNodePaths,
|
||
} from '@src/lang/util'
|
||
import type {
|
||
KclValue,
|
||
PathToNode,
|
||
PipeExpression,
|
||
Program,
|
||
VariableDeclaration,
|
||
} from '@src/lang/wasm'
|
||
import { parse, recast, resultIsOk } from '@src/lang/wasm'
|
||
import type { ModelingCommandSchema } from '@src/lib/commandBarConfigs/modelingCommandConfig'
|
||
import { modelingMachineCommandConfig } from '@src/lib/commandBarConfigs/modelingCommandConfig'
|
||
import {
|
||
EXECUTION_TYPE_MOCK,
|
||
EXPORT_TOAST_MESSAGES,
|
||
MAKE_TOAST_MESSAGES,
|
||
} from '@src/lib/constants'
|
||
import { exportMake } from '@src/lib/exportMake'
|
||
import { exportSave } from '@src/lib/exportSave'
|
||
import { promptToEditFlow } from '@src/lib/promptToEdit'
|
||
import type { Selections } from '@src/lib/selections'
|
||
import { handleSelectionBatch, updateSelections } from '@src/lib/selections'
|
||
import {
|
||
codeManager,
|
||
editorManager,
|
||
engineCommandManager,
|
||
kclManager,
|
||
rustContext,
|
||
sceneEntitiesManager,
|
||
sceneInfra,
|
||
} from '@src/lib/singletons'
|
||
import { submitAndAwaitTextToKcl } from '@src/lib/textToCad'
|
||
import { err, reject, reportRejection, trap } from '@src/lib/trap'
|
||
import type { IndexLoaderData } from '@src/lib/types'
|
||
import { platform, uuidv4 } from '@src/lib/utils'
|
||
import { useSettings, useToken } from '@src/lib/singletons'
|
||
import { commandBarActor } from '@src/lib/singletons'
|
||
import { kclEditorActor } from '@src/machines/kclEditorMachine'
|
||
import {
|
||
getPersistedContext,
|
||
modelingMachine,
|
||
modelingMachineDefaultContext,
|
||
} from '@src/machines/modelingMachine'
|
||
import type { WebContentSendPayload } from '@src/menu/channels'
|
||
|
||
export const ModelingMachineContext = createContext(
|
||
{} as {
|
||
state: StateFrom<typeof modelingMachine>
|
||
context: ContextFrom<typeof modelingMachine>
|
||
send: Prop<Actor<typeof modelingMachine>, 'send'>
|
||
}
|
||
)
|
||
|
||
const commandBarIsClosedSelector = (
|
||
state: SnapshotFrom<typeof commandBarActor>
|
||
) => state.matches('Closed')
|
||
|
||
export const ModelingMachineProvider = ({
|
||
children,
|
||
}: {
|
||
children: React.ReactNode
|
||
}) => {
|
||
const {
|
||
app: { theme, allowOrbitInSketchMode },
|
||
modeling: { defaultUnit, cameraProjection, highlightEdges, cameraOrbit },
|
||
} = useSettings()
|
||
const navigate = useNavigate()
|
||
const { context, send: fileMachineSend } = useFileContext()
|
||
const { file } = useLoaderData() as IndexLoaderData
|
||
const token = useToken()
|
||
const streamRef = useRef<HTMLDivElement>(null)
|
||
const persistedContext = useMemo(() => getPersistedContext(), [])
|
||
|
||
const isCommandBarClosed = useSelector(
|
||
commandBarActor,
|
||
commandBarIsClosedSelector
|
||
)
|
||
|
||
// 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 machineManager = useContext(MachineManagerContext)
|
||
|
||
const [modelingState, modelingSend, modelingActor] = useMachine(
|
||
modelingMachine.provide({
|
||
actions: {
|
||
'disable copilot': () => {
|
||
editorManager.setCopilotEnabled(false)
|
||
},
|
||
'enable copilot': () => {
|
||
editorManager.setCopilotEnabled(true)
|
||
},
|
||
'sketch exit execute': ({ context: { store } }) => {
|
||
// TODO: Remove this async callback. For some reason eslint wouldn't
|
||
// let me disable @typescript-eslint/no-misused-promises for the line.
|
||
;(async () => {
|
||
// When cancelling the sketch mode we should disable sketch mode within the engine.
|
||
await engineCommandManager.sendSceneCommand({
|
||
type: 'modeling_cmd_req',
|
||
cmd_id: uuidv4(),
|
||
cmd: { type: 'sketch_mode_disable' },
|
||
})
|
||
|
||
sceneInfra.camControls.syncDirection = 'clientToEngine'
|
||
|
||
if (cameraProjection.current === 'perspective') {
|
||
await sceneInfra.camControls.snapToPerspectiveBeforeHandingBackControlToEngine()
|
||
}
|
||
|
||
sceneInfra.camControls.syncDirection = 'engineToClient'
|
||
|
||
store.videoElement?.pause()
|
||
|
||
return kclManager
|
||
.executeCode()
|
||
.then(() => {
|
||
if (engineCommandManager.engineConnection?.idleMode) return
|
||
|
||
store.videoElement?.play().catch((e) => {
|
||
console.warn('Video playing was prevented', e)
|
||
})
|
||
})
|
||
.catch(reportRejection)
|
||
})().catch(reportRejection)
|
||
},
|
||
'Set mouse state': assign(({ context, event }) => {
|
||
if (event.type !== 'Set mouse state') return {}
|
||
const nextSegmentHoverMap = () => {
|
||
if (event.data.type === 'isHovering') {
|
||
const parent = getParentGroup(event.data.on, SEGMENT_BODIES)
|
||
const pathToNode = parent?.userData?.pathToNode
|
||
const pathToNodeString = JSON.stringify(pathToNode)
|
||
if (!parent || !pathToNode) return context.segmentHoverMap
|
||
if (context.segmentHoverMap[pathToNodeString] !== undefined)
|
||
clearTimeout(
|
||
context.segmentHoverMap[JSON.stringify(pathToNode)]
|
||
)
|
||
return {
|
||
...context.segmentHoverMap,
|
||
[pathToNodeString]: 0,
|
||
}
|
||
} else if (
|
||
event.data.type === 'idle' &&
|
||
context.mouseState.type === 'isHovering'
|
||
) {
|
||
const mouseOnParent = getParentGroup(
|
||
context.mouseState.on,
|
||
SEGMENT_BODIES
|
||
)
|
||
if (!mouseOnParent || !mouseOnParent?.userData?.pathToNode)
|
||
return context.segmentHoverMap
|
||
const pathToNodeString = JSON.stringify(
|
||
mouseOnParent?.userData?.pathToNode
|
||
)
|
||
const timeoutId = setTimeout(() => {
|
||
sceneInfra.modelingSend({
|
||
type: 'Set mouse state',
|
||
data: {
|
||
type: 'timeoutEnd',
|
||
pathToNodeString,
|
||
},
|
||
})
|
||
// overlay timeout is 1s
|
||
}, 1000) as unknown as number
|
||
return {
|
||
...context.segmentHoverMap,
|
||
[pathToNodeString]: timeoutId,
|
||
}
|
||
} else if (event.data.type === 'timeoutEnd') {
|
||
const copy = { ...context.segmentHoverMap }
|
||
delete copy[event.data.pathToNodeString]
|
||
return copy
|
||
}
|
||
return {}
|
||
}
|
||
return {
|
||
mouseState: event.data,
|
||
segmentHoverMap: nextSegmentHoverMap(),
|
||
}
|
||
}),
|
||
'Set Segment Overlays': assign({
|
||
segmentOverlays: ({ context: { segmentOverlays }, event }) => {
|
||
if (event.type !== 'Set Segment Overlays') return {}
|
||
if (event.data.type === 'add-many')
|
||
return {
|
||
...segmentOverlays,
|
||
...event.data.overlays,
|
||
}
|
||
if (event.data.type === 'set-one')
|
||
return {
|
||
...segmentOverlays,
|
||
[event.data.pathToNodeString]: event.data.seg,
|
||
}
|
||
if (event.data.type === 'delete-one') {
|
||
const copy = { ...segmentOverlays }
|
||
delete copy[event.data.pathToNodeString]
|
||
return copy
|
||
}
|
||
// data.type === 'clear'
|
||
return {}
|
||
},
|
||
}),
|
||
'Center camera on selection': () => {
|
||
engineCommandManager
|
||
.sendSceneCommand({
|
||
type: 'modeling_cmd_req',
|
||
cmd_id: uuidv4(),
|
||
cmd: {
|
||
type: 'default_camera_center_to_selection',
|
||
camera_movement: 'vantage',
|
||
},
|
||
})
|
||
.catch(reportRejection)
|
||
},
|
||
'Set sketchDetails': assign(({ context: { sketchDetails }, event }) => {
|
||
if (event.type !== 'Delete segment') return {}
|
||
if (!sketchDetails) return {}
|
||
return {
|
||
sketchDetails: {
|
||
...sketchDetails,
|
||
sketchEntryNodePath: event.data,
|
||
},
|
||
}
|
||
}),
|
||
'Set selection': assign(
|
||
({ context: { selectionRanges, sketchDetails }, event }) => {
|
||
// this was needed for ts after adding 'Set selection' action to on done modal events
|
||
const setSelections =
|
||
('data' in event &&
|
||
event.data &&
|
||
'selectionType' in event.data &&
|
||
event.data) ||
|
||
('output' in event &&
|
||
event.output &&
|
||
'selectionType' in event.output &&
|
||
event.output) ||
|
||
null
|
||
if (!setSelections) return {}
|
||
|
||
let selections: Selections = {
|
||
graphSelections: [],
|
||
otherSelections: [],
|
||
}
|
||
if (setSelections.selectionType === 'singleCodeCursor') {
|
||
if (!setSelections.selection && editorManager.isShiftDown) {
|
||
// if the user is holding shift, but they didn't select anything
|
||
// don't nuke their other selections (frustrating to have one bad click ruin your
|
||
// whole selection)
|
||
selections = {
|
||
graphSelections: selectionRanges.graphSelections,
|
||
otherSelections: selectionRanges.otherSelections,
|
||
}
|
||
} else if (
|
||
!setSelections.selection &&
|
||
!editorManager.isShiftDown
|
||
) {
|
||
selections = {
|
||
graphSelections: [],
|
||
otherSelections: [],
|
||
}
|
||
} else if (
|
||
setSelections.selection &&
|
||
!editorManager.isShiftDown
|
||
) {
|
||
selections = {
|
||
graphSelections: [setSelections.selection],
|
||
otherSelections: [],
|
||
}
|
||
} else if (setSelections.selection && editorManager.isShiftDown) {
|
||
// selecting and deselecting multiple objects
|
||
|
||
/**
|
||
* There are two scenarios:
|
||
* 1. General case:
|
||
* When selecting and deselecting edges,
|
||
* faces or segment (during sketch edit)
|
||
* we use its artifact ID to identify the selection
|
||
* 2. Initial sketch setup:
|
||
* The artifact is not yet created
|
||
* so we use the codeRef.range
|
||
*/
|
||
|
||
let updatedSelections: typeof selectionRanges.graphSelections
|
||
|
||
// 1. General case: Artifact exists, use its ID
|
||
if (setSelections.selection.artifact?.id) {
|
||
// check if already selected
|
||
const alreadySelected = selectionRanges.graphSelections.some(
|
||
(selection) =>
|
||
selection.artifact?.id ===
|
||
setSelections.selection?.artifact?.id
|
||
)
|
||
if (
|
||
alreadySelected &&
|
||
setSelections.selection?.artifact?.id
|
||
) {
|
||
// remove it
|
||
updatedSelections = selectionRanges.graphSelections.filter(
|
||
(selection) =>
|
||
selection.artifact?.id !==
|
||
setSelections.selection?.artifact?.id
|
||
)
|
||
} else {
|
||
// add it
|
||
updatedSelections = [
|
||
...selectionRanges.graphSelections,
|
||
setSelections.selection,
|
||
]
|
||
}
|
||
} else {
|
||
// 2. Initial sketch setup: Artifact not yet created – use codeRef.range
|
||
const selectionRange = JSON.stringify(
|
||
setSelections.selection?.codeRef?.range
|
||
)
|
||
|
||
// check if already selected
|
||
const alreadySelected = selectionRanges.graphSelections.some(
|
||
(selection) => {
|
||
const existingRange = JSON.stringify(
|
||
selection.codeRef?.range
|
||
)
|
||
return existingRange === selectionRange
|
||
}
|
||
)
|
||
|
||
if (
|
||
alreadySelected &&
|
||
setSelections.selection?.codeRef?.range
|
||
) {
|
||
// remove it
|
||
updatedSelections = selectionRanges.graphSelections.filter(
|
||
(selection) =>
|
||
JSON.stringify(selection.codeRef?.range) !==
|
||
selectionRange
|
||
)
|
||
} else {
|
||
// add it
|
||
updatedSelections = [
|
||
...selectionRanges.graphSelections,
|
||
setSelections.selection,
|
||
]
|
||
}
|
||
}
|
||
|
||
selections = {
|
||
graphSelections: updatedSelections,
|
||
otherSelections: selectionRanges.otherSelections,
|
||
}
|
||
}
|
||
|
||
const {
|
||
engineEvents,
|
||
codeMirrorSelection,
|
||
updateSceneObjectColors,
|
||
} = handleSelectionBatch({
|
||
selections,
|
||
})
|
||
if (codeMirrorSelection) {
|
||
kclEditorActor.send({
|
||
type: 'setLastSelectionEvent',
|
||
data: {
|
||
codeMirrorSelection,
|
||
scrollIntoView: setSelections.scrollIntoView ?? false,
|
||
},
|
||
})
|
||
}
|
||
|
||
// If there are engine commands that need sent off, send them
|
||
// TODO: This should be handled outside of an action as its own
|
||
// actor, so that the system state is more controlled.
|
||
engineEvents &&
|
||
engineEvents.forEach((event) => {
|
||
engineCommandManager
|
||
.sendSceneCommand(event)
|
||
.catch(reportRejection)
|
||
})
|
||
updateSceneObjectColors()
|
||
|
||
return {
|
||
selectionRanges: selections,
|
||
}
|
||
}
|
||
|
||
if (setSelections.selectionType === 'mirrorCodeMirrorSelections') {
|
||
return {
|
||
selectionRanges: setSelections.selection,
|
||
}
|
||
}
|
||
|
||
if (
|
||
setSelections.selectionType === 'axisSelection' ||
|
||
setSelections.selectionType === 'defaultPlaneSelection'
|
||
) {
|
||
if (editorManager.isShiftDown) {
|
||
selections = {
|
||
graphSelections: selectionRanges.graphSelections,
|
||
otherSelections: [setSelections.selection],
|
||
}
|
||
} else {
|
||
selections = {
|
||
graphSelections: [],
|
||
otherSelections: [setSelections.selection],
|
||
}
|
||
}
|
||
return {
|
||
selectionRanges: selections,
|
||
}
|
||
}
|
||
|
||
if (setSelections.selectionType === 'completeSelection') {
|
||
const codeMirrorSelection = editorManager.createEditorSelection(
|
||
setSelections.selection
|
||
)
|
||
kclEditorActor.send({
|
||
type: 'setLastSelectionEvent',
|
||
data: {
|
||
codeMirrorSelection,
|
||
scrollIntoView: false,
|
||
},
|
||
})
|
||
if (!sketchDetails)
|
||
return {
|
||
selectionRanges: setSelections.selection,
|
||
}
|
||
return {
|
||
selectionRanges: setSelections.selection,
|
||
sketchDetails: {
|
||
...sketchDetails,
|
||
sketchEntryNodePath:
|
||
setSelections.updatedSketchEntryNodePath ||
|
||
sketchDetails?.sketchEntryNodePath ||
|
||
[],
|
||
sketchNodePaths:
|
||
setSelections.updatedSketchNodePaths ||
|
||
sketchDetails?.sketchNodePaths ||
|
||
[],
|
||
planeNodePath:
|
||
setSelections.updatedPlaneNodePath ||
|
||
sketchDetails?.planeNodePath ||
|
||
[],
|
||
},
|
||
}
|
||
}
|
||
|
||
return {}
|
||
}
|
||
),
|
||
'Submit to Text-to-CAD API': ({ event }) => {
|
||
if (event.type !== 'Text-to-CAD') return
|
||
const trimmedPrompt = event.data.prompt.trim()
|
||
if (!trimmedPrompt) return
|
||
|
||
submitAndAwaitTextToKcl({
|
||
trimmedPrompt,
|
||
fileMachineSend,
|
||
navigate,
|
||
context,
|
||
token,
|
||
settings: {
|
||
theme: theme.current,
|
||
highlightEdges: highlightEdges.current,
|
||
},
|
||
}).catch(reportRejection)
|
||
},
|
||
},
|
||
guards: {
|
||
'has valid selection for deletion': ({
|
||
context: { selectionRanges },
|
||
}) => {
|
||
if (!isCommandBarClosed) return false
|
||
if (selectionRanges.graphSelections.length <= 0) return false
|
||
return true
|
||
},
|
||
'is-error-free': () => {
|
||
return kclManager.errors.length === 0 && !kclManager.hasErrors()
|
||
},
|
||
'Selection is on face': ({ context: { selectionRanges }, event }) => {
|
||
if (event.type !== 'Enter sketch') return false
|
||
if (event.data?.forceNewSketch) return false
|
||
if (artifactIsPlaneWithPaths(selectionRanges)) {
|
||
return true
|
||
} else if (selectionRanges.graphSelections[0]?.artifact) {
|
||
// See if the selection is "close enough" to be coerced to the plane later
|
||
const maybePlane = getPlaneFromArtifact(
|
||
selectionRanges.graphSelections[0].artifact,
|
||
kclManager.artifactGraph
|
||
)
|
||
return !err(maybePlane)
|
||
}
|
||
if (
|
||
isCursorInFunctionDefinition(
|
||
kclManager.ast,
|
||
selectionRanges.graphSelections[0]
|
||
)
|
||
) {
|
||
return false
|
||
}
|
||
return !!isCursorInSketchCommandRange(
|
||
kclManager.artifactGraph,
|
||
selectionRanges
|
||
)
|
||
},
|
||
'Has exportable geometry': () => {
|
||
if (!kclManager.hasErrors() && kclManager.ast.body.length > 0)
|
||
return true
|
||
else {
|
||
let errorMessage = 'Unable to Export '
|
||
if (kclManager.hasErrors()) errorMessage += 'due to KCL Errors'
|
||
else if (kclManager.ast.body.length === 0)
|
||
errorMessage += 'due to Empty Scene'
|
||
console.error(errorMessage)
|
||
toast.error(errorMessage)
|
||
return false
|
||
}
|
||
},
|
||
},
|
||
actors: {
|
||
exportFromEngine: fromPromise(
|
||
async ({ input }: { input?: ModelingCommandSchema['Export'] }) => {
|
||
if (!input) {
|
||
return new Error('No input provided')
|
||
}
|
||
|
||
let fileName = file?.name?.replace('.kcl', `.${input.type}`) || ''
|
||
// Ensure the file has an extension.
|
||
if (!fileName.includes('.')) {
|
||
fileName += `.${input.type}`
|
||
}
|
||
|
||
const format = {
|
||
...input,
|
||
} as Partial<OutputFormat3d>
|
||
|
||
// 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' }
|
||
}
|
||
|
||
const toastId = toast.loading(EXPORT_TOAST_MESSAGES.START)
|
||
const files = await rustContext.export(
|
||
format,
|
||
{
|
||
settings: { modeling: { base_unit: defaultUnit.current } },
|
||
},
|
||
toastId
|
||
)
|
||
|
||
if (files === undefined) {
|
||
// We already sent the toast message in the export function.
|
||
return
|
||
}
|
||
|
||
await exportSave({ files, toastId, fileName })
|
||
}
|
||
),
|
||
makeFromEngine: fromPromise(
|
||
async ({
|
||
input,
|
||
}: {
|
||
input?: {
|
||
machineManager: MachineManager
|
||
} & ModelingCommandSchema['Make']
|
||
}) => {
|
||
if (input === undefined) {
|
||
return new Error('No input provided')
|
||
}
|
||
|
||
const name = file?.name || ''
|
||
|
||
// Set the current machine.
|
||
// Due to our use of singeton pattern, we need to do this to reliably
|
||
// update this object across React and non-React boundary.
|
||
// We need to do this eagerly because of the exportToEngine call below.
|
||
if (engineCommandManager.machineManager === null) {
|
||
console.warn(
|
||
"engineCommandManager.machineManager is null. It shouldn't be at this point. Aborting operation."
|
||
)
|
||
return new Error('Machine manager is not set')
|
||
} else {
|
||
engineCommandManager.machineManager.currentMachine = input.machine
|
||
}
|
||
|
||
// Update the rest of the UI that needs to know the current machine
|
||
input.machineManager.setCurrentMachine(input.machine)
|
||
|
||
const format: OutputFormat3d = {
|
||
type: 'stl',
|
||
coords: {
|
||
forward: {
|
||
axis: 'y',
|
||
direction: 'negative',
|
||
},
|
||
up: {
|
||
axis: 'z',
|
||
direction: 'positive',
|
||
},
|
||
},
|
||
storage: 'ascii',
|
||
// Convert all units to mm since that is what the slicer expects.
|
||
units: 'mm',
|
||
selection: { type: 'default_scene' },
|
||
}
|
||
|
||
const toastId = toast.loading(MAKE_TOAST_MESSAGES.START)
|
||
const files = await rustContext.export(
|
||
format,
|
||
{
|
||
settings: { modeling: { base_unit: 'mm' } },
|
||
},
|
||
toastId
|
||
)
|
||
|
||
if (files === undefined) {
|
||
// We already sent the toast message in the export function.
|
||
return
|
||
}
|
||
|
||
await exportMake({
|
||
files,
|
||
toastId,
|
||
name,
|
||
machineManager: engineCommandManager.machineManager,
|
||
})
|
||
}
|
||
),
|
||
'AST-undo-startSketchOn': fromPromise(
|
||
async ({ input: { sketchDetails } }) => {
|
||
if (!sketchDetails) return
|
||
if (kclManager.ast.body.length) {
|
||
const newAst = structuredClone(kclManager.ast)
|
||
const varDecIndex = sketchDetails.planeNodePath[1][0]
|
||
|
||
const varDec = getNodeFromPath<VariableDeclaration>(
|
||
newAst,
|
||
sketchDetails.planeNodePath,
|
||
'VariableDeclaration'
|
||
)
|
||
if (err(varDec)) return reject(new Error('No varDec'))
|
||
const variableName = varDec.node.declaration.id.name
|
||
let isIdentifierUsed = false
|
||
traverse(newAst, {
|
||
enter: (node) => {
|
||
if (node.type === 'Name' && node.name.name === variableName) {
|
||
isIdentifierUsed = true
|
||
}
|
||
},
|
||
})
|
||
if (isIdentifierUsed) return
|
||
|
||
// remove body item at varDecIndex
|
||
newAst.body = newAst.body.filter((_, i) => i !== varDecIndex)
|
||
const didReParse = await kclManager.executeAstMock(newAst)
|
||
if (err(didReParse)) return reject(didReParse)
|
||
await codeManager.updateEditorWithAstAndWriteToFile(newAst)
|
||
}
|
||
sceneInfra.setCallbacks({
|
||
onClick: () => {},
|
||
onDrag: () => {},
|
||
})
|
||
return undefined
|
||
}
|
||
),
|
||
'animate-to-face': fromPromise(async ({ input }) => {
|
||
if (!input) return null
|
||
if (input.type === 'extrudeFace' || input.type === 'offsetPlane') {
|
||
const originalCode = codeManager.code
|
||
const sketched =
|
||
input.type === 'extrudeFace'
|
||
? sketchOnExtrudedFace(
|
||
kclManager.ast,
|
||
input.sketchPathToNode,
|
||
input.extrudePathToNode,
|
||
input.faceInfo
|
||
)
|
||
: sketchOnOffsetPlane(kclManager.ast, input.pathToNode)
|
||
if (err(sketched)) {
|
||
const sketchedError = new Error(
|
||
'Incompatible face, please try another'
|
||
)
|
||
trap(sketchedError)
|
||
return Promise.reject(sketchedError)
|
||
}
|
||
const { modifiedAst, pathToNode: pathToNewSketchNode } = sketched
|
||
|
||
const didReParse = await kclManager.executeAstMock(modifiedAst)
|
||
if (err(didReParse)) {
|
||
// there was a problem, restore the original code
|
||
codeManager.code = originalCode
|
||
await kclManager.executeCode()
|
||
return reject(didReParse)
|
||
}
|
||
|
||
const id =
|
||
input.type === 'extrudeFace' ? input.faceId : input.planeId
|
||
await letEngineAnimateAndSyncCamAfter(engineCommandManager, id)
|
||
sceneInfra.camControls.syncDirection = 'clientToEngine'
|
||
return {
|
||
sketchEntryNodePath: [],
|
||
planeNodePath: pathToNewSketchNode,
|
||
sketchNodePaths: [],
|
||
zAxis: input.zAxis,
|
||
yAxis: input.yAxis,
|
||
origin: input.position,
|
||
}
|
||
}
|
||
const { modifiedAst, pathToNode } = startSketchOnDefault(
|
||
kclManager.ast,
|
||
input.plane
|
||
)
|
||
await kclManager.updateAst(modifiedAst, false)
|
||
sceneInfra.camControls.enableRotate =
|
||
sceneInfra.camControls._setting_allowOrbitInSketchMode
|
||
sceneInfra.camControls.syncDirection = 'clientToEngine'
|
||
|
||
await letEngineAnimateAndSyncCamAfter(
|
||
engineCommandManager,
|
||
input.planeId
|
||
)
|
||
|
||
return {
|
||
sketchEntryNodePath: [],
|
||
planeNodePath: pathToNode,
|
||
sketchNodePaths: [],
|
||
zAxis: input.zAxis,
|
||
yAxis: input.yAxis,
|
||
origin: [0, 0, 0],
|
||
animateTargetId: input.planeId,
|
||
}
|
||
}),
|
||
'animate-to-sketch': fromPromise(
|
||
async ({ input: { selectionRanges } }) => {
|
||
const artifact = selectionRanges.graphSelections[0].artifact
|
||
const plane = getPlaneFromArtifact(
|
||
artifact,
|
||
kclManager.artifactGraph
|
||
)
|
||
if (err(plane)) return Promise.reject(plane)
|
||
// if the user selected a segment, make sure we enter the right sketch as there can be multiple on a plane
|
||
// but still works if the user selected a plane/face by defaulting to the first path
|
||
const mainPath =
|
||
artifact?.type === 'segment' || artifact?.type === 'solid2d'
|
||
? artifact?.pathId
|
||
: plane?.pathIds[0]
|
||
let sketch: KclValue | null = null
|
||
let planeVar: Plane | null = null
|
||
|
||
for (const variable of Object.values(
|
||
kclManager.execState.variables
|
||
)) {
|
||
// find programMemory that matches path artifact
|
||
if (
|
||
variable?.type === 'Sketch' &&
|
||
variable.value.artifactId === mainPath
|
||
) {
|
||
sketch = variable
|
||
break
|
||
}
|
||
if (
|
||
// if the variable is an sweep, check if the underlying sketch matches the artifact
|
||
variable?.type === 'Solid' &&
|
||
variable.value.sketch.on.type === 'plane' &&
|
||
variable.value.sketch.artifactId === mainPath
|
||
) {
|
||
sketch = {
|
||
type: 'Sketch',
|
||
value: variable.value.sketch,
|
||
}
|
||
break
|
||
}
|
||
if (
|
||
variable?.type === 'Plane' &&
|
||
plane.id === variable.value.id
|
||
) {
|
||
planeVar = variable.value
|
||
}
|
||
}
|
||
|
||
if (!sketch || sketch.type !== 'Sketch') {
|
||
if (artifact?.type !== 'plane')
|
||
return Promise.reject(new Error('No sketch'))
|
||
const planeCodeRef = getFaceCodeRef(artifact)
|
||
if (planeVar && planeCodeRef) {
|
||
const toTuple = (point: Point3d): [number, number, number] => [
|
||
point.x,
|
||
point.y,
|
||
point.z,
|
||
]
|
||
const planPath = getNodePathFromSourceRange(
|
||
kclManager.ast,
|
||
planeCodeRef.range
|
||
)
|
||
await letEngineAnimateAndSyncCamAfter(
|
||
engineCommandManager,
|
||
artifact.id
|
||
)
|
||
const normal = crossProduct(planeVar.xAxis, planeVar.yAxis)
|
||
return {
|
||
sketchEntryNodePath: [],
|
||
planeNodePath: planPath,
|
||
sketchNodePaths: [],
|
||
zAxis: toTuple(normal),
|
||
yAxis: toTuple(planeVar.yAxis),
|
||
origin: toTuple(planeVar.origin),
|
||
}
|
||
}
|
||
return Promise.reject(new Error('No sketch'))
|
||
}
|
||
const info = await sceneEntitiesManager.getSketchOrientationDetails(
|
||
sketch.value
|
||
)
|
||
await letEngineAnimateAndSyncCamAfter(
|
||
engineCommandManager,
|
||
info?.sketchDetails?.faceId || ''
|
||
)
|
||
|
||
const sketchArtifact = kclManager.artifactGraph.get(mainPath)
|
||
if (sketchArtifact?.type !== 'path')
|
||
return Promise.reject(new Error('No sketch artifact'))
|
||
const sketchPaths = getPathsFromArtifact({
|
||
artifact: kclManager.artifactGraph.get(plane.id),
|
||
sketchPathToNode: sketchArtifact?.codeRef?.pathToNode,
|
||
artifactGraph: kclManager.artifactGraph,
|
||
ast: kclManager.ast,
|
||
})
|
||
if (err(sketchPaths)) return Promise.reject(sketchPaths)
|
||
let codeRef = getFaceCodeRef(plane)
|
||
if (!codeRef) return Promise.reject(new Error('No plane codeRef'))
|
||
// codeRef.pathToNode is not always populated correctly
|
||
const planeNodePath = getNodePathFromSourceRange(
|
||
kclManager.ast,
|
||
codeRef.range
|
||
)
|
||
return {
|
||
sketchEntryNodePath: sketchArtifact.codeRef.pathToNode || [],
|
||
sketchNodePaths: sketchPaths,
|
||
planeNodePath,
|
||
zAxis: info.sketchDetails.zAxis || null,
|
||
yAxis: info.sketchDetails.yAxis || null,
|
||
origin: info.sketchDetails.origin.map(
|
||
(a) => a / sceneInfra._baseUnitMultiplier
|
||
) as [number, number, number],
|
||
animateTargetId: info?.sketchDetails?.faceId || '',
|
||
}
|
||
}
|
||
),
|
||
|
||
'Get horizontal info': fromPromise(
|
||
async ({ input: { selectionRanges, sketchDetails } }) => {
|
||
const { modifiedAst, pathToNodeMap, exprInsertIndex } =
|
||
await applyConstraintHorzVertDistance({
|
||
constraint: 'setHorzDistance',
|
||
selectionRanges,
|
||
})
|
||
const pResult = parse(recast(modifiedAst))
|
||
if (trap(pResult) || !resultIsOk(pResult))
|
||
return Promise.reject(new Error('Unexpected compilation error'))
|
||
const _modifiedAst = pResult.program
|
||
|
||
if (!sketchDetails)
|
||
return Promise.reject(new Error('No sketch details'))
|
||
|
||
const {
|
||
updatedSketchEntryNodePath,
|
||
updatedSketchNodePaths,
|
||
updatedPlaneNodePath,
|
||
} = updateSketchDetailsNodePaths({
|
||
sketchEntryNodePath: sketchDetails.sketchEntryNodePath,
|
||
sketchNodePaths: sketchDetails.sketchNodePaths,
|
||
planeNodePath: sketchDetails.planeNodePath,
|
||
exprInsertIndex,
|
||
})
|
||
|
||
const updatedAst =
|
||
await sceneEntitiesManager.updateAstAndRejigSketch(
|
||
updatedSketchEntryNodePath,
|
||
updatedSketchNodePaths,
|
||
updatedPlaneNodePath,
|
||
_modifiedAst,
|
||
sketchDetails.zAxis,
|
||
sketchDetails.yAxis,
|
||
sketchDetails.origin
|
||
)
|
||
if (err(updatedAst)) return Promise.reject(updatedAst)
|
||
|
||
await codeManager.updateEditorWithAstAndWriteToFile(
|
||
updatedAst.newAst
|
||
)
|
||
|
||
const selection = updateSelections(
|
||
pathToNodeMap,
|
||
selectionRanges,
|
||
updatedAst.newAst
|
||
)
|
||
if (err(selection)) return Promise.reject(selection)
|
||
return {
|
||
selectionType: 'completeSelection',
|
||
selection,
|
||
updatedSketchEntryNodePath,
|
||
updatedSketchNodePaths,
|
||
updatedPlaneNodePath,
|
||
}
|
||
}
|
||
),
|
||
'Get vertical info': fromPromise(
|
||
async ({ input: { selectionRanges, sketchDetails } }) => {
|
||
const { modifiedAst, pathToNodeMap, exprInsertIndex } =
|
||
await applyConstraintHorzVertDistance({
|
||
constraint: 'setVertDistance',
|
||
selectionRanges,
|
||
})
|
||
const pResult = parse(recast(modifiedAst))
|
||
if (trap(pResult) || !resultIsOk(pResult))
|
||
return Promise.reject(new Error('Unexpected compilation error'))
|
||
const _modifiedAst = pResult.program
|
||
if (!sketchDetails)
|
||
return Promise.reject(new Error('No sketch details'))
|
||
|
||
const {
|
||
updatedSketchEntryNodePath,
|
||
updatedSketchNodePaths,
|
||
updatedPlaneNodePath,
|
||
} = updateSketchDetailsNodePaths({
|
||
sketchEntryNodePath: sketchDetails.sketchEntryNodePath,
|
||
sketchNodePaths: sketchDetails.sketchNodePaths,
|
||
planeNodePath: sketchDetails.planeNodePath,
|
||
exprInsertIndex,
|
||
})
|
||
|
||
const updatedAst =
|
||
await sceneEntitiesManager.updateAstAndRejigSketch(
|
||
updatedSketchEntryNodePath,
|
||
updatedSketchNodePaths,
|
||
updatedPlaneNodePath,
|
||
_modifiedAst,
|
||
sketchDetails.zAxis,
|
||
sketchDetails.yAxis,
|
||
sketchDetails.origin
|
||
)
|
||
if (err(updatedAst)) return Promise.reject(updatedAst)
|
||
|
||
await codeManager.updateEditorWithAstAndWriteToFile(
|
||
updatedAst.newAst
|
||
)
|
||
|
||
const selection = updateSelections(
|
||
pathToNodeMap,
|
||
selectionRanges,
|
||
updatedAst.newAst
|
||
)
|
||
if (err(selection)) return Promise.reject(selection)
|
||
return {
|
||
selectionType: 'completeSelection',
|
||
selection,
|
||
updatedSketchEntryNodePath,
|
||
updatedSketchNodePaths,
|
||
updatedPlaneNodePath,
|
||
}
|
||
}
|
||
),
|
||
'Get angle info': fromPromise(
|
||
async ({ input: { selectionRanges, sketchDetails } }) => {
|
||
const info = angleBetweenInfo({
|
||
selectionRanges,
|
||
})
|
||
if (err(info)) return Promise.reject(info)
|
||
const { modifiedAst, pathToNodeMap, exprInsertIndex } =
|
||
await (info.enabled
|
||
? applyConstraintAngleBetween({
|
||
selectionRanges,
|
||
})
|
||
: applyConstraintAngleLength({
|
||
selectionRanges,
|
||
angleOrLength: 'setAngle',
|
||
}))
|
||
const pResult = parse(recast(modifiedAst))
|
||
if (trap(pResult) || !resultIsOk(pResult))
|
||
return Promise.reject(new Error('Unexpected compilation error'))
|
||
const _modifiedAst = pResult.program
|
||
if (err(_modifiedAst)) return Promise.reject(_modifiedAst)
|
||
|
||
if (!sketchDetails)
|
||
return Promise.reject(new Error('No sketch details'))
|
||
|
||
const {
|
||
updatedSketchEntryNodePath,
|
||
updatedSketchNodePaths,
|
||
updatedPlaneNodePath,
|
||
} = updateSketchDetailsNodePaths({
|
||
sketchEntryNodePath: sketchDetails.sketchEntryNodePath,
|
||
sketchNodePaths: sketchDetails.sketchNodePaths,
|
||
planeNodePath: sketchDetails.planeNodePath,
|
||
exprInsertIndex,
|
||
})
|
||
|
||
const updatedAst =
|
||
await sceneEntitiesManager.updateAstAndRejigSketch(
|
||
updatedSketchEntryNodePath,
|
||
updatedSketchNodePaths,
|
||
updatedPlaneNodePath,
|
||
_modifiedAst,
|
||
sketchDetails.zAxis,
|
||
sketchDetails.yAxis,
|
||
sketchDetails.origin
|
||
)
|
||
if (err(updatedAst)) return Promise.reject(updatedAst)
|
||
|
||
await codeManager.updateEditorWithAstAndWriteToFile(
|
||
updatedAst.newAst
|
||
)
|
||
|
||
const selection = updateSelections(
|
||
pathToNodeMap,
|
||
selectionRanges,
|
||
updatedAst.newAst
|
||
)
|
||
if (err(selection)) return Promise.reject(selection)
|
||
return {
|
||
selectionType: 'completeSelection',
|
||
selection,
|
||
updatedSketchEntryNodePath,
|
||
updatedSketchNodePaths,
|
||
updatedPlaneNodePath,
|
||
}
|
||
}
|
||
),
|
||
astConstrainLength: fromPromise(
|
||
async ({
|
||
input: { selectionRanges, sketchDetails, lengthValue },
|
||
}) => {
|
||
if (!lengthValue)
|
||
return Promise.reject(new Error('No length value'))
|
||
const constraintResult = await applyConstraintLength({
|
||
selectionRanges,
|
||
length: lengthValue,
|
||
})
|
||
if (err(constraintResult)) return Promise.reject(constraintResult)
|
||
const { modifiedAst, pathToNodeMap, exprInsertIndex } =
|
||
constraintResult
|
||
const pResult = parse(recast(modifiedAst))
|
||
if (trap(pResult) || !resultIsOk(pResult))
|
||
return Promise.reject(new Error('Unexpected compilation error'))
|
||
const _modifiedAst = pResult.program
|
||
if (!sketchDetails)
|
||
return Promise.reject(new Error('No sketch details'))
|
||
|
||
const {
|
||
updatedSketchEntryNodePath,
|
||
updatedSketchNodePaths,
|
||
updatedPlaneNodePath,
|
||
} = updateSketchDetailsNodePaths({
|
||
sketchEntryNodePath: sketchDetails.sketchEntryNodePath,
|
||
sketchNodePaths: sketchDetails.sketchNodePaths,
|
||
planeNodePath: sketchDetails.planeNodePath,
|
||
exprInsertIndex,
|
||
})
|
||
const updatedAst =
|
||
await sceneEntitiesManager.updateAstAndRejigSketch(
|
||
updatedSketchEntryNodePath,
|
||
updatedSketchNodePaths,
|
||
updatedPlaneNodePath,
|
||
_modifiedAst,
|
||
sketchDetails.zAxis,
|
||
sketchDetails.yAxis,
|
||
sketchDetails.origin
|
||
)
|
||
if (err(updatedAst)) return Promise.reject(updatedAst)
|
||
|
||
await codeManager.updateEditorWithAstAndWriteToFile(
|
||
updatedAst.newAst
|
||
)
|
||
|
||
const selection = updateSelections(
|
||
pathToNodeMap,
|
||
selectionRanges,
|
||
updatedAst.newAst
|
||
)
|
||
if (err(selection)) return Promise.reject(selection)
|
||
return {
|
||
selectionType: 'completeSelection',
|
||
selection,
|
||
updatedSketchEntryNodePath,
|
||
updatedSketchNodePaths,
|
||
updatedPlaneNodePath,
|
||
}
|
||
}
|
||
),
|
||
'Get perpendicular distance info': fromPromise(
|
||
async ({ input: { selectionRanges, sketchDetails } }) => {
|
||
const { modifiedAst, pathToNodeMap, exprInsertIndex } =
|
||
await applyConstraintIntersect({
|
||
selectionRanges,
|
||
})
|
||
const pResult = parse(recast(modifiedAst))
|
||
if (trap(pResult) || !resultIsOk(pResult))
|
||
return Promise.reject(new Error('Unexpected compilation error'))
|
||
const _modifiedAst = pResult.program
|
||
if (!sketchDetails)
|
||
return Promise.reject(new Error('No sketch details'))
|
||
|
||
const {
|
||
updatedSketchEntryNodePath,
|
||
updatedSketchNodePaths,
|
||
updatedPlaneNodePath,
|
||
} = updateSketchDetailsNodePaths({
|
||
sketchEntryNodePath: sketchDetails.sketchEntryNodePath,
|
||
sketchNodePaths: sketchDetails.sketchNodePaths,
|
||
planeNodePath: sketchDetails.planeNodePath,
|
||
exprInsertIndex,
|
||
})
|
||
const updatedAst =
|
||
await sceneEntitiesManager.updateAstAndRejigSketch(
|
||
updatedSketchEntryNodePath,
|
||
updatedSketchNodePaths,
|
||
updatedPlaneNodePath,
|
||
_modifiedAst,
|
||
sketchDetails.zAxis,
|
||
sketchDetails.yAxis,
|
||
sketchDetails.origin
|
||
)
|
||
if (err(updatedAst)) return Promise.reject(updatedAst)
|
||
|
||
await codeManager.updateEditorWithAstAndWriteToFile(
|
||
updatedAst.newAst
|
||
)
|
||
|
||
const selection = updateSelections(
|
||
pathToNodeMap,
|
||
selectionRanges,
|
||
updatedAst.newAst
|
||
)
|
||
if (err(selection)) return Promise.reject(selection)
|
||
return {
|
||
selectionType: 'completeSelection',
|
||
selection,
|
||
updatedSketchEntryNodePath,
|
||
updatedSketchNodePaths,
|
||
updatedPlaneNodePath,
|
||
}
|
||
}
|
||
),
|
||
'Get ABS X info': fromPromise(
|
||
async ({ input: { selectionRanges, sketchDetails } }) => {
|
||
const { modifiedAst, pathToNodeMap, exprInsertIndex } =
|
||
await applyConstraintAbsDistance({
|
||
constraint: 'xAbs',
|
||
selectionRanges,
|
||
})
|
||
const pResult = parse(recast(modifiedAst))
|
||
if (trap(pResult) || !resultIsOk(pResult))
|
||
return Promise.reject(new Error('Unexpected compilation error'))
|
||
const _modifiedAst = pResult.program
|
||
if (!sketchDetails)
|
||
return Promise.reject(new Error('No sketch details'))
|
||
|
||
const {
|
||
updatedSketchEntryNodePath,
|
||
updatedSketchNodePaths,
|
||
updatedPlaneNodePath,
|
||
} = updateSketchDetailsNodePaths({
|
||
sketchEntryNodePath: sketchDetails.sketchEntryNodePath,
|
||
sketchNodePaths: sketchDetails.sketchNodePaths,
|
||
planeNodePath: sketchDetails.planeNodePath,
|
||
exprInsertIndex,
|
||
})
|
||
const updatedAst =
|
||
await sceneEntitiesManager.updateAstAndRejigSketch(
|
||
updatedSketchEntryNodePath,
|
||
updatedSketchNodePaths,
|
||
updatedPlaneNodePath,
|
||
_modifiedAst,
|
||
sketchDetails.zAxis,
|
||
sketchDetails.yAxis,
|
||
sketchDetails.origin
|
||
)
|
||
if (err(updatedAst)) return Promise.reject(updatedAst)
|
||
|
||
await codeManager.updateEditorWithAstAndWriteToFile(
|
||
updatedAst.newAst
|
||
)
|
||
|
||
const selection = updateSelections(
|
||
pathToNodeMap,
|
||
selectionRanges,
|
||
updatedAst.newAst
|
||
)
|
||
if (err(selection)) return Promise.reject(selection)
|
||
return {
|
||
selectionType: 'completeSelection',
|
||
selection,
|
||
updatedSketchEntryNodePath,
|
||
updatedSketchNodePaths,
|
||
updatedPlaneNodePath,
|
||
}
|
||
}
|
||
),
|
||
'Get ABS Y info': fromPromise(
|
||
async ({ input: { selectionRanges, sketchDetails } }) => {
|
||
const { modifiedAst, pathToNodeMap, exprInsertIndex } =
|
||
await applyConstraintAbsDistance({
|
||
constraint: 'yAbs',
|
||
selectionRanges,
|
||
})
|
||
const pResult = parse(recast(modifiedAst))
|
||
if (trap(pResult) || !resultIsOk(pResult))
|
||
return Promise.reject(new Error('Unexpected compilation error'))
|
||
const _modifiedAst = pResult.program
|
||
if (!sketchDetails)
|
||
return Promise.reject(new Error('No sketch details'))
|
||
|
||
const {
|
||
updatedSketchEntryNodePath,
|
||
updatedSketchNodePaths,
|
||
updatedPlaneNodePath,
|
||
} = updateSketchDetailsNodePaths({
|
||
sketchEntryNodePath: sketchDetails.sketchEntryNodePath,
|
||
sketchNodePaths: sketchDetails.sketchNodePaths,
|
||
planeNodePath: sketchDetails.planeNodePath,
|
||
exprInsertIndex,
|
||
})
|
||
const updatedAst =
|
||
await sceneEntitiesManager.updateAstAndRejigSketch(
|
||
updatedSketchEntryNodePath,
|
||
updatedSketchNodePaths,
|
||
updatedPlaneNodePath,
|
||
_modifiedAst,
|
||
sketchDetails.zAxis,
|
||
sketchDetails.yAxis,
|
||
sketchDetails.origin
|
||
)
|
||
if (err(updatedAst)) return Promise.reject(updatedAst)
|
||
|
||
await codeManager.updateEditorWithAstAndWriteToFile(
|
||
updatedAst.newAst
|
||
)
|
||
|
||
const selection = updateSelections(
|
||
pathToNodeMap,
|
||
selectionRanges,
|
||
updatedAst.newAst
|
||
)
|
||
if (err(selection)) return Promise.reject(selection)
|
||
return {
|
||
selectionType: 'completeSelection',
|
||
selection,
|
||
updatedSketchEntryNodePath,
|
||
updatedSketchNodePaths,
|
||
updatedPlaneNodePath,
|
||
}
|
||
}
|
||
),
|
||
'Apply named value constraint': fromPromise(
|
||
async ({ input: { selectionRanges, sketchDetails, data } }) => {
|
||
if (!sketchDetails) {
|
||
return Promise.reject(new Error('No sketch details'))
|
||
}
|
||
if (!data) {
|
||
return Promise.reject(new Error('No data from command flow'))
|
||
}
|
||
let pResult = parse(recast(kclManager.ast))
|
||
if (trap(pResult) || !resultIsOk(pResult))
|
||
return Promise.reject(new Error('Unexpected compilation error'))
|
||
let parsed = pResult.program
|
||
|
||
let result: {
|
||
modifiedAst: Node<Program>
|
||
pathToReplaced: PathToNode | null
|
||
exprInsertIndex: number
|
||
} = {
|
||
modifiedAst: parsed,
|
||
pathToReplaced: null,
|
||
exprInsertIndex: -1,
|
||
}
|
||
// If the user provided a constant name,
|
||
// we need to insert the named constant
|
||
// and then replace the node with the constant's name.
|
||
if ('variableName' in data.namedValue) {
|
||
const astAfterReplacement = replaceValueAtNodePath({
|
||
ast: parsed,
|
||
pathToNode: data.currentValue.pathToNode,
|
||
newExpressionString: data.namedValue.variableName,
|
||
})
|
||
if (trap(astAfterReplacement)) {
|
||
return Promise.reject(astAfterReplacement)
|
||
}
|
||
const parseResultAfterInsertion = parse(
|
||
recast(
|
||
insertNamedConstant({
|
||
node: astAfterReplacement.modifiedAst,
|
||
newExpression: data.namedValue,
|
||
})
|
||
)
|
||
)
|
||
if (
|
||
trap(parseResultAfterInsertion) ||
|
||
!resultIsOk(parseResultAfterInsertion)
|
||
)
|
||
return Promise.reject(parseResultAfterInsertion)
|
||
result = {
|
||
modifiedAst: parseResultAfterInsertion.program,
|
||
pathToReplaced: astAfterReplacement.pathToReplaced,
|
||
exprInsertIndex: astAfterReplacement.exprInsertIndex,
|
||
}
|
||
} else if ('valueText' in data.namedValue) {
|
||
// If they didn't provide a constant name,
|
||
// just replace the node with the value.
|
||
const astAfterReplacement = replaceValueAtNodePath({
|
||
ast: parsed,
|
||
pathToNode: data.currentValue.pathToNode,
|
||
newExpressionString: data.namedValue.valueText,
|
||
})
|
||
if (trap(astAfterReplacement)) {
|
||
return Promise.reject(astAfterReplacement)
|
||
}
|
||
// The `replacer` function returns a pathToNode that assumes
|
||
// an identifier is also being inserted into the AST, creating an off-by-one error.
|
||
// This corrects that error, but TODO we should fix this upstream
|
||
// to avoid this kind of error in the future.
|
||
astAfterReplacement.pathToReplaced[1][0] =
|
||
(astAfterReplacement.pathToReplaced[1][0] as number) - 1
|
||
result = astAfterReplacement
|
||
}
|
||
|
||
pResult = parse(recast(result.modifiedAst))
|
||
if (trap(pResult) || !resultIsOk(pResult))
|
||
return Promise.reject(new Error('Unexpected compilation error'))
|
||
parsed = pResult.program
|
||
|
||
if (trap(parsed)) return Promise.reject(parsed)
|
||
if (!result.pathToReplaced)
|
||
return Promise.reject(new Error('No path to replaced node'))
|
||
const {
|
||
updatedSketchEntryNodePath,
|
||
updatedSketchNodePaths,
|
||
updatedPlaneNodePath,
|
||
} = updateSketchDetailsNodePaths({
|
||
sketchEntryNodePath: sketchDetails.sketchEntryNodePath,
|
||
sketchNodePaths: sketchDetails.sketchNodePaths,
|
||
planeNodePath: sketchDetails.planeNodePath,
|
||
exprInsertIndex: result.exprInsertIndex,
|
||
})
|
||
|
||
const updatedAst =
|
||
await sceneEntitiesManager.updateAstAndRejigSketch(
|
||
updatedSketchEntryNodePath,
|
||
updatedSketchNodePaths,
|
||
updatedPlaneNodePath,
|
||
parsed,
|
||
sketchDetails.zAxis,
|
||
sketchDetails.yAxis,
|
||
sketchDetails.origin
|
||
)
|
||
if (err(updatedAst)) return Promise.reject(updatedAst)
|
||
|
||
await codeManager.updateEditorWithAstAndWriteToFile(
|
||
updatedAst.newAst
|
||
)
|
||
|
||
const selection = updateSelections(
|
||
{ 0: result.pathToReplaced },
|
||
selectionRanges,
|
||
updatedAst.newAst
|
||
)
|
||
if (err(selection)) return Promise.reject(selection)
|
||
return {
|
||
selectionType: 'completeSelection',
|
||
selection,
|
||
updatedSketchEntryNodePath,
|
||
updatedSketchNodePaths,
|
||
updatedPlaneNodePath,
|
||
}
|
||
}
|
||
),
|
||
'set-up-draft-circle': fromPromise(
|
||
async ({ input: { sketchDetails, data } }) => {
|
||
if (!sketchDetails || !data)
|
||
return reject('No sketch details or data')
|
||
|
||
const result = await sceneEntitiesManager.setupDraftCircle(
|
||
sketchDetails.sketchEntryNodePath,
|
||
sketchDetails.sketchNodePaths,
|
||
sketchDetails.planeNodePath,
|
||
sketchDetails.zAxis,
|
||
sketchDetails.yAxis,
|
||
sketchDetails.origin,
|
||
data
|
||
)
|
||
if (err(result)) return reject(result)
|
||
await codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
|
||
|
||
return result
|
||
}
|
||
),
|
||
'set-up-draft-circle-three-point': fromPromise(
|
||
async ({ input: { sketchDetails, data } }) => {
|
||
if (!sketchDetails || !data)
|
||
return reject('No sketch details or data')
|
||
|
||
const result =
|
||
await sceneEntitiesManager.setupDraftCircleThreePoint(
|
||
sketchDetails.sketchEntryNodePath,
|
||
sketchDetails.sketchNodePaths,
|
||
sketchDetails.planeNodePath,
|
||
sketchDetails.zAxis,
|
||
sketchDetails.yAxis,
|
||
sketchDetails.origin,
|
||
data.p1,
|
||
data.p2
|
||
)
|
||
if (err(result)) return reject(result)
|
||
await codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
|
||
|
||
return result
|
||
}
|
||
),
|
||
'set-up-draft-rectangle': fromPromise(
|
||
async ({ input: { sketchDetails, data } }) => {
|
||
if (!sketchDetails || !data)
|
||
return reject('No sketch details or data')
|
||
|
||
const result = await sceneEntitiesManager.setupDraftRectangle(
|
||
sketchDetails.sketchEntryNodePath,
|
||
sketchDetails.sketchNodePaths,
|
||
sketchDetails.planeNodePath,
|
||
sketchDetails.zAxis,
|
||
sketchDetails.yAxis,
|
||
sketchDetails.origin,
|
||
data
|
||
)
|
||
if (err(result)) return reject(result)
|
||
await codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
|
||
|
||
return result
|
||
}
|
||
),
|
||
'set-up-draft-center-rectangle': fromPromise(
|
||
async ({ input: { sketchDetails, data } }) => {
|
||
if (!sketchDetails || !data)
|
||
return reject('No sketch details or data')
|
||
const result = await sceneEntitiesManager.setupDraftCenterRectangle(
|
||
sketchDetails.sketchEntryNodePath,
|
||
sketchDetails.sketchNodePaths,
|
||
sketchDetails.planeNodePath,
|
||
sketchDetails.zAxis,
|
||
sketchDetails.yAxis,
|
||
sketchDetails.origin,
|
||
data
|
||
)
|
||
if (err(result)) return reject(result)
|
||
await codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
|
||
|
||
return result
|
||
}
|
||
),
|
||
'set-up-draft-arc-three-point': fromPromise(
|
||
async ({ input: { sketchDetails, data } }) => {
|
||
if (!sketchDetails || !data)
|
||
return reject('No sketch details or data')
|
||
const result = await sceneEntitiesManager.setupDraftArcThreePoint(
|
||
sketchDetails.sketchEntryNodePath,
|
||
sketchDetails.sketchNodePaths,
|
||
sketchDetails.planeNodePath,
|
||
sketchDetails.zAxis,
|
||
sketchDetails.yAxis,
|
||
sketchDetails.origin,
|
||
data
|
||
)
|
||
if (err(result)) return reject(result)
|
||
await codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
|
||
|
||
return result
|
||
}
|
||
),
|
||
'set-up-draft-arc': fromPromise(
|
||
async ({ input: { sketchDetails, data } }) => {
|
||
if (!sketchDetails || !data)
|
||
return reject('No sketch details or data')
|
||
const result = await sceneEntitiesManager.setupDraftArc(
|
||
sketchDetails.sketchEntryNodePath,
|
||
sketchDetails.sketchNodePaths,
|
||
sketchDetails.planeNodePath,
|
||
sketchDetails.zAxis,
|
||
sketchDetails.yAxis,
|
||
sketchDetails.origin,
|
||
data
|
||
)
|
||
if (err(result)) return reject(result)
|
||
await codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
|
||
|
||
return result
|
||
}
|
||
),
|
||
'setup-client-side-sketch-segments': fromPromise(
|
||
async ({ input: { sketchDetails, selectionRanges } }) => {
|
||
if (!sketchDetails) return
|
||
if (!sketchDetails.sketchEntryNodePath?.length) return
|
||
sceneInfra.resetMouseListeners()
|
||
await sceneEntitiesManager.setupSketch({
|
||
sketchEntryNodePath: sketchDetails?.sketchEntryNodePath || [],
|
||
sketchNodePaths: sketchDetails.sketchNodePaths,
|
||
forward: sketchDetails.zAxis,
|
||
up: sketchDetails.yAxis,
|
||
position: sketchDetails.origin,
|
||
maybeModdedAst: kclManager.ast,
|
||
selectionRanges,
|
||
})
|
||
sceneInfra.resetMouseListeners()
|
||
|
||
sceneEntitiesManager.setupSketchIdleCallbacks({
|
||
sketchEntryNodePath: sketchDetails?.sketchEntryNodePath || [],
|
||
forward: sketchDetails.zAxis,
|
||
up: sketchDetails.yAxis,
|
||
position: sketchDetails.origin,
|
||
sketchNodePaths: sketchDetails.sketchNodePaths,
|
||
planeNodePath: sketchDetails.planeNodePath,
|
||
// We will want to pass sketchTools here
|
||
// to add their interactions
|
||
})
|
||
|
||
// We will want to update the context with sketchTools.
|
||
// They'll be used for their .destroy() in tearDownSketch
|
||
return undefined
|
||
}
|
||
),
|
||
'split-sketch-pipe-if-needed': fromPromise(
|
||
async ({ input: { sketchDetails } }) => {
|
||
if (!sketchDetails) return reject('No sketch details')
|
||
const existingSketchInfoNoOp = {
|
||
updatedEntryNodePath: sketchDetails.sketchEntryNodePath,
|
||
updatedSketchNodePaths: sketchDetails.sketchNodePaths,
|
||
updatedPlaneNodePath: sketchDetails.planeNodePath,
|
||
expressionIndexToDelete: -1,
|
||
} as const
|
||
if (!sketchDetails?.sketchEntryNodePath?.length) {
|
||
return existingSketchInfoNoOp
|
||
}
|
||
if (
|
||
!sketchDetails.sketchNodePaths.length &&
|
||
sketchDetails.planeNodePath.length
|
||
) {
|
||
// new sketch, no profiles yet
|
||
return existingSketchInfoNoOp
|
||
}
|
||
const doesNeedSplitting = doesSketchPipeNeedSplitting(
|
||
kclManager.ast,
|
||
sketchDetails.sketchEntryNodePath
|
||
)
|
||
if (err(doesNeedSplitting)) return reject(doesNeedSplitting)
|
||
let moddedAst: Node<Program> = structuredClone(kclManager.ast)
|
||
let pathToProfile = sketchDetails.sketchEntryNodePath
|
||
let updatedSketchNodePaths = sketchDetails.sketchNodePaths
|
||
if (doesNeedSplitting) {
|
||
const splitResult = splitPipedProfile(
|
||
moddedAst,
|
||
sketchDetails.sketchEntryNodePath
|
||
)
|
||
if (err(splitResult)) return reject(splitResult)
|
||
moddedAst = splitResult.modifiedAst
|
||
pathToProfile = splitResult.pathToProfile
|
||
updatedSketchNodePaths = [pathToProfile]
|
||
}
|
||
|
||
const indexToDelete = sketchDetails?.expressionIndexToDelete || -1
|
||
let isLastInPipeThreePointArc = false
|
||
if (indexToDelete >= 0) {
|
||
// this is the expression that was added when as sketch tool was used but not completed
|
||
// i.e first click for the center of the circle, but not the second click for the radius
|
||
// we added a circle to editor, but they bailed out early so we should remove it
|
||
|
||
const pipe = getNodeFromPath<PipeExpression>(
|
||
moddedAst,
|
||
pathToProfile,
|
||
'PipeExpression'
|
||
)
|
||
if (err(pipe)) {
|
||
isLastInPipeThreePointArc = false
|
||
} else {
|
||
const lastInPipe = pipe?.node?.body?.[pipe.node.body.length - 1]
|
||
if (
|
||
lastInPipe &&
|
||
Number(pathToProfile[1][0]) === indexToDelete &&
|
||
lastInPipe.type === 'CallExpression' &&
|
||
lastInPipe.callee.type === 'Name' &&
|
||
lastInPipe.callee.name.name === 'arcTo'
|
||
) {
|
||
isLastInPipeThreePointArc = true
|
||
pipe.node.body = pipe.node.body.slice(0, -1)
|
||
}
|
||
}
|
||
|
||
if (!isLastInPipeThreePointArc) {
|
||
moddedAst.body.splice(indexToDelete, 1)
|
||
// make sure the deleted expression is removed from the sketchNodePaths
|
||
updatedSketchNodePaths = updatedSketchNodePaths.filter(
|
||
(path) => path[1][0] !== indexToDelete
|
||
)
|
||
// if the deleted expression was the entryNodePath, we should just make it the first sketchNodePath
|
||
// as a safe default
|
||
pathToProfile =
|
||
pathToProfile[1][0] !== indexToDelete
|
||
? pathToProfile
|
||
: updatedSketchNodePaths[0]
|
||
}
|
||
}
|
||
|
||
if (
|
||
doesNeedSplitting ||
|
||
indexToDelete >= 0 ||
|
||
isLastInPipeThreePointArc
|
||
) {
|
||
await updateModelingState(moddedAst, EXECUTION_TYPE_MOCK, {
|
||
kclManager,
|
||
editorManager,
|
||
codeManager,
|
||
})
|
||
}
|
||
return {
|
||
updatedEntryNodePath: pathToProfile,
|
||
updatedSketchNodePaths: updatedSketchNodePaths,
|
||
updatedPlaneNodePath: sketchDetails.planeNodePath,
|
||
expressionIndexToDelete: -1,
|
||
}
|
||
}
|
||
),
|
||
'submit-prompt-edit': fromPromise(async ({ input }) => {
|
||
return await promptToEditFlow({
|
||
code: codeManager.code,
|
||
prompt: input.prompt,
|
||
selections: input.selection,
|
||
token,
|
||
artifactGraph: kclManager.artifactGraph,
|
||
projectName: context.project.name,
|
||
})
|
||
}),
|
||
},
|
||
}),
|
||
{
|
||
input: {
|
||
...modelingMachineDefaultContext,
|
||
store: {
|
||
...modelingMachineDefaultContext.store,
|
||
...persistedContext,
|
||
},
|
||
machineManager,
|
||
},
|
||
// devTools: true,
|
||
}
|
||
)
|
||
|
||
// Register file menu actions based off modeling send
|
||
const cb = (data: WebContentSendPayload) => {
|
||
const openPanes = modelingActor.getSnapshot().context.store.openPanes
|
||
if (data.menuLabel === 'View.Panes.Feature tree') {
|
||
const featureTree: SidebarType = 'feature-tree'
|
||
const alwaysAddFeatureTree: SidebarType[] = [
|
||
...new Set([...openPanes, featureTree]),
|
||
]
|
||
modelingSend({
|
||
type: 'Set context',
|
||
data: {
|
||
openPanes: alwaysAddFeatureTree,
|
||
},
|
||
})
|
||
} else if (data.menuLabel === 'View.Panes.KCL code') {
|
||
const code: SidebarType = 'code'
|
||
const alwaysAddCode: SidebarType[] = [...new Set([...openPanes, code])]
|
||
modelingSend({
|
||
type: 'Set context',
|
||
data: {
|
||
openPanes: alwaysAddCode,
|
||
},
|
||
})
|
||
} else if (data.menuLabel === 'View.Panes.Project files') {
|
||
const projectFiles: SidebarType = 'files'
|
||
const alwaysAddProjectFiles: SidebarType[] = [
|
||
...new Set([...openPanes, projectFiles]),
|
||
]
|
||
modelingSend({
|
||
type: 'Set context',
|
||
data: {
|
||
openPanes: alwaysAddProjectFiles,
|
||
},
|
||
})
|
||
} else if (data.menuLabel === 'View.Panes.Variables') {
|
||
const variables: SidebarType = 'variables'
|
||
const alwaysAddVariables: SidebarType[] = [
|
||
...new Set([...openPanes, variables]),
|
||
]
|
||
modelingSend({
|
||
type: 'Set context',
|
||
data: {
|
||
openPanes: alwaysAddVariables,
|
||
},
|
||
})
|
||
} else if (data.menuLabel === 'View.Panes.Logs') {
|
||
const logs: SidebarType = 'logs'
|
||
const alwaysAddLogs: SidebarType[] = [...new Set([...openPanes, logs])]
|
||
modelingSend({
|
||
type: 'Set context',
|
||
data: {
|
||
openPanes: alwaysAddLogs,
|
||
},
|
||
})
|
||
} else if (data.menuLabel === 'Design.Start sketch') {
|
||
modelingSend({
|
||
type: 'Enter sketch',
|
||
data: { forceNewSketch: true },
|
||
})
|
||
}
|
||
}
|
||
useMenuListener(cb)
|
||
|
||
const { overallState } = useNetworkContext()
|
||
const { isExecuting } = useKclContext()
|
||
const { isStreamReady } = useAppState()
|
||
|
||
// Assumes all commands are network commands
|
||
useSketchModeMenuEnableDisable(
|
||
modelingState.context.currentMode,
|
||
overallState,
|
||
isExecuting,
|
||
isStreamReady,
|
||
[
|
||
{ menuLabel: 'Edit.Modify with Zoo Text-To-CAD' },
|
||
{ menuLabel: 'View.Standard views' },
|
||
{ menuLabel: 'View.Named views' },
|
||
{ menuLabel: 'Design.Start sketch' },
|
||
{
|
||
menuLabel: 'Design.Create an offset plane',
|
||
commandName: 'Offset plane',
|
||
groupId: 'modeling',
|
||
},
|
||
{
|
||
menuLabel: 'Design.Create a helix',
|
||
commandName: 'Helix',
|
||
groupId: 'modeling',
|
||
},
|
||
{
|
||
menuLabel: 'Design.Create an additive feature.Extrude',
|
||
commandName: 'Extrude',
|
||
groupId: 'modeling',
|
||
},
|
||
{
|
||
menuLabel: 'Design.Create an additive feature.Revolve',
|
||
commandName: 'Revolve',
|
||
groupId: 'modeling',
|
||
},
|
||
{
|
||
menuLabel: 'Design.Create an additive feature.Sweep',
|
||
commandName: 'Sweep',
|
||
groupId: 'modeling',
|
||
},
|
||
{
|
||
menuLabel: 'Design.Create an additive feature.Loft',
|
||
commandName: 'Loft',
|
||
groupId: 'modeling',
|
||
},
|
||
{
|
||
menuLabel: 'Design.Apply modification feature.Fillet',
|
||
commandName: 'Fillet',
|
||
groupId: 'modeling',
|
||
},
|
||
{
|
||
menuLabel: 'Design.Apply modification feature.Chamfer',
|
||
commandName: 'Chamfer',
|
||
groupId: 'modeling',
|
||
},
|
||
{
|
||
menuLabel: 'Design.Apply modification feature.Shell',
|
||
commandName: 'Shell',
|
||
groupId: 'modeling',
|
||
},
|
||
{
|
||
menuLabel: 'Design.Create with Zoo Text-To-CAD',
|
||
commandName: 'Text-to-CAD',
|
||
groupId: 'modeling',
|
||
},
|
||
{
|
||
menuLabel: 'Design.Modify with Zoo Text-To-CAD',
|
||
commandName: 'Prompt-to-edit',
|
||
groupId: 'modeling',
|
||
},
|
||
]
|
||
)
|
||
|
||
// Add debug function to window object
|
||
useEffect(() => {
|
||
// @ts-ignore - we're intentionally adding this to window
|
||
window.getModelingState = () => {
|
||
const modelingState = modelingActor.getSnapshot()
|
||
return {
|
||
modelingState,
|
||
id: modelingState._nodes[modelingState._nodes.length - 1].id,
|
||
}
|
||
}
|
||
}, [modelingActor])
|
||
|
||
// Give the state back to the editorManager.
|
||
useEffect(() => {
|
||
editorManager.modelingSend = modelingSend
|
||
}, [modelingSend])
|
||
|
||
useEffect(() => {
|
||
editorManager.modelingState = modelingState
|
||
}, [modelingState])
|
||
|
||
useEffect(() => {
|
||
editorManager.selectionRanges = modelingState.context.selectionRanges
|
||
}, [modelingState.context.selectionRanges])
|
||
|
||
// When changing camera modes reset the camera to the default orientation to correct
|
||
// the up vector otherwise the conconical orientation for the camera modes will be
|
||
// wrong
|
||
useEffect(() => {
|
||
sceneInfra.camControls.resetCameraPosition().catch(reportRejection)
|
||
}, [cameraOrbit.current])
|
||
|
||
useEffect(() => {
|
||
const onConnectionStateChanged = ({ detail }: CustomEvent) => {
|
||
// If we are in sketch mode we need to exit it.
|
||
// TODO: how do i check if we are in a sketch mode, I only want to call
|
||
// this then.
|
||
if (detail.type === EngineConnectionStateType.Disconnecting) {
|
||
modelingSend({ type: 'Cancel' })
|
||
}
|
||
}
|
||
engineCommandManager.engineConnection?.addEventListener(
|
||
EngineConnectionEvents.ConnectionStateChanged,
|
||
onConnectionStateChanged as EventListener
|
||
)
|
||
return () => {
|
||
engineCommandManager.engineConnection?.removeEventListener(
|
||
EngineConnectionEvents.ConnectionStateChanged,
|
||
onConnectionStateChanged as EventListener
|
||
)
|
||
}
|
||
}, [engineCommandManager.engineConnection, modelingSend])
|
||
|
||
useEffect(() => {
|
||
const inSketchMode = modelingState.matches('Sketch')
|
||
|
||
// If you are in sketch mode and you disable the orbit, return back to the normal view to the target
|
||
if (!allowOrbitInSketchMode.current) {
|
||
const targetId = modelingState.context.sketchDetails?.animateTargetId
|
||
if (inSketchMode && targetId) {
|
||
letEngineAnimateAndSyncCamAfter(engineCommandManager, targetId)
|
||
.then(() => {})
|
||
.catch((e) => {
|
||
console.error(
|
||
'failed to sync engine and client scene after disabling allow orbit in sketch mode'
|
||
)
|
||
console.error(e)
|
||
})
|
||
}
|
||
}
|
||
|
||
// While you are in sketch mode you should be able to control the enable rotate
|
||
// Once you exit it goes back to normal
|
||
if (inSketchMode) {
|
||
sceneInfra.camControls.enableRotate = allowOrbitInSketchMode.current
|
||
}
|
||
}, [allowOrbitInSketchMode.current])
|
||
|
||
// Allow using the delete key to delete solids. Backspace only on macOS as Windows and Linux have dedicated Delete
|
||
// `navigator.platform` is deprecated, but the alternative `navigator.userAgentData.platform` is not reliable
|
||
const deleteKeys =
|
||
platform() === 'macos' ? ['backspace', 'delete', 'del'] : ['delete', 'del']
|
||
useHotkeys(deleteKeys, () => {
|
||
modelingSend({ type: 'Delete selection' })
|
||
})
|
||
|
||
// Allow ctrl+alt+c to center to selection
|
||
useHotkeys(['mod + alt + c'], () => {
|
||
modelingSend({ type: 'Center camera on selection' })
|
||
})
|
||
|
||
useStateMachineCommands({
|
||
machineId: 'modeling',
|
||
state: modelingState,
|
||
send: modelingSend,
|
||
actor: modelingActor,
|
||
commandBarConfig: modelingMachineCommandConfig,
|
||
allCommandsRequireNetwork: true,
|
||
// TODO for when sketch tools are in the toolbar: This was added when we used one "Cancel" event,
|
||
// but we need to support "SketchCancel" and basically
|
||
// make this function take the actor or state so it
|
||
// can call the correct event.
|
||
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
|