Fixing up edit flows for transforms, and WIP for simpler feature tree action

This commit is contained in:
Pierre Jacquier
2025-07-04 07:46:32 -04:00
parent a40ba06641
commit 78f885c3d1
5 changed files with 291 additions and 212 deletions

View File

@ -24,7 +24,12 @@ import {
getOperationVariableName,
stdLibMap,
} from '@src/lib/operations'
import { editorManager, kclManager, rustContext } from '@src/lib/singletons'
import {
commandBarActor,
editorManager,
kclManager,
rustContext,
} from '@src/lib/singletons'
import {
featureTreeMachine,
featureTreeMachineDefaultContext,
@ -59,6 +64,15 @@ export const FeatureTreePane = () => {
scrollToError: () => {
editorManager.scrollToFirstErrorDiagnosticIfExists()
},
sendTranslateCommand: ({ context }) => {
commandBarActor.send({
type: 'Find and select command',
data: {
name: 'Translate',
groupId: 'modeling',
},
})
},
sendSelectionEvent: ({ context }) => {
if (!context.targetSourceRange) {
return
@ -354,10 +368,6 @@ const OperationItem = (props: {
})
}
/**
* For now we can only enter the "edit" flow for the startSketchOn operation.
* TODO: https://github.com/KittyCAD/modeling-app/issues/4442
*/
function enterEditFlow() {
if (
props.item.type === 'StdLibCall' ||

View File

@ -32,15 +32,17 @@ export function addTranslate({
z,
global,
nodeToEdit,
callName,
}: {
ast: Node<Program>
artifactGraph: ArtifactGraph
objects: Selections
x: KclCommandValue
y: KclCommandValue
z: KclCommandValue
x?: KclCommandValue
y?: KclCommandValue
z?: KclCommandValue
global?: boolean
nodeToEdit?: PathToNode
callName?: string
}): Error | { modifiedAst: Node<Program>; pathToNode: PathToNode } {
// 1. Clone the ast so we can edit it
const modifiedAst = structuredClone(ast)
@ -58,26 +60,28 @@ export function addTranslate({
return variableExpressions
}
// Extra labeled args expression
const xExpr = x ? [createLabeledArg('x', valueOrVariable(x))] : []
const yExpr = y ? [createLabeledArg('y', valueOrVariable(y))] : []
const zExpr = z ? [createLabeledArg('z', valueOrVariable(z))] : []
const globalExpr = global
? [createLabeledArg('global', createLiteral(global))]
: []
const objectsExpr = createVariableExpressionsArray(variableExpressions.exprs)
const call = createCallExpressionStdLibKw('translate', objectsExpr, [
createLabeledArg('x', valueOrVariable(x)),
createLabeledArg('y', valueOrVariable(y)),
createLabeledArg('z', valueOrVariable(z)),
...globalExpr,
])
const call = createCallExpressionStdLibKw(
callName ?? 'translate',
objectsExpr,
[...xExpr, ...yExpr, ...zExpr, ...globalExpr]
)
// Insert variables for labeled arguments if provided
if ('variableName' in x && x.variableName) {
if (x && 'variableName' in x && x.variableName) {
insertVariableAndOffsetPathToNode(x, modifiedAst, nodeToEdit)
}
if ('variableName' in y && y.variableName) {
if (y && 'variableName' in y && y.variableName) {
insertVariableAndOffsetPathToNode(y, modifiedAst, nodeToEdit)
}
if ('variableName' in z && z.variableName) {
if (z && 'variableName' in z && z.variableName) {
insertVariableAndOffsetPathToNode(z, modifiedAst, nodeToEdit)
}
@ -108,9 +112,9 @@ export function addRotate({
ast: Node<Program>
artifactGraph: ArtifactGraph
objects: Selections
roll: KclCommandValue
pitch: KclCommandValue
yaw: KclCommandValue
roll?: KclCommandValue
pitch?: KclCommandValue
yaw?: KclCommandValue
global?: boolean
nodeToEdit?: PathToNode
}): Error | { modifiedAst: Node<Program>; pathToNode: PathToNode } {
@ -130,27 +134,32 @@ export function addRotate({
return variableExpressions
}
// Extra labeled args expression
const rollExpr = roll ? [createLabeledArg('roll', valueOrVariable(roll))] : []
const pitchExpr = pitch
? [createLabeledArg('pitch', valueOrVariable(pitch))]
: []
const yawExpr = yaw ? [createLabeledArg('yaw', valueOrVariable(yaw))] : []
const globalExpr = global
? [createLabeledArg('global', createLiteral(global))]
: []
const objectsExpr = createVariableExpressionsArray(variableExpressions.exprs)
const call = createCallExpressionStdLibKw('rotate', objectsExpr, [
createLabeledArg('roll', valueOrVariable(roll)),
createLabeledArg('pitch', valueOrVariable(pitch)),
createLabeledArg('yaw', valueOrVariable(yaw)),
...rollExpr,
...pitchExpr,
...yawExpr,
...globalExpr,
])
// Insert variables for labeled arguments if provided
if ('variableName' in roll && roll.variableName) {
if (roll && 'variableName' in roll && roll.variableName) {
insertVariableAndOffsetPathToNode(roll, modifiedAst, nodeToEdit)
}
if ('variableName' in roll && roll.variableName) {
insertVariableAndOffsetPathToNode(roll, modifiedAst, nodeToEdit)
if (pitch && 'variableName' in pitch && pitch.variableName) {
insertVariableAndOffsetPathToNode(pitch, modifiedAst, nodeToEdit)
}
if ('variableName' in roll && roll.variableName) {
insertVariableAndOffsetPathToNode(roll, modifiedAst, nodeToEdit)
if (yaw && 'variableName' in yaw && yaw.variableName) {
insertVariableAndOffsetPathToNode(yaw, modifiedAst, nodeToEdit)
}
// 3. If edit, we assign the new function call declaration to the existing node,
@ -180,63 +189,23 @@ export function addScale({
ast: Node<Program>
artifactGraph: ArtifactGraph
objects: Selections
x: KclCommandValue
y: KclCommandValue
z: KclCommandValue
x?: KclCommandValue
y?: KclCommandValue
z?: KclCommandValue
global?: boolean
nodeToEdit?: PathToNode
}): Error | { modifiedAst: Node<Program>; pathToNode: PathToNode } {
// 1. Clone the ast so we can edit it
const modifiedAst = structuredClone(ast)
// 2. Prepare unlabeled and labeled arguments
// Map the sketches selection into a list of kcl expressions to be passed as unlabelled argument
const variableExpressions = getVariableExprsFromSelection(
return addTranslate({
ast,
artifactGraph,
objects,
modifiedAst,
x,
y,
z,
global,
nodeToEdit,
true,
artifactGraph
)
if (err(variableExpressions)) {
return variableExpressions
}
// Extra labeled args expression
const globalExpr = global
? [createLabeledArg('global', createLiteral(global))]
: []
const objectsExpr = createVariableExpressionsArray(variableExpressions.exprs)
const call = createCallExpressionStdLibKw('scale', objectsExpr, [
createLabeledArg('x', valueOrVariable(x)),
createLabeledArg('y', valueOrVariable(y)),
createLabeledArg('z', valueOrVariable(z)),
...globalExpr,
])
// Insert variables for labeled arguments if provided
if ('variableName' in x && x.variableName) {
insertVariableAndOffsetPathToNode(x, modifiedAst, nodeToEdit)
}
if ('variableName' in y && y.variableName) {
insertVariableAndOffsetPathToNode(y, modifiedAst, nodeToEdit)
}
if ('variableName' in z && z.variableName) {
insertVariableAndOffsetPathToNode(z, modifiedAst, nodeToEdit)
}
// 3. If edit, we assign the new function call declaration to the existing node,
// otherwise just push to the end
const lastPath = variableExpressions.paths.pop() // TODO: check if this is correct
const pathToNode = setCallInAst(modifiedAst, call, nodeToEdit, lastPath)
if (err(pathToNode)) {
return pathToNode
}
return {
modifiedAst,
pathToNode,
}
callName: 'scale',
})
}
export function retrievePathToNodeFromTransformSelection(

View File

@ -187,25 +187,25 @@ export type ModelingCommandSchema = {
Translate: {
nodeToEdit?: PathToNode
objects: Selections
x: KclCommandValue
y: KclCommandValue
z: KclCommandValue
x?: KclCommandValue
y?: KclCommandValue
z?: KclCommandValue
global?: boolean
}
Rotate: {
nodeToEdit?: PathToNode
objects: Selections
roll: KclCommandValue
pitch: KclCommandValue
yaw: KclCommandValue
roll?: KclCommandValue
pitch?: KclCommandValue
yaw?: KclCommandValue
global?: boolean
}
Scale: {
nodeToEdit?: PathToNode
objects: Selections
x: KclCommandValue
y: KclCommandValue
z: KclCommandValue
x?: KclCommandValue
y?: KclCommandValue
z?: KclCommandValue
global?: boolean
}
Clone: {

View File

@ -1524,28 +1524,22 @@ export async function enterAppearanceFlow({
)
}
async function prepareToEditTranslate({
operation,
artifact,
}: EnterEditFlowProps) {
async function prepareToEditTranslate({ operation }: EnterEditFlowProps) {
const baseCommand = {
name: 'Translate',
groupId: 'modeling',
}
// const isModuleImport = operation.type === 'GroupBegin'
// const isSupportedStdLibCall =
// operation.type === 'StdLibCall' &&
// stdLibMap[operation.name]?.supportsTransform
// if (!isModuleImport && !isSupportedStdLibCall) {
// return {
// reason: 'Unsupported operation type. Please edit in the code editor.',
// }
// }
if (operation.type !== 'StdLibCall') {
return { reason: 'Wrong operation type' }
const isModuleImport = operation.type === 'GroupBegin'
const isSupportedStdLibCall =
operation.type === 'StdLibCall' &&
stdLibMap[operation.name]?.supportsTransform
if (!isModuleImport && !isSupportedStdLibCall) {
return {
reason: 'Unsupported operation type. Please edit in the code editor.',
}
}
// 1. Map the unlabeled arguments to solid2d selections
// 1. Map the unlabeled arguments to selections
const objects = getObjectSelectionsFromOperation(
operation,
kclManager.artifactGraph
@ -1555,44 +1549,57 @@ async function prepareToEditTranslate({
}
// 2. Convert the x y z arguments from a string to a KCL expression
const x = await stringToKclExpression(
codeManager.code.slice(
operation.labeledArgs.x?.sourceRange[0],
operation.labeledArgs.x?.sourceRange[1]
)
)
if (err(x) || 'errors' in x) {
return { reason: "Couldn't retrieve x argument" }
}
const y = await stringToKclExpression(
codeManager.code.slice(
operation.labeledArgs.y?.sourceRange[0],
operation.labeledArgs.y?.sourceRange[1]
)
)
if (err(y) || 'errors' in y) {
return { reason: "Couldn't retrieve y argument" }
}
const z = await stringToKclExpression(
codeManager.code.slice(
operation.labeledArgs.z?.sourceRange[0],
operation.labeledArgs.z?.sourceRange[1]
)
)
if (err(z) || 'errors' in z) {
return { reason: "Couldn't retrieve z argument" }
}
// symmetric argument from a string to boolean
let x: KclCommandValue | undefined = undefined
let y: KclCommandValue | undefined = undefined
let z: KclCommandValue | undefined = undefined
let global: boolean | undefined
if ('global' in operation.labeledArgs && operation.labeledArgs.global) {
global =
codeManager.code.slice(
operation.labeledArgs.global.sourceRange[0],
operation.labeledArgs.global.sourceRange[1]
) === 'true'
if (isSupportedStdLibCall) {
if (operation.labeledArgs.x) {
const result = await stringToKclExpression(
codeManager.code.slice(
operation.labeledArgs.x.sourceRange[0],
operation.labeledArgs.x.sourceRange[1]
)
)
if (err(result) || 'errors' in result) {
return { reason: "Couldn't retrieve x argument" }
}
x = result
}
if (operation.labeledArgs.y) {
const result = await stringToKclExpression(
codeManager.code.slice(
operation.labeledArgs.y.sourceRange[0],
operation.labeledArgs.y.sourceRange[1]
)
)
if (err(result) || 'errors' in result) {
return { reason: "Couldn't retrieve y argument" }
}
y = result
}
if (operation.labeledArgs.z) {
const result = await stringToKclExpression(
codeManager.code.slice(
operation.labeledArgs.z.sourceRange[0],
operation.labeledArgs.z.sourceRange[1]
)
)
if (err(result) || 'errors' in result) {
return { reason: "Couldn't retrieve z argument" }
}
z = result
}
if (operation.labeledArgs.global) {
global =
codeManager.code.slice(
operation.labeledArgs.global.sourceRange[0],
operation.labeledArgs.global.sourceRange[1]
) === 'true'
}
}
// 3. Assemble the default argument values for the command,
@ -1641,49 +1648,80 @@ async function prepareToEditScale({ operation }: EnterEditFlowProps) {
}
}
const nodeToEdit = pathToNodeFromRustNodePath(operation.nodePath)
let x: KclExpression | undefined = undefined
let y: KclExpression | undefined = undefined
let z: KclExpression | undefined = undefined
let global: boolean | undefined
const pipeLookupFromOperation = getNodeFromPath<PipeExpression>(
kclManager.ast,
nodeToEdit,
'PipeExpression'
// 1. Map the unlabeled arguments to selections
const objects = getObjectSelectionsFromOperation(
operation,
kclManager.artifactGraph
)
let pipe: PipeExpression | undefined
const ast = kclManager.ast
if (
err(pipeLookupFromOperation) ||
pipeLookupFromOperation.node.type !== 'PipeExpression'
) {
// Look for the last pipe with the import alias and a call to scale
const pipes = findPipesWithImportAlias(ast, nodeToEdit, 'scale')
pipe = pipes.at(-1)?.expression
} else {
pipe = pipeLookupFromOperation.node
if (err(objects)) {
return { reason: "Couldn't retrieve objects" }
}
if (pipe) {
const scale = pipe.body.find(
(n) => n.type === 'CallExpressionKw' && n.callee.name.name === 'scale'
)
if (scale?.type === 'CallExpressionKw') {
x = await retrieveArgFromPipedCallExpression(scale, 'x')
y = await retrieveArgFromPipedCallExpression(scale, 'y')
z = await retrieveArgFromPipedCallExpression(scale, 'z')
// optional global argument
const result = await retrieveArgFromPipedCallExpression(scale, 'global')
if (result) {
global = result.valueText === 'true'
// 2. Convert the x y z arguments from a string to a KCL expression
let x: KclCommandValue | undefined = undefined
let y: KclCommandValue | undefined = undefined
let z: KclCommandValue | undefined = undefined
let global: boolean | undefined
if (isSupportedStdLibCall) {
if (operation.labeledArgs.x) {
const result = await stringToKclExpression(
codeManager.code.slice(
operation.labeledArgs.x.sourceRange[0],
operation.labeledArgs.x.sourceRange[1]
)
)
if (err(result) || 'errors' in result) {
return { reason: "Couldn't retrieve x argument" }
}
x = result
}
if (operation.labeledArgs.y) {
const result = await stringToKclExpression(
codeManager.code.slice(
operation.labeledArgs.y.sourceRange[0],
operation.labeledArgs.y.sourceRange[1]
)
)
if (err(result) || 'errors' in result) {
return { reason: "Couldn't retrieve y argument" }
}
y = result
}
if (operation.labeledArgs.z) {
const result = await stringToKclExpression(
codeManager.code.slice(
operation.labeledArgs.z.sourceRange[0],
operation.labeledArgs.z.sourceRange[1]
)
)
if (err(result) || 'errors' in result) {
return { reason: "Couldn't retrieve z argument" }
}
z = result
}
if (operation.labeledArgs.global) {
global =
codeManager.code.slice(
operation.labeledArgs.global.sourceRange[0],
operation.labeledArgs.global.sourceRange[1]
) === 'true'
}
}
// Won't be used since we provide nodeToEdit
const selection: Selections = { graphSelections: [], otherSelections: [] }
const argDefaultValues = { nodeToEdit, selection, x, y, z, global }
// 3. Assemble the default argument values for the command,
// with `nodeToEdit` set, which will let the actor know
// to edit the node that corresponds to the StdLibCall.
const argDefaultValues: ModelingCommandSchema['Scale'] = {
objects,
x,
y,
z,
global,
nodeToEdit: pathToNodeFromRustNodePath(operation.nodePath),
}
return {
...baseCommand,
argDefaultValues,
@ -1719,42 +1757,80 @@ async function prepareToEditRotate({ operation }: EnterEditFlowProps) {
}
}
const nodeToEdit = pathToNodeFromRustNodePath(operation.nodePath)
let roll: KclExpression | undefined = undefined
let pitch: KclExpression | undefined = undefined
let yaw: KclExpression | undefined = undefined
const pipeLookupFromOperation = getNodeFromPath<PipeExpression>(
kclManager.ast,
nodeToEdit,
'PipeExpression'
// 1. Map the unlabeled arguments to selections
const objects = getObjectSelectionsFromOperation(
operation,
kclManager.artifactGraph
)
let pipe: PipeExpression | undefined
const ast = kclManager.ast
if (
err(pipeLookupFromOperation) ||
pipeLookupFromOperation.node.type !== 'PipeExpression'
) {
// Look for the last pipe with the import alias and a call to rotate
const pipes = findPipesWithImportAlias(ast, nodeToEdit, 'rotate')
pipe = pipes.at(-1)?.expression
} else {
pipe = pipeLookupFromOperation.node
if (err(objects)) {
return { reason: "Couldn't retrieve objects" }
}
if (pipe) {
const rotate = pipe.body.find(
(n) => n.type === 'CallExpressionKw' && n.callee.name.name === 'rotate'
)
if (rotate?.type === 'CallExpressionKw') {
roll = await retrieveArgFromPipedCallExpression(rotate, 'roll')
pitch = await retrieveArgFromPipedCallExpression(rotate, 'pitch')
yaw = await retrieveArgFromPipedCallExpression(rotate, 'yaw')
// 2. Convert the x y z arguments from a string to a KCL expression
let roll: KclCommandValue | undefined = undefined
let pitch: KclCommandValue | undefined = undefined
let yaw: KclCommandValue | undefined = undefined
let global: boolean | undefined
if (isSupportedStdLibCall) {
if (operation.labeledArgs.roll) {
const result = await stringToKclExpression(
codeManager.code.slice(
operation.labeledArgs.roll.sourceRange[0],
operation.labeledArgs.roll.sourceRange[1]
)
)
if (err(result) || 'errors' in result) {
return { reason: "Couldn't retrieve roll argument" }
}
roll = result
}
if (operation.labeledArgs.pitch) {
const result = await stringToKclExpression(
codeManager.code.slice(
operation.labeledArgs.pitch.sourceRange[0],
operation.labeledArgs.pitch.sourceRange[1]
)
)
if (err(result) || 'errors' in result) {
return { reason: "Couldn't retrieve pitch argument" }
}
pitch = result
}
if (operation.labeledArgs.yaw) {
const result = await stringToKclExpression(
codeManager.code.slice(
operation.labeledArgs.yaw.sourceRange[0],
operation.labeledArgs.yaw.sourceRange[1]
)
)
if (err(result) || 'errors' in result) {
return { reason: "Couldn't retrieve yaw argument" }
}
yaw = result
}
if (operation.labeledArgs.global) {
global =
codeManager.code.slice(
operation.labeledArgs.global.sourceRange[0],
operation.labeledArgs.global.sourceRange[1]
) === 'true'
}
}
// Won't be used since we provide nodeToEdit
const selection: Selections = { graphSelections: [], otherSelections: [] }
const argDefaultValues = { nodeToEdit, selection, roll, pitch, yaw }
// 3. Assemble the default argument values for the command,
// with `nodeToEdit` set, which will let the actor know
// to edit the node that corresponds to the StdLibCall.
const argDefaultValues: ModelingCommandSchema['Rotate'] = {
objects,
roll,
pitch,
yaw,
global,
nodeToEdit: pathToNodeFromRustNodePath(operation.nodePath),
}
return {
...baseCommand,
argDefaultValues,

View File

@ -280,6 +280,7 @@ export const featureTreeMachine = setup({
targetSourceRange: undefined,
}),
sendSelectionEvent: () => {},
sendTranslateCommand: () => {},
openCodePane: () => {},
scrollToError: () => {},
},
@ -312,8 +313,12 @@ export const featureTreeMachine = setup({
},
enterTranslateFlow: {
target: 'enteringTranslateFlow',
actions: ['saveTargetSourceRange', 'saveCurrentOperation'],
target: 'enteringTranslateFlow2',
actions: [
'saveTargetSourceRange',
'saveCurrentOperation',
'sendSelectionEvent',
],
},
enterRotateFlow: {
@ -388,6 +393,25 @@ export const featureTreeMachine = setup({
initial: 'selecting',
},
enteringTranslateFlow2: {
states: {
enteringTranslateFlow2: {
on: {
selected: 'done',
},
entry: 'sendTranslateCommand',
},
done: {
always: '#featureTree.idle',
entry: 'clearContext',
},
},
initial: 'enteringTranslateFlow2',
},
enteringEditFlow: {
states: {
selecting: {