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 { assign, fromPromise } from 'xstate'
|
||||
|
||||
import type {
|
||||
OutputFormat3d,
|
||||
Point3d,
|
||||
} from '@rust/kcl-lib/bindings/ModelingCmd'
|
||||
import type { OutputFormat3d } 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'
|
||||
@ -38,26 +34,16 @@ import useModelingMachineCommands 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 {
|
||||
EngineConnectionStateType,
|
||||
EngineConnectionEvents,
|
||||
@ -66,7 +52,6 @@ import { err, reportRejection, trap, reject } from '@src/lib/trap'
|
||||
import { isNonNullable, platform, uuidv4 } from '@src/lib/utils'
|
||||
import { promptToEditFlow } from '@src/lib/promptToEdit'
|
||||
import type { FileMeta } from '@src/lib/types'
|
||||
import { kclEditorActor } from '@src/machines/kclEditorMachine'
|
||||
import { commandBarActor } from '@src/lib/singletons'
|
||||
import { useToken, useSettings } from '@src/lib/singletons'
|
||||
import type { IndexLoaderData } from '@src/lib/types'
|
||||
@ -98,23 +83,13 @@ import {
|
||||
} from '@src/lib/singletons'
|
||||
import type { MachineManager } from '@src/components/MachineManagerProvider'
|
||||
import { MachineManagerContext } from '@src/components/MachineManagerProvider'
|
||||
import {
|
||||
handleSelectionBatch,
|
||||
updateSelections,
|
||||
type Selections,
|
||||
} from '@src/lib/selections'
|
||||
import {
|
||||
crossProduct,
|
||||
isCursorInSketchCommandRange,
|
||||
updateSketchDetailsNodePaths,
|
||||
} from '@src/lang/util'
|
||||
import { updateSelections } from '@src/lib/selections'
|
||||
import { updateSketchDetailsNodePaths } from '@src/lang/util'
|
||||
import {
|
||||
modelingMachineCommandConfig,
|
||||
type ModelingCommandSchema,
|
||||
} from '@src/lib/commandBarConfigs/modelingCommandConfig'
|
||||
import type {
|
||||
KclValue,
|
||||
PathToNode,
|
||||
PipeExpression,
|
||||
Program,
|
||||
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: {
|
||||
'has valid selection for deletion': ({
|
||||
@ -552,35 +304,6 @@ export const ModelingMachineProvider = ({
|
||||
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': () =>
|
||||
!kclManager.hasErrors() && kclManager.ast.body.length > 0,
|
||||
},
|
||||
@ -850,123 +573,6 @@ export const ModelingMachineProvider = ({
|
||||
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 } =
|
||||
@ -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(
|
||||
async ({ input: { sketchDetails, data } }) => {
|
||||
if (!sketchDetails || !data)
|
||||
@ -1608,38 +1090,6 @@ export const ModelingMachineProvider = ({
|
||||
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')
|
||||
|
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