Modeling machine unit tests (#7098)
* successfully transition to sketch idle * get constraint to mod code * clean up * remove .only * Fixed tsc --------- Co-authored-by: lee-at-zoo-corp <lee@zoo.dev>
This commit is contained in:
@ -12,12 +12,8 @@ import { useLoaderData } from 'react-router-dom'
|
|||||||
import type { Actor, ContextFrom, Prop, SnapshotFrom, StateFrom } from 'xstate'
|
import type { Actor, ContextFrom, Prop, SnapshotFrom, StateFrom } from 'xstate'
|
||||||
import { assign, fromPromise } from 'xstate'
|
import { assign, fromPromise } from 'xstate'
|
||||||
|
|
||||||
import type {
|
import type { OutputFormat3d } from '@rust/kcl-lib/bindings/ModelingCmd'
|
||||||
OutputFormat3d,
|
|
||||||
Point3d,
|
|
||||||
} from '@rust/kcl-lib/bindings/ModelingCmd'
|
|
||||||
import type { Node } from '@rust/kcl-lib/bindings/Node'
|
import type { Node } from '@rust/kcl-lib/bindings/Node'
|
||||||
import type { Plane } from '@rust/kcl-lib/bindings/Plane'
|
|
||||||
|
|
||||||
import { useAppState } from '@src/AppState'
|
import { useAppState } from '@src/AppState'
|
||||||
import { letEngineAnimateAndSyncCamAfter } from '@src/clientSideScene/CameraControls'
|
import { letEngineAnimateAndSyncCamAfter } from '@src/clientSideScene/CameraControls'
|
||||||
@ -38,26 +34,16 @@ import useModelingMachineCommands from '@src/hooks/useStateMachineCommands'
|
|||||||
import { useKclContext } from '@src/lang/KclProvider'
|
import { useKclContext } from '@src/lang/KclProvider'
|
||||||
import { updateModelingState } from '@src/lang/modelingWorkflows'
|
import { updateModelingState } from '@src/lang/modelingWorkflows'
|
||||||
import {
|
import {
|
||||||
insertNamedConstant,
|
|
||||||
replaceValueAtNodePath,
|
|
||||||
sketchOnExtrudedFace,
|
sketchOnExtrudedFace,
|
||||||
sketchOnOffsetPlane,
|
sketchOnOffsetPlane,
|
||||||
splitPipedProfile,
|
splitPipedProfile,
|
||||||
startSketchOnDefault,
|
startSketchOnDefault,
|
||||||
} from '@src/lang/modifyAst'
|
} from '@src/lang/modifyAst'
|
||||||
import {
|
import {
|
||||||
artifactIsPlaneWithPaths,
|
|
||||||
doesSketchPipeNeedSplitting,
|
doesSketchPipeNeedSplitting,
|
||||||
getNodeFromPath,
|
getNodeFromPath,
|
||||||
isCursorInFunctionDefinition,
|
|
||||||
traverse,
|
traverse,
|
||||||
} from '@src/lang/queryAst'
|
} from '@src/lang/queryAst'
|
||||||
import { getNodePathFromSourceRange } from '@src/lang/queryAstNodePathUtils'
|
|
||||||
import {
|
|
||||||
getFaceCodeRef,
|
|
||||||
getPathsFromArtifact,
|
|
||||||
getPlaneFromArtifact,
|
|
||||||
} from '@src/lang/std/artifactGraph'
|
|
||||||
import {
|
import {
|
||||||
EngineConnectionStateType,
|
EngineConnectionStateType,
|
||||||
EngineConnectionEvents,
|
EngineConnectionEvents,
|
||||||
@ -66,7 +52,6 @@ import { err, reportRejection, trap, reject } from '@src/lib/trap'
|
|||||||
import { isNonNullable, platform, uuidv4 } from '@src/lib/utils'
|
import { isNonNullable, platform, uuidv4 } from '@src/lib/utils'
|
||||||
import { promptToEditFlow } from '@src/lib/promptToEdit'
|
import { promptToEditFlow } from '@src/lib/promptToEdit'
|
||||||
import type { FileMeta } from '@src/lib/types'
|
import type { FileMeta } from '@src/lib/types'
|
||||||
import { kclEditorActor } from '@src/machines/kclEditorMachine'
|
|
||||||
import { commandBarActor } from '@src/lib/singletons'
|
import { commandBarActor } from '@src/lib/singletons'
|
||||||
import { useToken, useSettings } from '@src/lib/singletons'
|
import { useToken, useSettings } from '@src/lib/singletons'
|
||||||
import type { IndexLoaderData } from '@src/lib/types'
|
import type { IndexLoaderData } from '@src/lib/types'
|
||||||
@ -98,23 +83,13 @@ import {
|
|||||||
} from '@src/lib/singletons'
|
} from '@src/lib/singletons'
|
||||||
import type { MachineManager } from '@src/components/MachineManagerProvider'
|
import type { MachineManager } from '@src/components/MachineManagerProvider'
|
||||||
import { MachineManagerContext } from '@src/components/MachineManagerProvider'
|
import { MachineManagerContext } from '@src/components/MachineManagerProvider'
|
||||||
import {
|
import { updateSelections } from '@src/lib/selections'
|
||||||
handleSelectionBatch,
|
import { updateSketchDetailsNodePaths } from '@src/lang/util'
|
||||||
updateSelections,
|
|
||||||
type Selections,
|
|
||||||
} from '@src/lib/selections'
|
|
||||||
import {
|
|
||||||
crossProduct,
|
|
||||||
isCursorInSketchCommandRange,
|
|
||||||
updateSketchDetailsNodePaths,
|
|
||||||
} from '@src/lang/util'
|
|
||||||
import {
|
import {
|
||||||
modelingMachineCommandConfig,
|
modelingMachineCommandConfig,
|
||||||
type ModelingCommandSchema,
|
type ModelingCommandSchema,
|
||||||
} from '@src/lib/commandBarConfigs/modelingCommandConfig'
|
} from '@src/lib/commandBarConfigs/modelingCommandConfig'
|
||||||
import type {
|
import type {
|
||||||
KclValue,
|
|
||||||
PathToNode,
|
|
||||||
PipeExpression,
|
PipeExpression,
|
||||||
Program,
|
Program,
|
||||||
VariableDeclaration,
|
VariableDeclaration,
|
||||||
@ -320,229 +295,6 @@ export const ModelingMachineProvider = ({
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
'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 {}
|
|
||||||
}
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
guards: {
|
guards: {
|
||||||
'has valid selection for deletion': ({
|
'has valid selection for deletion': ({
|
||||||
@ -552,35 +304,6 @@ export const ModelingMachineProvider = ({
|
|||||||
if (selectionRanges.graphSelections.length <= 0) return false
|
if (selectionRanges.graphSelections.length <= 0) return false
|
||||||
return true
|
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': () =>
|
'Has exportable geometry': () =>
|
||||||
!kclManager.hasErrors() && kclManager.ast.body.length > 0,
|
!kclManager.hasErrors() && kclManager.ast.body.length > 0,
|
||||||
},
|
},
|
||||||
@ -850,123 +573,6 @@ export const ModelingMachineProvider = ({
|
|||||||
animateTargetId: input.planeId,
|
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(
|
'Get horizontal info': fromPromise(
|
||||||
async ({ input: { selectionRanges, sketchDetails } }) => {
|
async ({ input: { selectionRanges, sketchDetails } }) => {
|
||||||
const { modifiedAst, pathToNodeMap, exprInsertIndex } =
|
const { modifiedAst, pathToNodeMap, exprInsertIndex } =
|
||||||
@ -1371,130 +977,6 @@ export const ModelingMachineProvider = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
'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,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
)
|
|
||||||
result.exprInsertIndex = data.namedValue.insertIndex
|
|
||||||
|
|
||||||
if (
|
|
||||||
trap(parseResultAfterInsertion) ||
|
|
||||||
!resultIsOk(parseResultAfterInsertion)
|
|
||||||
)
|
|
||||||
return Promise.reject(parseResultAfterInsertion)
|
|
||||||
result = {
|
|
||||||
modifiedAst: parseResultAfterInsertion.program,
|
|
||||||
pathToReplaced: astAfterReplacement.pathToReplaced,
|
|
||||||
exprInsertIndex: result.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(
|
'set-up-draft-circle': fromPromise(
|
||||||
async ({ input: { sketchDetails, data } }) => {
|
async ({ input: { sketchDetails, data } }) => {
|
||||||
if (!sketchDetails || !data)
|
if (!sketchDetails || !data)
|
||||||
@ -1608,38 +1090,6 @@ export const ModelingMachineProvider = ({
|
|||||||
return result
|
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(
|
'split-sketch-pipe-if-needed': fromPromise(
|
||||||
async ({ input: { sketchDetails } }) => {
|
async ({ input: { sketchDetails } }) => {
|
||||||
if (!sketchDetails) return reject('No sketch details')
|
if (!sketchDetails) return reject('No sketch details')
|
||||||
|
245
src/machines/modelingMachine.test.ts
Normal file
245
src/machines/modelingMachine.test.ts
Normal file
@ -0,0 +1,245 @@
|
|||||||
|
import {
|
||||||
|
modelingMachine,
|
||||||
|
modelingMachineDefaultContext,
|
||||||
|
} from '@src/machines/modelingMachine'
|
||||||
|
import { createActor } from 'xstate'
|
||||||
|
import { vi } from 'vitest'
|
||||||
|
import { assertParse, type CallExpressionKw } from '@src/lang/wasm'
|
||||||
|
import { initPromise } from '@src/lang/wasmUtils'
|
||||||
|
import {
|
||||||
|
codeManager,
|
||||||
|
engineCommandManager,
|
||||||
|
kclManager,
|
||||||
|
} from '@src/lib/singletons'
|
||||||
|
import { VITE_KC_DEV_TOKEN } from '@src/env'
|
||||||
|
import { line } from '@src/lang/std/sketch'
|
||||||
|
import { getNodeFromPath } from '@src/lang/queryAst'
|
||||||
|
import type { Node } from '@rust/kcl-lib/bindings/Node'
|
||||||
|
import { err } from '@src/lib/trap'
|
||||||
|
import {
|
||||||
|
createIdentifier,
|
||||||
|
createLiteral,
|
||||||
|
createVariableDeclaration,
|
||||||
|
} from '@src/lang/create'
|
||||||
|
|
||||||
|
// Store original method to restore in afterAll
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await initPromise
|
||||||
|
|
||||||
|
// THESE TEST WILL FAIL without VITE_KC_DEV_TOKEN set in .env.development.local
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
engineCommandManager.start({
|
||||||
|
token: VITE_KC_DEV_TOKEN,
|
||||||
|
width: 256,
|
||||||
|
height: 256,
|
||||||
|
setMediaStream: () => {},
|
||||||
|
setIsStreamReady: () => {},
|
||||||
|
callbackOnEngineLiteConnect: () => {
|
||||||
|
resolve(true)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}, 30_000)
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
// Restore the original method
|
||||||
|
|
||||||
|
engineCommandManager.tearDown()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Define mock implementations that will be referenced in vi.mock calls
|
||||||
|
vi.mock('@src/components/SetHorVertDistanceModal', () => ({
|
||||||
|
createInfoModal: vi.fn(() => ({
|
||||||
|
open: vi.fn().mockResolvedValue({
|
||||||
|
value: '10',
|
||||||
|
segName: 'test',
|
||||||
|
valueNode: {},
|
||||||
|
newVariableInsertIndex: 0,
|
||||||
|
sign: 1,
|
||||||
|
}),
|
||||||
|
})),
|
||||||
|
GetInfoModal: vi.fn(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@src/components/SetAngleLengthModal', () => ({
|
||||||
|
createSetAngleLengthModal: vi.fn(() => ({
|
||||||
|
open: vi.fn().mockResolvedValue({
|
||||||
|
value: '45',
|
||||||
|
segName: 'test',
|
||||||
|
valueNode: {},
|
||||||
|
newVariableInsertIndex: 0,
|
||||||
|
sign: 1,
|
||||||
|
}),
|
||||||
|
})),
|
||||||
|
SetAngleLengthModal: vi.fn(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Add this function before the test cases
|
||||||
|
// Utility function to wait for a condition to be met
|
||||||
|
const waitForCondition = async (
|
||||||
|
condition: () => boolean,
|
||||||
|
timeout = 5000,
|
||||||
|
interval = 100
|
||||||
|
) => {
|
||||||
|
const startTime = Date.now()
|
||||||
|
|
||||||
|
while (Date.now() - startTime < timeout) {
|
||||||
|
try {
|
||||||
|
if (condition()) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore errors, keep polling
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for the next interval
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, interval))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Last attempt before failing
|
||||||
|
return condition()
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('modelingMachine - XState', () => {
|
||||||
|
describe('when initialized', () => {
|
||||||
|
it('should start in the idle state', () => {
|
||||||
|
const actor = createActor(modelingMachine, {
|
||||||
|
input: modelingMachineDefaultContext,
|
||||||
|
}).start()
|
||||||
|
const state = actor.getSnapshot().value
|
||||||
|
|
||||||
|
// The machine should start in the idle state
|
||||||
|
expect(state).toEqual({ idle: 'hidePlanes' })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when in sketch mode', () => {
|
||||||
|
it('should transition to sketch state when entering sketch mode', async () => {
|
||||||
|
const code = `sketch001 = startSketchOn(XZ)
|
||||||
|
profile001 = startProfile(sketch001, at = [2263.04, -2721.2])
|
||||||
|
|> line(end = [16.27, 73.81])
|
||||||
|
|> line(end = [75.72, 18.41])
|
||||||
|
`
|
||||||
|
|
||||||
|
const ast = assertParse(code)
|
||||||
|
|
||||||
|
await kclManager.executeAst({ ast })
|
||||||
|
|
||||||
|
expect(kclManager.errors).toEqual([])
|
||||||
|
|
||||||
|
const indexOfInterest = code.indexOf('[16.27, 73.81]')
|
||||||
|
|
||||||
|
// segment artifact with that source range
|
||||||
|
const artifact = [...kclManager.artifactGraph].find(
|
||||||
|
([_, artifact]) =>
|
||||||
|
artifact?.type === 'segment' &&
|
||||||
|
artifact.codeRef.range[0] <= indexOfInterest &&
|
||||||
|
indexOfInterest <= artifact.codeRef.range[1]
|
||||||
|
)?.[1]
|
||||||
|
if (!artifact || !('codeRef' in artifact)) {
|
||||||
|
throw new Error('Artifact not found or invalid artifact structure')
|
||||||
|
}
|
||||||
|
|
||||||
|
const actor = createActor(modelingMachine, {
|
||||||
|
input: modelingMachineDefaultContext,
|
||||||
|
}).start()
|
||||||
|
|
||||||
|
// Send event to transition to sketch mode
|
||||||
|
actor.send({
|
||||||
|
type: 'Set selection',
|
||||||
|
data: {
|
||||||
|
selectionType: 'mirrorCodeMirrorSelections',
|
||||||
|
selection: {
|
||||||
|
graphSelections: [
|
||||||
|
{
|
||||||
|
artifact: artifact,
|
||||||
|
codeRef: artifact.codeRef,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
otherSelections: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
actor.send({ type: 'Enter sketch' })
|
||||||
|
|
||||||
|
// Check that we're in the sketch state
|
||||||
|
let state = actor.getSnapshot()
|
||||||
|
expect(state.value).toBe('animating to existing sketch')
|
||||||
|
|
||||||
|
// wait for it to transition
|
||||||
|
await waitForCondition(() => {
|
||||||
|
const snapshot = actor.getSnapshot()
|
||||||
|
return snapshot.value !== 'animating to existing sketch'
|
||||||
|
}, 5000)
|
||||||
|
|
||||||
|
// After the condition is met, do the actual assertion
|
||||||
|
expect(actor.getSnapshot().value).toEqual({
|
||||||
|
Sketch: { SketchIdle: 'scene drawn' },
|
||||||
|
})
|
||||||
|
|
||||||
|
const getConstraintInfo = line.getConstraintInfo
|
||||||
|
const callExp = getNodeFromPath<Node<CallExpressionKw>>(
|
||||||
|
kclManager.ast,
|
||||||
|
artifact.codeRef.pathToNode,
|
||||||
|
'CallExpressionKw'
|
||||||
|
)
|
||||||
|
if (err(callExp)) {
|
||||||
|
throw new Error('Failed to get CallExpressionKw node')
|
||||||
|
}
|
||||||
|
const constraintInfo = getConstraintInfo(
|
||||||
|
callExp.node,
|
||||||
|
codeManager.code,
|
||||||
|
artifact.codeRef.pathToNode
|
||||||
|
)
|
||||||
|
const first = constraintInfo[0]
|
||||||
|
|
||||||
|
// Now that we're in sketchIdle state, test the "Constrain with named value" event
|
||||||
|
actor.send({
|
||||||
|
type: 'Constrain with named value',
|
||||||
|
data: {
|
||||||
|
currentValue: {
|
||||||
|
valueText: first.value,
|
||||||
|
pathToNode: first.pathToNode,
|
||||||
|
variableName: 'test_variable',
|
||||||
|
},
|
||||||
|
// Use type assertion to mock the complex type
|
||||||
|
namedValue: {
|
||||||
|
valueText: '20',
|
||||||
|
variableName: 'test_variable',
|
||||||
|
insertIndex: 0,
|
||||||
|
valueCalculated: '20',
|
||||||
|
variableDeclarationAst: createVariableDeclaration(
|
||||||
|
'test_variable',
|
||||||
|
createLiteral('20')
|
||||||
|
),
|
||||||
|
variableIdentifierAst: createIdentifier('test_variable') as any,
|
||||||
|
valueAst: createLiteral('20'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Wait for the state to change in response to the constraint
|
||||||
|
await waitForCondition(() => {
|
||||||
|
const snapshot = actor.getSnapshot()
|
||||||
|
// Check if we've transitioned to a different state
|
||||||
|
return (
|
||||||
|
JSON.stringify(snapshot.value) !==
|
||||||
|
JSON.stringify({
|
||||||
|
Sketch: { SketchIdle: 'set up segments' },
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}, 5000)
|
||||||
|
|
||||||
|
await waitForCondition(() => {
|
||||||
|
const snapshot = actor.getSnapshot()
|
||||||
|
// Check if we've transitioned to a different state
|
||||||
|
return (
|
||||||
|
JSON.stringify(snapshot.value) !==
|
||||||
|
JSON.stringify({ Sketch: 'Converting to named value' })
|
||||||
|
)
|
||||||
|
}, 5000)
|
||||||
|
expect(codeManager.code).toContain('line(end = [test_variable,')
|
||||||
|
}, 10_000)
|
||||||
|
})
|
||||||
|
})
|
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user