Compare commits

...

34 Commits

Author SHA1 Message Date
3ae4b045ad Merge branch 'pierremtb/issue7657-Allow-all-sweeps-to-work-on-variable-less-profiles' into pierremtb/issue7615-Expose-global-optional-arg-for-all-point-and-click-transforms 2025-07-04 11:33:53 -04:00
cd672d52f6 Merge branch 'main' into pierremtb/issue7657-Allow-all-sweeps-to-work-on-variable-less-profiles 2025-07-04 11:33:24 -04:00
30c4f79285 Quick variable break out for clarity 2025-07-04 08:53:56 -04:00
c3c8b727bd Big time clean up and bringing clone on for the ride 2025-07-04 08:51:56 -04:00
65168eb139 Clean up 2025-07-04 07:47:09 -04:00
6a9870b6f3 Clean pu 2025-07-04 07:47:07 -04:00
78f885c3d1 Fixing up edit flows for transforms, and WIP for simpler feature tree action 2025-07-04 07:46:32 -04:00
a40ba06641 WIP edit flows 2025-07-03 19:45:52 -04:00
c035398ad7 Update rotate and scale to match 2025-07-03 19:08:51 -04:00
1387c2fbcb Add support for imports 2025-07-03 17:10:39 -04:00
a9f95f7574 Progress integrating new stuff from sweeps 2025-07-03 16:50:25 -04:00
b2b7ac5bf8 Merge branch 'pierremtb/issue7657-Allow-all-sweeps-to-work-on-variable-less-profiles' into pierremtb/issue7615-Expose-global-optional-arg-for-all-point-and-click-transforms 2025-07-03 15:54:43 -04:00
172e01529c Add basic addLoft and addRevolve tests, will have to see how to distribute coverage 2025-07-03 15:45:32 -04:00
5a4a32c044 Add addSweep test 2025-07-03 15:00:21 -04:00
b955184191 Lint & complete addExtrude tests 2025-07-03 14:31:06 -04:00
d7914219da We going 2025-07-03 13:58:10 -04:00
ead4c1286b Add other test case 2025-07-03 13:41:31 -04:00
a0fe33260e WIP new util function 2025-07-03 12:15:20 -04:00
8955b5fcd3 Merge branch 'main' into pierremtb/issue7657-Allow-all-sweeps-to-work-on-variable-less-profiles 2025-07-03 11:28:57 -04:00
5708b8c64b Add unit test createPathToNodeForLastVariable 2025-07-03 08:50:08 -04:00
5b8284e737 Add unit tests for createVariableExpressionsArray 2025-07-03 07:44:29 -04:00
dd9b0ec5f0 Codespell 2025-07-02 19:38:34 -04:00
c467568ee4 Add unit tests for getVariableExprsFromSelection 2025-07-02 19:26:28 -04:00
cb976ec31b Fix circ dep 2025-07-02 17:42:40 -04:00
cc9eb65456 Fix test 2025-07-02 17:00:17 -04:00
298583181b Merge branch 'pierremtb/issue7657-Allow-all-sweeps-to-work-on-variable-less-profiles' into pierremtb/issue7615-Expose-global-optional-arg-for-all-point-and-click-transforms 2025-07-02 16:47:11 -04:00
a589f56e73 Merge branch 'main' into pierremtb/issue7657-Allow-all-sweeps-to-work-on-variable-less-profiles 2025-07-02 12:42:38 -04:00
e668cb55f9 WIP 2025-07-01 19:49:05 -04:00
9942a65612 WIP 2025-07-01 19:38:47 -04:00
187925ff21 Merge branch 'pierremtb/issue7657-Allow-all-sweeps-to-work-on-variable-less-profiles' into pierremtb/issue7615-Expose-global-optional-arg-for-all-point-and-click-transforms 2025-07-01 17:22:37 -04:00
4c1564e2b0 WIP: Allow all sweeps to work on variable-less profiles
Fixes #7657
2025-07-01 16:41:14 -04:00
50ac0244fd WIP transforms like sweep 2025-07-01 15:46:36 -04:00
8d7858978f Merge branch 'main' into pierremtb/issue7615-Expose-global-optional-arg-for-all-point-and-click-transforms 2025-07-01 13:53:59 -04:00
74d73ece7b WIP: global optional arg for all point-and-click transforms and add Scale
Closes #7615 and #7634
2025-06-27 15:26:52 -04:00
16 changed files with 1854 additions and 1002 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,30 @@ export const FeatureTreePane = () => {
scrollToError: () => {
editorManager.scrollToFirstErrorDiagnosticIfExists()
},
sendTranslateCommand: () => {
commandBarActor.send({
type: 'Find and select command',
data: { name: 'Translate', groupId: 'modeling' },
})
},
sendRotateCommand: () => {
commandBarActor.send({
type: 'Find and select command',
data: { name: 'Rotate', groupId: 'modeling' },
})
},
sendScaleCommand: () => {
commandBarActor.send({
type: 'Find and select command',
data: { name: 'Scale', groupId: 'modeling' },
})
},
sendCloneCommand: () => {
commandBarActor.send({
type: 'Find and select command',
data: { name: 'Clone', groupId: 'modeling' },
})
},
sendSelectionEvent: ({ context }) => {
if (!context.targetSourceRange) {
return
@ -354,10 +383,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' ||
@ -409,6 +434,18 @@ const OperationItem = (props: {
}
}
function enterScaleFlow() {
if (props.item.type === 'StdLibCall' || props.item.type === 'GroupBegin') {
props.send({
type: 'enterScaleFlow',
data: {
targetSourceRange: sourceRangeFromRust(props.item.sourceRange),
currentOperation: props.item,
},
})
}
}
function enterCloneFlow() {
if (props.item.type === 'StdLibCall' || props.item.type === 'GroupBegin') {
props.send({
@ -515,7 +552,7 @@ const OperationItem = (props: {
!stdLibMap[props.item.name]?.supportsTransform
}
>
Set translate
Translate
</ContextMenuItem>,
<ContextMenuItem
onClick={enterRotateFlow}
@ -525,7 +562,17 @@ const OperationItem = (props: {
!stdLibMap[props.item.name]?.supportsTransform
}
>
Set rotate
Rotate
</ContextMenuItem>,
<ContextMenuItem
onClick={enterScaleFlow}
data-testid="context-menu-set-scale"
disabled={
props.item.type !== 'GroupBegin' &&
!stdLibMap[props.item.name]?.supportsTransform
}
>
Scale
</ContextMenuItem>,
<ContextMenuItem
onClick={enterCloneFlow}

View File

@ -2,9 +2,12 @@ import type { Node } from '@rust/kcl-lib/bindings/Node'
import {
createArrayExpression,
createCallExpressionStdLibKw,
createIdentifier,
createLabeledArg,
createLiteral,
createLiteralMaybeSuffix,
createLocalName,
createObjectExpression,
createPipeExpression,
createPipeSubstitution,
@ -14,12 +17,19 @@ import {
} from '@src/lang/create'
import {
addSketchTo,
createPathToNodeForLastVariable,
createVariableExpressionsArray,
deleteSegmentFromPipeExpression,
moveValueIntoNewVariable,
setCallInAst,
sketchOnExtrudedFace,
splitPipedProfile,
} from '@src/lang/modifyAst'
import { findUsesOfTagInPipe } from '@src/lang/queryAst'
import {
findUsesOfTagInPipe,
getNodeFromPath,
getVariableExprsFromSelection,
} from '@src/lang/queryAst'
import { getNodePathFromSourceRange } from '@src/lang/queryAstNodePathUtils'
import type { Artifact } from '@src/lang/std/artifactGraph'
import { codeRefFromRange } from '@src/lang/std/artifactGraph'
@ -31,6 +41,7 @@ import { enginelessExecutor } from '@src/lib/testHelpers'
import { err } from '@src/lib/trap'
import { deleteFromSelection } from '@src/lang/modifyAst/deleteFromSelection'
import { assertNotErr } from '@src/unitTestUtils'
import type { Selections } from '@src/lib/selections'
beforeAll(async () => {
await initPromise
@ -917,3 +928,212 @@ extrude001 = extrude(part001, length = 5)
expect(result instanceof Error).toBe(true)
})
})
describe('Testing createVariableExpressionsArray', () => {
it('should return null for any number of pipe substitutions', () => {
const onePipe = [createPipeSubstitution()]
const twoPipes = [createPipeSubstitution(), createPipeSubstitution()]
const threePipes = [
createPipeSubstitution(),
createPipeSubstitution(),
createPipeSubstitution(),
]
expect(createVariableExpressionsArray(onePipe)).toBeNull()
expect(createVariableExpressionsArray(twoPipes)).toBeNull()
expect(createVariableExpressionsArray(threePipes)).toBeNull()
})
it('should create a variable expressions for one variable', () => {
const oneVariableName = [createLocalName('var1')]
const expr = createVariableExpressionsArray(oneVariableName)
if (expr?.type !== 'Name') {
throw new Error(`Expected Literal type, got ${expr?.type}`)
}
expect(expr.name.name).toBe('var1')
})
it('should create an array of variable expressions for two variables', () => {
const twoVariableNames = [createLocalName('var1'), createLocalName('var2')]
const exprs = createVariableExpressionsArray(twoVariableNames)
if (exprs?.type !== 'ArrayExpression') {
throw new Error('Expected ArrayExpression type')
}
expect(exprs.elements).toHaveLength(2)
if (
exprs.elements[0].type !== 'Name' ||
exprs.elements[1].type !== 'Name'
) {
throw new Error(
`Expected elements to be of type Name, got ${exprs.elements[0].type} and ${exprs.elements[1].type}`
)
}
expect(exprs.elements[0].name.name).toBe('var1')
expect(exprs.elements[1].name.name).toBe('var2')
})
// This would catch the issue at https://github.com/KittyCAD/modeling-app/issues/7669
// TODO: fix function to get this test to pass
// it('should create one expr if the array of variable names are the same', () => {
// const twoVariableNames = [createLocalName('var1'), createLocalName('var1')]
// const expr = createVariableExpressionsArray(twoVariableNames)
// if (expr?.type !== 'Name') {
// throw new Error(`Expected Literal type, got ${expr?.type}`)
// }
// expect(expr.name.name).toBe('var1')
// })
it('should create an array of variable expressions for one variable and a pipe', () => {
const oneVarOnePipe = [createPipeSubstitution(), createLocalName('var1')]
const exprs = createVariableExpressionsArray(oneVarOnePipe)
if (exprs?.type !== 'ArrayExpression') {
throw new Error('Expected ArrayExpression type')
}
expect(exprs.elements).toHaveLength(2)
expect(exprs.elements[0].type).toBe('PipeSubstitution')
if (exprs.elements[1].type !== 'Name') {
throw new Error(
`Expected elements[1] to be of type Name, got ${exprs.elements[1].type}`
)
}
expect(exprs.elements[1].name.name).toBe('var1')
})
})
describe('Testing createPathToNodeForLastVariable', () => {
it('should create a path to the last variable in the array', () => {
const circleProfileInVar = `sketch001 = startSketchOn(XY)
profile001 = circle(sketch001, center = [0, 0], radius = 1)
extrude001 = extrude(profile001, length = 5)
`
const ast = assertParse(circleProfileInVar)
const path = createPathToNodeForLastVariable(ast, false)
expect(path.length).toEqual(4)
// Verify we can get the right node
const node = getNodeFromPath<any>(ast, path)
if (err(node)) {
throw node
}
// With the expected range
const startOfExtrudeIndex = circleProfileInVar.indexOf('extrude(')
expect(node.node.start).toEqual(startOfExtrudeIndex)
expect(node.node.end).toEqual(circleProfileInVar.length - 1)
})
it('should create a path to the first kwarg in the last expression', () => {
const circleProfileInVar = `sketch001 = startSketchOn(XY)
profile001 = circle(sketch001, center = [0, 0], radius = 1)
extrude001 = extrude(profile001, length = 123)
`
const ast = assertParse(circleProfileInVar)
const path = createPathToNodeForLastVariable(ast, true)
expect(path.length).toEqual(7)
// Verify we can get the right node
const node = getNodeFromPath<any>(ast, path)
if (err(node)) {
throw node
}
// With the expected range
const startOfKwargIndex = circleProfileInVar.indexOf('123')
expect(node.node.start).toEqual(startOfKwargIndex)
expect(node.node.end).toEqual(startOfKwargIndex + 3)
})
})
describe('Testing setCallInAst', () => {
it('should push an extrude call with variable on variable profile', () => {
const code = `sketch001 = startSketchOn(XY)
profile001 = circle(sketch001, center = [0, 0], radius = 1)
`
const ast = assertParse(code)
const exprs = createVariableExpressionsArray([
createLocalName('profile001'),
])
const call = createCallExpressionStdLibKw('extrude', exprs, [
createLabeledArg('length', createLiteral(5)),
])
const pathToNode = setCallInAst(ast, call)
if (err(pathToNode)) {
throw pathToNode
}
const newCode = recast(ast)
expect(newCode).toContain(code)
expect(newCode).toContain(`extrude001 = extrude(profile001, length = 5)`)
})
it('should push an extrude call in pipe is selection was in variable-less pipe', async () => {
const code = `startSketchOn(XY)
|> circle(center = [0, 0], radius = 1)
`
const ast = assertParse(code)
const { artifactGraph } = await enginelessExecutor(ast)
const artifact = artifactGraph.values().find((a) => a.type === 'path')
if (!artifact) {
throw new Error('Artifact not found in the graph')
}
const selections: Selections = {
graphSelections: [
{
codeRef: artifact.codeRef,
artifact,
},
],
otherSelections: [],
}
const variableExprs = getVariableExprsFromSelection(selections, ast)
if (err(variableExprs)) throw variableExprs
const exprs = createVariableExpressionsArray(variableExprs.exprs)
const call = createCallExpressionStdLibKw('extrude', exprs, [
createLabeledArg('length', createLiteral(5)),
])
const lastPathToNode = variableExprs.paths.pop()
const pathToNode = setCallInAst(ast, call, undefined, lastPathToNode)
if (err(pathToNode)) {
throw pathToNode
}
const newCode = recast(ast)
expect(newCode).toContain(code)
expect(newCode).toContain(`|> extrude(length = 5)`)
})
it('should push an extrude call with variable if selection was in variable pipe', async () => {
const code = `profile001 = startSketchOn(XY)
|> circle(center = [0, 0], radius = 1)
`
const ast = assertParse(code)
const { artifactGraph } = await enginelessExecutor(ast)
const artifact = artifactGraph.values().find((a) => a.type === 'path')
if (!artifact) {
throw new Error('Artifact not found in the graph')
}
const selections: Selections = {
graphSelections: [
{
codeRef: artifact.codeRef,
artifact,
},
],
otherSelections: [],
}
const variableExprs = getVariableExprsFromSelection(selections, ast)
if (err(variableExprs)) throw variableExprs
const exprs = createVariableExpressionsArray(variableExprs.exprs)
const call = createCallExpressionStdLibKw('extrude', exprs, [
createLabeledArg('length', createLiteral(5)),
])
const lastPathToNode = variableExprs.paths.pop()
const pathToNode = setCallInAst(ast, call, undefined, lastPathToNode)
if (err(pathToNode)) {
throw pathToNode
}
const newCode = recast(ast)
expect(newCode).toContain(code)
expect(newCode).toContain(`extrude001 = extrude(profile001, length = 5)`)
})
})

View File

@ -655,39 +655,6 @@ export function addHelix({
}
}
/**
* Add clone statement
*/
export function addClone({
ast,
geometryName,
variableName,
}: {
ast: Node<Program>
geometryName: string
variableName: string
}): { modifiedAst: Node<Program>; pathToNode: PathToNode } {
const modifiedAst = structuredClone(ast)
const variable = createVariableDeclaration(
variableName,
createCallExpressionStdLibKw('clone', createLocalName(geometryName), [])
)
modifiedAst.body.push(variable)
const insertAt = modifiedAst.body.length - 1
const pathToNode: PathToNode = [
['body', ''],
[insertAt, 'index'],
['declaration', 'VariableDeclaration'],
['init', 'VariableDeclarator'],
]
return {
modifiedAst,
pathToNode,
}
}
/**
* Return a modified clone of an AST with a named constant inserted into the body
*/
@ -1209,3 +1176,84 @@ export function insertVariableAndOffsetPathToNode(
}
}
}
// Create an array expression for variables,
// or keep it null if all are PipeSubstitutions
export function createVariableExpressionsArray(sketches: Expr[]): Expr | null {
let exprs: Expr | null = null
if (sketches.every((s) => s.type === 'PipeSubstitution')) {
// Keeping null so we don't even put it the % sign
} else if (sketches.length === 1) {
exprs = sketches[0]
} else {
exprs = createArrayExpression(sketches)
}
return exprs
}
// Create a path to node to the last variable declaroator of an ast
// Optionally, can point to the first kwarg of the CallExpressionKw
export function createPathToNodeForLastVariable(
ast: Node<Program>,
toFirstKwarg = true
): PathToNode {
const argIndex = 0 // first kwarg for all sweeps here
const pathToCall: PathToNode = [
['body', ''],
[ast.body.length - 1, 'index'],
['declaration', 'VariableDeclaration'],
['init', 'VariableDeclarator'],
]
if (toFirstKwarg) {
pathToCall.push(
['arguments', 'CallExpressionKw'],
[argIndex, ARG_INDEX_FIELD],
['arg', LABELED_ARG_FIELD]
)
}
return pathToCall
}
export function setCallInAst(
ast: Node<Program>,
call: Node<CallExpressionKw>,
nodeToEdit?: PathToNode,
lastPathToNode?: PathToNode,
toFirstKwarg?: boolean
): Error | PathToNode {
let pathToNode: PathToNode | undefined
if (nodeToEdit) {
const result = getNodeFromPath<CallExpressionKw>(
ast,
nodeToEdit,
'CallExpressionKw'
)
if (err(result)) {
return result
}
Object.assign(result.node, call)
pathToNode = nodeToEdit
} else {
if (!call.unlabeled && lastPathToNode) {
const pipe = getNodeFromPath<PipeExpression>(
ast,
lastPathToNode,
'PipeExpression'
)
if (err(pipe)) {
return pipe
}
pipe.node.body.push(call)
pathToNode = lastPathToNode
} else {
const name = findUniqueName(ast, call.callee.name.name)
const declaration = createVariableDeclaration(name, call)
ast.body.push(declaration)
pathToNode = createPathToNodeForLastVariable(ast, toFirstKwarg)
}
}
return pathToNode
}

View File

@ -1,192 +0,0 @@
import type { Node } from '@rust/kcl-lib/bindings/Node'
import {
createCallExpressionStdLibKw,
createExpressionStatement,
createLabeledArg,
createLocalName,
createPipeExpression,
} from '@src/lang/create'
import { getNodeFromPath } from '@src/lang/queryAst'
import type {
ArtifactGraph,
CallExpressionKw,
Expr,
ExpressionStatement,
PathToNode,
PipeExpression,
Program,
VariableDeclarator,
} from '@src/lang/wasm'
import { err } from '@src/lib/trap'
import {
findAllChildrenAndOrderByPlaceInCode,
getLastVariable,
} from '@src/lang/modifyAst/boolean'
import type { Selections } from '@src/lib/selections'
export function setTranslate({
modifiedAst,
pathToNode,
x,
y,
z,
}: {
modifiedAst: Node<Program>
pathToNode: PathToNode
x: Expr
y: Expr
z: Expr
}): Error | { modifiedAst: Node<Program>; pathToNode: PathToNode } {
const noPercentSign = null
const call = createCallExpressionStdLibKw('translate', noPercentSign, [
createLabeledArg('x', x),
createLabeledArg('y', y),
createLabeledArg('z', z),
])
const potentialPipe = getNodeFromPath<PipeExpression>(
modifiedAst,
pathToNode,
['PipeExpression']
)
if (!err(potentialPipe) && potentialPipe.node.type === 'PipeExpression') {
setTransformInPipe(potentialPipe.node, call)
} else {
const error = createPipeWithTransform(modifiedAst, pathToNode, call)
if (err(error)) {
return error
}
}
return {
modifiedAst,
pathToNode,
}
}
export function setRotate({
modifiedAst,
pathToNode,
roll,
pitch,
yaw,
}: {
modifiedAst: Node<Program>
pathToNode: PathToNode
roll: Expr
pitch: Expr
yaw: Expr
}): Error | { modifiedAst: Node<Program>; pathToNode: PathToNode } {
const noPercentSign = null
const call = createCallExpressionStdLibKw('rotate', noPercentSign, [
createLabeledArg('roll', roll),
createLabeledArg('pitch', pitch),
createLabeledArg('yaw', yaw),
])
const potentialPipe = getNodeFromPath<PipeExpression>(
modifiedAst,
pathToNode,
['PipeExpression']
)
if (!err(potentialPipe) && potentialPipe.node.type === 'PipeExpression') {
setTransformInPipe(potentialPipe.node, call)
} else {
const error = createPipeWithTransform(modifiedAst, pathToNode, call)
if (err(error)) {
return error
}
}
return {
modifiedAst,
pathToNode,
}
}
function setTransformInPipe(
expression: PipeExpression,
call: Node<CallExpressionKw>
) {
const existingIndex = expression.body.findIndex(
(v) =>
v.type === 'CallExpressionKw' &&
v.callee.type === 'Name' &&
v.callee.name.name === call.callee.name.name
)
if (existingIndex > -1) {
expression.body[existingIndex] = call
} else {
expression.body.push(call)
}
}
function createPipeWithTransform(
modifiedAst: Node<Program>,
pathToNode: PathToNode,
call: Node<CallExpressionKw>
) {
const existingCall = getNodeFromPath<
VariableDeclarator | ExpressionStatement
>(modifiedAst, pathToNode, ['VariableDeclarator', 'ExpressionStatement'])
if (err(existingCall)) {
return new Error('Unsupported operation type.')
}
if (existingCall.node.type === 'ExpressionStatement') {
existingCall.node.expression = createPipeExpression([
existingCall.node.expression,
call,
])
} else if (existingCall.node.type === 'VariableDeclarator') {
existingCall.node.init = createPipeExpression([
existingCall.node.init,
call,
])
} else {
return new Error('Unsupported operation type.')
}
}
export function insertExpressionNode(ast: Node<Program>, alias: string) {
const expression = createExpressionStatement(createLocalName(alias))
ast.body.push(expression)
const pathToNode: PathToNode = [
['body', ''],
[ast.body.length - 1, 'index'],
['expression', 'Name'],
]
return pathToNode
}
export function retrievePathToNodeFromTransformSelection(
selection: Selections,
artifactGraph: ArtifactGraph,
ast: Node<Program>
): PathToNode | Error {
const error = new Error(
"Couldn't retrieve selection. If you're trying to transform an import, use the feature tree."
)
const hasPathToNode = !!selection.graphSelections[0].codeRef.pathToNode.length
const artifact = selection.graphSelections[0].artifact
let pathToNode: PathToNode | undefined
if (hasPathToNode && artifact) {
const children = findAllChildrenAndOrderByPlaceInCode(
artifact,
artifactGraph
)
const variable = getLastVariable(children, ast)
if (!variable) {
return error
}
pathToNode = variable.pathToNode
} else if (hasPathToNode) {
pathToNode = selection.graphSelections[0].codeRef.pathToNode
} else {
return error
}
return pathToNode
}

View File

@ -0,0 +1,252 @@
import {
type Artifact,
assertParse,
type CodeRef,
type Program,
recast,
} from '@src/lang/wasm'
import type { Selection, Selections } from '@src/lib/selections'
import { enginelessExecutor } from '@src/lib/testHelpers'
import { err } from '@src/lib/trap'
import {
addExtrude,
addLoft,
addRevolve,
addSweep,
} from '@src/lang/modifyAst/sweeps'
import { stringToKclExpression } from '@src/lib/kclHelpers'
import type { Node } from '@rust/kcl-lib/bindings/Node'
async function getAstAndArtifactGraph(code: string) {
const ast = assertParse(code)
if (err(ast)) throw ast
const { artifactGraph } = await enginelessExecutor(ast)
return { ast, artifactGraph }
}
function createSelectionFromPathArtifact(
artifacts: (Artifact & { codeRef: CodeRef })[]
): Selections {
const graphSelections = artifacts.map(
(artifact) =>
({
codeRef: artifact.codeRef,
artifact,
}) as Selection
)
return {
graphSelections,
otherSelections: [],
}
}
async function getAstAndSketchSelections(code: string) {
const { ast, artifactGraph } = await getAstAndArtifactGraph(code)
const artifacts = [...artifactGraph.values()].filter((a) => a.type === 'path')
if (artifacts.length === 0) {
throw new Error('Artifact not found in the graph')
}
const sketches = createSelectionFromPathArtifact(artifacts)
return { ast, sketches }
}
async function getKclCommandValue(value: string) {
const result = await stringToKclExpression(value)
if (err(result) || 'errors' in result) {
throw new Error(`Couldn't create kcl expression`)
}
return result
}
async function runNewAstAndCheckForSweep(ast: Node<Program>) {
const { artifactGraph } = await enginelessExecutor(ast)
console.log('artifactGraph', artifactGraph)
const sweepArtifact = artifactGraph.values().find((a) => a.type === 'sweep')
expect(sweepArtifact).toBeDefined()
}
describe('Testing addExtrude', () => {
it('should push a call in pipe if selection was in variable-less pipe', async () => {
const code = `startSketchOn(XY)
|> circle(center = [0, 0], radius = 1)
`
const { ast, sketches } = await getAstAndSketchSelections(code)
const length = await getKclCommandValue('1')
const result = addExtrude({ ast, sketches, length })
if (err(result)) throw result
const newCode = recast(result.modifiedAst)
expect(newCode).toContain(code)
expect(newCode).toContain(`|> extrude(length = 1)`)
await runNewAstAndCheckForSweep(result.modifiedAst)
})
it('should push a call with variable if selection was in variable profile', async () => {
const code = `sketch001 = startSketchOn(XY)
profile001 = circle(sketch001, center = [0, 0], radius = 1)
`
const { ast, sketches } = await getAstAndSketchSelections(code)
const length = await getKclCommandValue('2')
const result = addExtrude({ ast, sketches, length })
if (err(result)) throw result
const newCode = recast(result.modifiedAst)
expect(newCode).toContain(code)
expect(newCode).toContain(`extrude001 = extrude(profile001, length = 2)`)
await runNewAstAndCheckForSweep(result.modifiedAst)
})
it('should push a call with variable if selection was in variable pipe', async () => {
const code = `profile001 = startSketchOn(XY)
|> circle(center = [0, 0], radius = 1)
`
const { ast, sketches } = await getAstAndSketchSelections(code)
const length = await getKclCommandValue('3')
const result = addExtrude({ ast, sketches, length })
if (err(result)) throw result
await runNewAstAndCheckForSweep(result.modifiedAst)
const newCode = recast(result.modifiedAst)
expect(newCode).toContain(code)
expect(newCode).toContain(`extrude001 = extrude(profile001, length = 3)`)
})
it('should push a call with many compatible optional args if asked', async () => {
const code = `sketch001 = startSketchOn(XY)
profile001 = circle(sketch001, center = [0, 0], radius = 1)
`
const { ast, sketches } = await getAstAndSketchSelections(code)
const length = await getKclCommandValue('10')
const bidirectionalLength = await getKclCommandValue('20')
const twistAngle = await getKclCommandValue('30')
const result = addExtrude({
ast,
sketches,
length,
bidirectionalLength,
twistAngle,
})
if (err(result)) throw result
await runNewAstAndCheckForSweep(result.modifiedAst)
const newCode = recast(result.modifiedAst)
expect(newCode).toContain(code)
expect(newCode).toContain(`extrude001 = extrude(
profile001,
length = 10,
bidirectionalLength = 20,
twistAngle = 30,
)`)
})
// TODO: missing edit flow test
// TODO: missing multi-profile test
})
describe('Testing addSweep', () => {
it('should push a call with variable and all compatible optional args', async () => {
const code = `sketch001 = startSketchOn(XY)
profile001 = circle(sketch001, center = [0, 0], radius = 1)
sketch002 = startSketchOn(XZ)
profile002 = startProfile(sketch002, at = [0, 0])
|> xLine(length = -5)
|> tangentialArc(endAbsolute = [-20, 5])
`
const { ast, artifactGraph } = await getAstAndArtifactGraph(code)
const artifact1 = artifactGraph.values().find((a) => a.type === 'path')
const artifact2 = [...artifactGraph.values()].findLast(
(a) => a.type === 'path'
)
if (!artifact1 || !artifact2) {
throw new Error('Artifact not found in the graph')
}
const sketches = createSelectionFromPathArtifact([artifact1])
const path = createSelectionFromPathArtifact([artifact2])
const sectional = true
const relativeTo = 'sketchPlane'
const result = addSweep({
ast,
sketches,
path,
sectional,
relativeTo,
})
if (err(result)) throw result
await runNewAstAndCheckForSweep(result.modifiedAst)
const newCode = recast(result.modifiedAst)
expect(newCode).toContain(code)
expect(newCode).toContain(`sweep001 = sweep(
profile001,
path = profile002,
sectional = true,
relativeTo = 'sketchPlane',
)`)
})
// TODO: missing edit flow test
// TODO: missing multi-profile test
})
describe('Testing addLoft', () => {
it('should push a call with variable and all optional args if asked', async () => {
const code = `sketch001 = startSketchOn(XZ)
profile001 = circle(sketch001, center = [0, 0], radius = 30)
plane001 = offsetPlane(XZ, offset = 50)
sketch002 = startSketchOn(plane001)
profile002 = circle(sketch002, center = [0, 0], radius = 20)
`
const { ast, sketches } = await getAstAndSketchSelections(code)
expect(sketches.graphSelections).toHaveLength(2)
const vDegree = await getKclCommandValue('3')
const result = addLoft({
ast,
sketches,
vDegree,
})
if (err(result)) throw result
const newCode = recast(result.modifiedAst)
expect(newCode).toContain(code)
expect(newCode).toContain(
`loft001 = loft([profile001, profile002], vDegree = 3)`
)
// Don't think we can find the artifact here for loft?
})
// TODO: missing edit flow test
})
describe('Testing addRevolve', () => {
it('should push a call with variable and compatible optional args if asked', async () => {
const code = `sketch001 = startSketchOn(XZ)
profile001 = circle(sketch001, center = [3, 0], radius = 1)
`
const { ast, sketches } = await getAstAndSketchSelections(code)
expect(sketches.graphSelections).toHaveLength(1)
const result = addRevolve({
ast,
sketches,
angle: await getKclCommandValue('1'),
axisOrEdge: 'Axis',
axis: 'X',
edge: undefined,
symmetric: false,
bidirectionalAngle: await getKclCommandValue('2'),
})
if (err(result)) throw result
await runNewAstAndCheckForSweep(result.modifiedAst)
const newCode = recast(result.modifiedAst)
console.log(newCode)
expect(newCode).toContain(code)
expect(newCode).toContain(`revolve001 = revolve(
profile001,
angle = 1,
axis = X,
bidirectionalAngle = 2,
)`)
})
// TODO: missing edit flow test
// TODO: missing multi-profile test
})

View File

@ -1,35 +1,28 @@
import type { Node } from '@rust/kcl-lib/bindings/Node'
import {
createArrayExpression,
createCallExpressionStdLibKw,
createLabeledArg,
createLiteral,
createLocalName,
createVariableDeclaration,
findUniqueName,
} from '@src/lang/create'
import { insertVariableAndOffsetPathToNode } from '@src/lang/modifyAst'
import {
createVariableExpressionsArray,
insertVariableAndOffsetPathToNode,
setCallInAst,
} from '@src/lang/modifyAst'
import {
getEdgeTagCall,
mutateAstWithTagForSketchSegment,
} from '@src/lang/modifyAst/addEdgeTreatment'
import {
getNodeFromPath,
getSketchExprsFromSelection,
getVariableExprsFromSelection,
valueOrVariable,
} from '@src/lang/queryAst'
import { ARG_INDEX_FIELD, LABELED_ARG_FIELD } from '@src/lang/queryAstConstants'
import { getNodePathFromSourceRange } from '@src/lang/queryAstNodePathUtils'
import type {
CallExpressionKw,
Expr,
PathToNode,
Program,
VariableDeclaration,
} from '@src/lang/wasm'
import type { PathToNode, Program, VariableDeclaration } from '@src/lang/wasm'
import type { KclCommandValue } from '@src/lib/commandTypes'
import { KCL_DEFAULT_CONSTANT_PREFIXES } from '@src/lib/constants'
import type { Selections } from '@src/lib/selections'
import { err } from '@src/lib/trap'
@ -60,13 +53,13 @@ export function addExtrude({
// 2. Prepare unlabeled and labeled arguments
// Map the sketches selection into a list of kcl expressions to be passed as unlabelled argument
const sketchesExprList = getSketchExprsFromSelection(
const variableExpressions = getVariableExprsFromSelection(
sketches,
modifiedAst,
nodeToEdit
)
if (err(sketchesExprList)) {
return sketchesExprList
if (err(variableExpressions)) {
return variableExpressions
}
// Extra labeled args expressions
@ -85,7 +78,7 @@ export function addExtrude({
? [createLabeledArg('twistAngle', valueOrVariable(twistAngle))]
: []
const sketchesExpr = createSketchExpression(sketchesExprList)
const sketchesExpr = createVariableExpressionsArray(variableExpressions.exprs)
const call = createCallExpressionStdLibKw('extrude', sketchesExpr, [
createLabeledArg('length', valueOrVariable(length)),
...symmetricExpr,
@ -114,27 +107,10 @@ export function addExtrude({
// 3. If edit, we assign the new function call declaration to the existing node,
// otherwise just push to the end
let pathToNode: PathToNode | undefined
if (nodeToEdit) {
const result = getNodeFromPath<CallExpressionKw>(
modifiedAst,
nodeToEdit,
'CallExpressionKw'
)
if (err(result)) {
return result
}
Object.assign(result.node, call)
pathToNode = nodeToEdit
} else {
const name = findUniqueName(
modifiedAst,
KCL_DEFAULT_CONSTANT_PREFIXES.EXTRUDE
)
const declaration = createVariableDeclaration(name, call)
modifiedAst.body.push(declaration)
pathToNode = createPathToNode(modifiedAst)
const lastPath = variableExpressions.paths.pop() // TODO: check if this is correct
const pathToNode = setCallInAst(modifiedAst, call, nodeToEdit, lastPath)
if (err(pathToNode)) {
return pathToNode
}
return {
@ -168,13 +144,13 @@ export function addSweep({
// 2. Prepare unlabeled and labeled arguments
// Map the sketches selection into a list of kcl expressions to be passed as unlabelled argument
const sketchesExprList = getSketchExprsFromSelection(
const variableExprs = getVariableExprsFromSelection(
sketches,
modifiedAst,
nodeToEdit
)
if (err(sketchesExprList)) {
return sketchesExprList
if (err(variableExprs)) {
return variableExprs
}
// Find the path declaration for the labeled argument
@ -196,7 +172,7 @@ export function addSweep({
? [createLabeledArg('relativeTo', createLiteral(relativeTo))]
: []
const sketchesExpr = createSketchExpression(sketchesExprList)
const sketchesExpr = createVariableExpressionsArray(variableExprs.exprs)
const call = createCallExpressionStdLibKw('sweep', sketchesExpr, [
createLabeledArg('path', pathExpr),
...sectionalExpr,
@ -205,27 +181,10 @@ export function addSweep({
// 3. If edit, we assign the new function call declaration to the existing node,
// otherwise just push to the end
let pathToNode: PathToNode | undefined
if (nodeToEdit) {
const result = getNodeFromPath<CallExpressionKw>(
modifiedAst,
nodeToEdit,
'CallExpressionKw'
)
if (err(result)) {
return result
}
Object.assign(result.node, call)
pathToNode = nodeToEdit
} else {
const name = findUniqueName(
modifiedAst,
KCL_DEFAULT_CONSTANT_PREFIXES.SWEEP
)
const declaration = createVariableDeclaration(name, call)
modifiedAst.body.push(declaration)
pathToNode = createPathToNode(modifiedAst)
const lastPath = variableExprs.paths.pop() // TODO: check if this is correct
const pathToNode = setCallInAst(modifiedAst, call, nodeToEdit, lastPath)
if (err(pathToNode)) {
return pathToNode
}
return {
@ -255,13 +214,13 @@ export function addLoft({
// 2. Prepare unlabeled and labeled arguments
// Map the sketches selection into a list of kcl expressions to be passed as unlabelled argument
const sketchesExprList = getSketchExprsFromSelection(
const variableExprs = getVariableExprsFromSelection(
sketches,
modifiedAst,
nodeToEdit
)
if (err(sketchesExprList)) {
return sketchesExprList
if (err(variableExprs)) {
return variableExprs
}
// Extra labeled args expressions
@ -269,7 +228,7 @@ export function addLoft({
? [createLabeledArg('vDegree', valueOrVariable(vDegree))]
: []
const sketchesExpr = createSketchExpression(sketchesExprList)
const sketchesExpr = createVariableExpressionsArray(variableExprs.exprs)
const call = createCallExpressionStdLibKw('loft', sketchesExpr, [
...vDegreeExpr,
])
@ -281,25 +240,10 @@ export function addLoft({
// 3. If edit, we assign the new function call declaration to the existing node,
// otherwise just push to the end
let pathToNode: PathToNode | undefined
if (nodeToEdit) {
const result = getNodeFromPath<CallExpressionKw>(
modifiedAst,
nodeToEdit,
'CallExpressionKw'
)
if (err(result)) {
return result
}
Object.assign(result.node, call)
pathToNode = nodeToEdit
} else {
const name = findUniqueName(modifiedAst, KCL_DEFAULT_CONSTANT_PREFIXES.LOFT)
const declaration = createVariableDeclaration(name, call)
modifiedAst.body.push(declaration)
const toFirstKwarg = !!vDegree
pathToNode = createPathToNode(modifiedAst, toFirstKwarg)
const lastPath = variableExprs.paths.pop() // TODO: check if this is correct
const pathToNode = setCallInAst(modifiedAst, call, nodeToEdit, lastPath)
if (err(pathToNode)) {
return pathToNode
}
return {
@ -339,13 +283,13 @@ export function addRevolve({
// 2. Prepare unlabeled and labeled arguments
// Map the sketches selection into a list of kcl expressions to be passed as unlabelled argument
const sketchesExprList = getSketchExprsFromSelection(
const variableExprs = getVariableExprsFromSelection(
sketches,
modifiedAst,
nodeToEdit
)
if (err(sketchesExprList)) {
return sketchesExprList
if (err(variableExprs)) {
return variableExprs
}
// Retrieve axis expression depending on mode
@ -372,7 +316,7 @@ export function addRevolve({
]
: []
const sketchesExpr = createSketchExpression(sketchesExprList)
const sketchesExpr = createVariableExpressionsArray(variableExprs.exprs)
const call = createCallExpressionStdLibKw('revolve', sketchesExpr, [
createLabeledArg('angle', valueOrVariable(angle)),
createLabeledArg('axis', getAxisResult.generatedAxis),
@ -399,27 +343,10 @@ export function addRevolve({
// 3. If edit, we assign the new function call declaration to the existing node,
// otherwise just push to the end
let pathToNode: PathToNode | undefined
if (nodeToEdit) {
const result = getNodeFromPath<CallExpressionKw>(
modifiedAst,
nodeToEdit,
'CallExpressionKw'
)
if (err(result)) {
return result
}
Object.assign(result.node, call)
pathToNode = nodeToEdit
} else {
const name = findUniqueName(
modifiedAst,
KCL_DEFAULT_CONSTANT_PREFIXES.REVOLVE
)
const declaration = createVariableDeclaration(name, call)
modifiedAst.body.push(declaration)
pathToNode = createPathToNode(modifiedAst)
const lastPath = variableExprs.paths.pop() // TODO: check if this is correct
const pathToNode = setCallInAst(modifiedAst, call, nodeToEdit, lastPath)
if (err(pathToNode)) {
return pathToNode
}
return {
@ -430,40 +357,6 @@ export function addRevolve({
// Utilities
function createSketchExpression(sketches: Expr[]) {
let sketchesExpr: Expr | null = null
if (sketches.every((s) => s.type === 'PipeSubstitution')) {
// Keeping null so we don't even put it the % sign
} else if (sketches.length === 1) {
sketchesExpr = sketches[0]
} else {
sketchesExpr = createArrayExpression(sketches)
}
return sketchesExpr
}
function createPathToNode(
modifiedAst: Node<Program>,
toFirstKwarg = true
): PathToNode {
const argIndex = 0 // first kwarg for all sweeps here
const pathToCall: PathToNode = [
['body', ''],
[modifiedAst.body.length - 1, 'index'],
['declaration', 'VariableDeclaration'],
['init', 'VariableDeclarator'],
]
if (toFirstKwarg) {
pathToCall.push(
['arguments', 'CallExpressionKw'],
[argIndex, ARG_INDEX_FIELD],
['arg', LABELED_ARG_FIELD]
)
}
return pathToCall
}
export function getAxisExpressionAndIndex(
axisOrEdge: 'Axis' | 'Edge',
axis: string | undefined,

View File

@ -0,0 +1,127 @@
import {
type Artifact,
assertParse,
type CodeRef,
type Program,
recast,
} from '@src/lang/wasm'
import type { Selection, Selections } from '@src/lib/selections'
import { enginelessExecutor } from '@src/lib/testHelpers'
import { err } from '@src/lib/trap'
import { stringToKclExpression } from '@src/lib/kclHelpers'
import type { Node } from '@rust/kcl-lib/bindings/Node'
import { addTranslate } from '@src/lang/modifyAst/transforms'
async function getAstAndArtifactGraph(code: string) {
const ast = assertParse(code)
if (err(ast)) throw ast
const { artifactGraph } = await enginelessExecutor(ast)
return { ast, artifactGraph }
}
function createSelectionFromPathArtifact(
artifacts: (Artifact & { codeRef: CodeRef })[]
): Selections {
const graphSelections = artifacts.map(
(artifact) =>
({
codeRef: artifact.codeRef,
artifact,
}) as Selection
)
return {
graphSelections,
otherSelections: [],
}
}
async function getAstAndSketchSelections(code: string) {
const { ast, artifactGraph } = await getAstAndArtifactGraph(code)
const artifacts = [...artifactGraph.values()].filter((a) => a.type === 'path')
if (artifacts.length === 0) {
throw new Error('Artifact not found in the graph')
}
const sketches = createSelectionFromPathArtifact(artifacts)
return { artifactGraph, ast, sketches }
}
async function getKclCommandValue(value: string) {
const result = await stringToKclExpression(value)
if (err(result) || 'errors' in result) {
throw new Error(`Couldn't create kcl expression`)
}
return result
}
async function runNewAstAndCheckForSweep(ast: Node<Program>) {
const { artifactGraph } = await enginelessExecutor(ast)
console.log('artifactGraph', artifactGraph)
const sweepArtifact = artifactGraph.values().find((a) => a.type === 'sweep')
expect(sweepArtifact).toBeDefined()
}
describe('Testing addTranslate', () => {
it('should push a call with variable if selection was a variable sweep', async () => {
const code = `sketch001 = startSketchOn(XY)
profile001 = circle(sketch001, center = [0, 0], radius = 1)
extrude001 = extrude(profile001, length = 1)
`
const {
artifactGraph,
ast,
sketches: objects,
} = await getAstAndSketchSelections(code)
const result = addTranslate({
ast,
artifactGraph,
objects,
x: await getKclCommandValue('1'),
y: await getKclCommandValue('2'),
z: await getKclCommandValue('3'),
global: true,
})
if (err(result)) throw result
const newCode = recast(result.modifiedAst)
expect(newCode).toContain(code)
// TODO: this should be extrude001, I don't understand why
expect(newCode).toContain(`translate001 = translate(
profile001,
x = 1,
y = 2,
z = 3,
global = true,
)`)
await runNewAstAndCheckForSweep(result.modifiedAst)
})
it('should push a call in pipe if selection was in variable-less pipe', async () => {
const code = `startSketchOn(XY)
|> circle(center = [0, 0], radius = 1)
|> extrude(length = 1)
`
const {
artifactGraph,
ast,
sketches: objects,
} = await getAstAndSketchSelections(code)
const result = addTranslate({
ast,
artifactGraph,
objects,
x: await getKclCommandValue('1'),
y: await getKclCommandValue('2'),
z: await getKclCommandValue('3'),
})
if (err(result)) throw result
const newCode = recast(result.modifiedAst)
expect(newCode).toContain(code)
expect(newCode).toContain(`|> translate(x = 1, y = 2, z = 3)`)
await runNewAstAndCheckForSweep(result.modifiedAst)
})
// TODO: missing edit flow test
// TODO: missing multi-objects test
})

View File

@ -0,0 +1,259 @@
import type { Node } from '@rust/kcl-lib/bindings/Node'
import {
createCallExpressionStdLibKw,
createLabeledArg,
createLiteral,
} from '@src/lang/create'
import {
getVariableExprsFromSelection,
valueOrVariable,
} from '@src/lang/queryAst'
import type { ArtifactGraph, PathToNode, Program } from '@src/lang/wasm'
import { err } from '@src/lib/trap'
import type { Selections } from '@src/lib/selections'
import type { KclCommandValue } from '@src/lib/commandTypes'
import {
createVariableExpressionsArray,
insertVariableAndOffsetPathToNode,
setCallInAst,
} from '@src/lang/modifyAst'
export function addTranslate({
ast,
artifactGraph,
objects,
x,
y,
z,
global,
nodeToEdit,
callName,
}: {
ast: Node<Program>
artifactGraph: ArtifactGraph
objects: Selections
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)
// 2. Prepare unlabeled and labeled arguments
// Map the sketches selection into a list of kcl expressions to be passed as unlabelled argument
const lastChildLookup = true
const variableExpressions = getVariableExprsFromSelection(
objects,
modifiedAst,
nodeToEdit,
lastChildLookup,
artifactGraph
)
if (err(variableExpressions)) {
return variableExpressions
}
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(
callName ?? 'translate',
objectsExpr,
[...xExpr, ...yExpr, ...zExpr, ...globalExpr]
)
// Insert variables for labeled arguments if provided
if (x && 'variableName' in x && x.variableName) {
insertVariableAndOffsetPathToNode(x, modifiedAst, nodeToEdit)
}
if (y && 'variableName' in y && y.variableName) {
insertVariableAndOffsetPathToNode(y, modifiedAst, nodeToEdit)
}
if (z && '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,
}
}
export function addRotate({
ast,
artifactGraph,
objects,
roll,
pitch,
yaw,
global,
nodeToEdit,
}: {
ast: Node<Program>
artifactGraph: ArtifactGraph
objects: Selections
roll?: KclCommandValue
pitch?: KclCommandValue
yaw?: 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 lastChildLookup = true
const variableExpressions = getVariableExprsFromSelection(
objects,
modifiedAst,
nodeToEdit,
lastChildLookup,
artifactGraph
)
if (err(variableExpressions)) {
return variableExpressions
}
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, [
...rollExpr,
...pitchExpr,
...yawExpr,
...globalExpr,
])
// Insert variables for labeled arguments if provided
if (roll && 'variableName' in roll && roll.variableName) {
insertVariableAndOffsetPathToNode(roll, modifiedAst, nodeToEdit)
}
if (pitch && 'variableName' in pitch && pitch.variableName) {
insertVariableAndOffsetPathToNode(pitch, 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,
// 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,
}
}
export function addScale({
ast,
artifactGraph,
objects,
x,
y,
z,
global,
nodeToEdit,
}: {
ast: Node<Program>
artifactGraph: ArtifactGraph
objects: Selections
x?: KclCommandValue
y?: KclCommandValue
z?: KclCommandValue
global?: boolean
nodeToEdit?: PathToNode
}): Error | { modifiedAst: Node<Program>; pathToNode: PathToNode } {
return addTranslate({
ast,
artifactGraph,
objects,
x,
y,
z,
global,
nodeToEdit,
callName: 'scale',
})
}
export function addClone({
ast,
artifactGraph,
objects,
nodeToEdit,
}: {
ast: Node<Program>
artifactGraph: ArtifactGraph
objects: Selections
nodeToEdit?: PathToNode
}): Error | { modifiedAst: Node<Program>; pathToNode: PathToNode } {
// 1. Clone the ast so we can edit it
const modifiedAst = structuredClone(ast)
// 2. Prepare unlabeled arguments
// Map the sketches selection into a list of kcl expressions to be passed as unlabelled argument
const lastChildLookup = true
const variableExpressions = getVariableExprsFromSelection(
objects,
modifiedAst,
nodeToEdit,
lastChildLookup,
artifactGraph
)
if (err(variableExpressions)) {
return variableExpressions
}
const objectsExpr = createVariableExpressionsArray(variableExpressions.exprs)
const call = createCallExpressionStdLibKw('clone', objectsExpr, [])
// 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 toFirstKwarg = false
const pathToNode = setCallInAst(
modifiedAst,
call,
nodeToEdit,
lastPath,
toFirstKwarg
)
if (err(pathToNode)) {
return pathToNode
}
return {
modifiedAst,
pathToNode,
}
}

View File

@ -15,6 +15,7 @@ import {
findAllPreviousVariables,
findUsesOfTagInPipe,
getNodeFromPath,
getVariableExprsFromSelection,
hasSketchPipeBeenExtruded,
isCursorInFunctionDefinition,
isNodeSafeToReplace,
@ -27,7 +28,7 @@ import { topLevelRange } from '@src/lang/util'
import type { Identifier, PathToNode } from '@src/lang/wasm'
import { assertParse, recast } from '@src/lang/wasm'
import { initPromise } from '@src/lang/wasmUtils'
import { type Selection } from '@src/lib/selections'
import type { Selections, Selection } from '@src/lib/selections'
import { enginelessExecutor } from '@src/lib/testHelpers'
import { err } from '@src/lib/trap'
@ -778,3 +779,184 @@ describe('Testing specific sketch getNodeFromPath workflow', () => {
expect(result).toEqual(false)
})
})
describe('Testing getVariableExprsFromSelection', () => {
it('should find the variable expr in a simple profile selection', async () => {
const circleProfileInVar = `sketch001 = startSketchOn(XY)
profile001 = circle(sketch001, center = [0, 0], radius = 1)
`
const ast = assertParse(circleProfileInVar)
const { artifactGraph } = await enginelessExecutor(ast)
const artifact = artifactGraph.values().find((a) => a.type === 'path')
if (!artifact) {
throw new Error('Artifact not found in the graph')
}
const selections: Selections = {
graphSelections: [
{
codeRef: artifact.codeRef,
artifact,
},
],
otherSelections: [],
}
const variableExprs = getVariableExprsFromSelection(selections, ast)
if (err(variableExprs)) throw variableExprs
expect(variableExprs.exprs).toHaveLength(1)
if (variableExprs.exprs[0].type !== 'Name') {
throw new Error(`Expected Name, got ${variableExprs.exprs[0].type}`)
}
expect(variableExprs.exprs[0].name.name).toEqual('profile001')
expect(variableExprs.paths).toHaveLength(1)
expect(variableExprs.paths[0]).toEqual([
['body', ''],
[1, 'index'],
['declaration', 'VariableDeclaration'],
['init', ''],
])
})
it('should return the pipe substitution symbol in a variable-less simple profile selection', async () => {
const circleProfileInVar = `startSketchOn(XY)
|> circle(center = [0, 0], radius = 1)
`
const ast = assertParse(circleProfileInVar)
const { artifactGraph } = await enginelessExecutor(ast)
const artifact = artifactGraph.values().find((a) => a.type === 'path')
if (!artifact) {
throw new Error('Artifact not found in the graph')
}
const selections: Selections = {
graphSelections: [
{
codeRef: artifact.codeRef,
artifact,
},
],
otherSelections: [],
}
const variableExprs = getVariableExprsFromSelection(selections, ast)
if (err(variableExprs)) throw variableExprs
expect(variableExprs.exprs).toHaveLength(1)
expect(variableExprs.exprs[0].type).toEqual('PipeSubstitution')
expect(variableExprs.paths).toHaveLength(1)
expect(variableExprs.paths[0]).toEqual([
['body', ''],
[0, 'index'],
['expression', 'ExpressionStatement'],
['body', 'PipeExpression'],
[1, 'index'],
])
})
it('should find the variable exprs in a multi profile selection ', async () => {
const circleProfileInVar = `sketch001 = startSketchOn(XY)
profile001 = circle(sketch001, center = [0, 0], radius = 1)
profile002 = circle(sketch001, center = [2, 2], radius = 1)
`
const ast = assertParse(circleProfileInVar)
const { artifactGraph } = await enginelessExecutor(ast)
const artifacts = [...artifactGraph.values()].filter(
(a) => a.type === 'path'
)
if (!artifacts || artifacts.length !== 2) {
throw new Error('Artifact not found in the graph')
}
const selections: Selections = {
graphSelections: artifacts.map((artifact) => {
return {
codeRef: artifact.codeRef,
artifact,
}
}),
otherSelections: [],
}
const variableExprs = getVariableExprsFromSelection(selections, ast)
if (err(variableExprs)) throw variableExprs
expect(variableExprs.exprs).toHaveLength(2)
if (variableExprs.exprs[0].type !== 'Name') {
throw new Error(`Expected Name, got ${variableExprs.exprs[0].type}`)
}
if (variableExprs.exprs[1].type !== 'Name') {
throw new Error(`Expected Name, got ${variableExprs.exprs[1].type}`)
}
expect(variableExprs.exprs[0].name.name).toEqual('profile001')
expect(variableExprs.exprs[1].name.name).toEqual('profile002')
expect(variableExprs.paths).toHaveLength(2)
expect(variableExprs.paths[0]).toEqual([
['body', ''],
[1, 'index'],
['declaration', 'VariableDeclaration'],
['init', ''],
])
expect(variableExprs.paths[1]).toEqual([
['body', ''],
[2, 'index'],
['declaration', 'VariableDeclaration'],
['init', ''],
])
})
it('should return the pipe substitution symbol and a variable name in a complex multi profile selection', async () => {
const circleProfileInVar = `startSketchOn(XY)
|> circle(center = [0, 0], radius = 1)
profile002 = circle(sketch001, center = [2, 2], radius = 1)
`
const ast = assertParse(circleProfileInVar)
const { artifactGraph } = await enginelessExecutor(ast)
const artifacts = [...artifactGraph.values()].filter(
(a) => a.type === 'path'
)
if (!artifacts || artifacts.length !== 2) {
throw new Error('Artifact not found in the graph')
}
const selections: Selections = {
graphSelections: artifacts.map((artifact) => {
return {
codeRef: artifact.codeRef,
artifact,
}
}),
otherSelections: [],
}
const variableExprs = getVariableExprsFromSelection(selections, ast)
if (err(variableExprs)) throw variableExprs
expect(variableExprs.exprs).toHaveLength(2)
if (variableExprs.exprs[0].type !== 'PipeSubstitution') {
throw new Error(
`Expected PipeSubstitution, got ${variableExprs.exprs[0].type}`
)
}
if (variableExprs.exprs[1].type !== 'Name') {
throw new Error(`Expected Name, got ${variableExprs.exprs[1].type}`)
}
expect(variableExprs.exprs[1].name.name).toEqual('profile002')
expect(variableExprs.paths).toHaveLength(2)
expect(variableExprs.paths[0]).toEqual([
['body', ''],
[0, 'index'],
['expression', 'ExpressionStatement'],
['body', 'PipeExpression'],
[1, 'index'],
])
expect(variableExprs.paths[1]).toEqual([
['body', ''],
[1, 'index'],
['declaration', 'VariableDeclaration'],
['init', ''],
])
})
})

View File

@ -9,6 +9,7 @@ import { getNodePathFromSourceRange } from '@src/lang/queryAstNodePathUtils'
import {
codeRefFromRange,
getArtifactOfTypes,
getCodeRefsByArtifactId,
} from '@src/lang/std/artifactGraph'
import { getArgForEnd } from '@src/lang/std/sketch'
import { getSketchSegmentFromSourceRange } from '@src/lang/std/sketchConstraints'
@ -56,6 +57,10 @@ import { ARG_INDEX_FIELD, LABELED_ARG_FIELD } from '@src/lang/queryAstConstants'
import type { KclCommandValue } from '@src/lib/commandTypes'
import type { UnaryExpression } from 'typescript'
import type { NumericType } from '@rust/kcl-lib/bindings/NumericType'
import {
findAllChildrenAndOrderByPlaceInCode,
getLastVariable,
} from '@src/lang/modifyAst/boolean'
/**
* Retrieves a node from a given path within a Program node structure, optionally stopping at a specified node type.
@ -1042,51 +1047,91 @@ export const valueOrVariable = (variable: KclCommandValue) => {
// Go from a selection of sketches to a list of KCL expressions that
// can be used to create KCL sweep call declarations.
export function getSketchExprsFromSelection(
export function getVariableExprsFromSelection(
selection: Selections,
ast: Node<Program>,
nodeToEdit?: PathToNode
): Error | Expr[] {
const sketches: Expr[] = selection.graphSelections.flatMap((s) => {
const sketchVariable = getNodeFromPath<VariableDeclarator>(
ast,
s?.codeRef.pathToNode,
'VariableDeclarator'
)
if (err(sketchVariable)) {
return []
nodeToEdit?: PathToNode,
lastChildLookup = false,
artifactGraph?: ArtifactGraph
): Error | { exprs: Expr[]; paths: PathToNode[] } {
const paths: PathToNode[] = []
const exprs: Expr[] = []
for (const s of selection.graphSelections) {
let variable:
| {
node: VariableDeclaration
shallowPath: PathToNode
deepPath: PathToNode
}
| undefined
if (lastChildLookup && s.artifact && artifactGraph) {
console.log('we are looking for last child variable', s.artifact)
console.log('artifactGraph', artifactGraph)
const children = findAllChildrenAndOrderByPlaceInCode(
s.artifact,
artifactGraph
)
console.log('found children', children)
const lastChildVariable = getLastVariable(children, ast)
if (!lastChildVariable) {
continue
}
variable = lastChildVariable.variableDeclaration
} else {
const directLookup = getNodeFromPath<VariableDeclaration>(
ast,
s.codeRef.pathToNode,
'VariableDeclaration'
)
if (err(directLookup)) {
continue
}
variable = directLookup
}
if (sketchVariable.node.id) {
const name = sketchVariable.node?.id.name
if (variable.node.declaration?.id) {
const name = variable.node.declaration.id.name
if (nodeToEdit) {
const result = getNodeFromPath<VariableDeclarator>(
const result = getNodeFromPath<VariableDeclaration>(
ast,
nodeToEdit,
'VariableDeclarator'
'VariableDeclaration'
)
if (
!err(result) &&
result.node.type === 'VariableDeclarator' &&
name === result.node.id.name
result.node.type === 'VariableDeclaration' &&
name === result.node.declaration.id.name
) {
// Pointing to same variable case
return createPipeSubstitution()
paths.push(nodeToEdit)
exprs.push(createPipeSubstitution())
continue
}
}
// Pointing to different variable case
return createLocalName(name)
} else {
// No variable case
return createPipeSubstitution()
paths.push(variable.deepPath)
exprs.push(createLocalName(name))
continue
}
})
if (sketches.length === 0) {
// import case
const importNodeAndAlias = findImportNodeAndAlias(ast, s.codeRef.pathToNode)
if (importNodeAndAlias) {
paths.push(s.codeRef.pathToNode)
exprs.push(createLocalName(importNodeAndAlias.alias))
continue
}
// No variable case
paths.push(variable.deepPath)
exprs.push(createPipeSubstitution())
}
if (exprs.length === 0) {
return new Error("Couldn't map selections to program references")
}
return sketches
return { exprs, paths }
}
// Go from the sketches argument in a KCL sweep call declaration
@ -1158,6 +1203,61 @@ export function getSketchSelectionsFromOperation(
}
}
export function getObjectSelectionsFromOperation(
operation: Operation,
artifactGraph: ArtifactGraph
): Error | Selections {
const error = new Error("Couldn't retrieve sketches from operation")
if (operation.type !== 'StdLibCall' || !operation.unlabeledArg) {
return error
}
let artifactIds: string[] = []
if (
operation.unlabeledArg.value.type === 'Solid' ||
operation.unlabeledArg.value.type === 'Sketch'
) {
artifactIds = [operation.unlabeledArg.value.value.artifactId]
} else if (operation.unlabeledArg.value.type === 'ImportedGeometry') {
artifactIds = [operation.unlabeledArg.value.artifact_id]
} else if (operation.unlabeledArg.value.type === 'Array') {
artifactIds = operation.unlabeledArg.value.value
.filter((v) => v.type === 'Solid' || v.type === 'Sketch')
.map((v) => v.value.artifactId)
} else {
return error
}
const graphSelections: Selection[] = []
for (const artifactId of artifactIds) {
const artifact = artifactGraph.get(artifactId)
if (!artifact) {
continue
}
const codeRefs = getCodeRefsByArtifactId(artifactId, artifactGraph)
if (!codeRefs || codeRefs.length === 0) {
continue
}
graphSelections.push({
artifact,
codeRef: codeRefs[0], // TODO: figure out why two codeRefs could be possible?
})
}
if (graphSelections.length === 0) {
return error
}
const selections: Selections = {
graphSelections,
otherSelections: [],
}
console.log('massaged selections', selections)
return selections
}
export function locateVariableWithCallOrPipe(
ast: Program,
pathToNode: PathToNode

View File

@ -1,7 +1,6 @@
import type { Models } from '@kittycad/lib'
import { angleLengthInfo } from '@src/components/Toolbar/angleLengthInfo'
import { findUniqueName } from '@src/lang/create'
import { getNodeFromPath } from '@src/lang/queryAst'
import { getVariableDeclaration } from '@src/lang/queryAst/getVariableDeclaration'
import { getNodePathFromSourceRange } from '@src/lang/queryAstNodePathUtils'
@ -19,7 +18,6 @@ import type {
} from '@src/lib/commandTypes'
import {
IS_ML_EXPERIMENTAL,
KCL_DEFAULT_CONSTANT_PREFIXES,
KCL_DEFAULT_DEGREE,
KCL_DEFAULT_LENGTH,
KCL_DEFAULT_TRANSFORM,
@ -186,22 +184,31 @@ export type ModelingCommandSchema = {
}
Translate: {
nodeToEdit?: PathToNode
selection: Selections
x: KclCommandValue
y: KclCommandValue
z: KclCommandValue
objects: Selections
x?: KclCommandValue
y?: KclCommandValue
z?: KclCommandValue
global?: boolean
}
Rotate: {
nodeToEdit?: PathToNode
selection: Selections
roll: KclCommandValue
pitch: KclCommandValue
yaw: KclCommandValue
objects: Selections
roll?: KclCommandValue
pitch?: KclCommandValue
yaw?: KclCommandValue
global?: boolean
}
Scale: {
nodeToEdit?: PathToNode
objects: Selections
x?: KclCommandValue
y?: KclCommandValue
z?: KclCommandValue
global?: boolean
}
Clone: {
nodeToEdit?: PathToNode
selection: Selections
variableName: string
objects: Selections
}
'Boolean Subtract': {
solids: Selections
@ -750,14 +757,8 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
required: false,
displayName: 'CounterClockWise',
options: [
{
name: 'False',
value: false,
},
{
name: 'True',
value: true,
},
{ name: 'False', value: false },
{ name: 'True', value: true },
],
},
},
@ -1079,14 +1080,13 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
nodeToEdit: {
...nodeToEditProps,
},
selection: {
objects: {
// selectionMixed allows for feature tree selection of module imports
inputType: 'selectionMixed',
multiple: false,
required: true,
skip: true,
selectionTypes: ['path'],
selectionTypes: ['path', 'sweep'],
selectionFilter: ['object'],
multiple: true,
required: true,
hidden: (context) => Boolean(context.argumentsToSubmit.nodeToEdit),
},
x: {
@ -1104,6 +1104,14 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
defaultValue: KCL_DEFAULT_TRANSFORM,
required: true,
},
global: {
inputType: 'options',
required: false,
options: [
{ name: 'False', value: false },
{ name: 'True', value: true },
],
},
},
},
Rotate: {
@ -1114,7 +1122,7 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
nodeToEdit: {
...nodeToEditProps,
},
selection: {
objects: {
// selectionMixed allows for feature tree selection of module imports
inputType: 'selectionMixed',
multiple: false,
@ -1139,6 +1147,57 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
defaultValue: KCL_DEFAULT_TRANSFORM,
required: true,
},
global: {
inputType: 'options',
required: false,
options: [
{ name: 'False', value: false },
{ name: 'True', value: true },
],
},
},
},
Scale: {
description: 'Set scale on solid or sketch.',
icon: 'scale',
needsReview: true,
args: {
nodeToEdit: {
...nodeToEditProps,
},
objects: {
// selectionMixed allows for feature tree selection of module imports
inputType: 'selectionMixed',
multiple: false,
required: true,
skip: true,
selectionTypes: ['path'],
selectionFilter: ['object'],
hidden: (context) => Boolean(context.argumentsToSubmit.nodeToEdit),
},
x: {
inputType: 'kcl',
defaultValue: KCL_DEFAULT_TRANSFORM,
required: true,
},
y: {
inputType: 'kcl',
defaultValue: KCL_DEFAULT_TRANSFORM,
required: true,
},
z: {
inputType: 'kcl',
defaultValue: KCL_DEFAULT_TRANSFORM,
required: true,
},
global: {
inputType: 'options',
required: false,
options: [
{ name: 'False', value: false },
{ name: 'True', value: true },
],
},
},
},
Clone: {
@ -1149,39 +1208,14 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
nodeToEdit: {
...nodeToEditProps,
},
selection: {
objects: {
// selectionMixed allows for feature tree selection of module imports
inputType: 'selectionMixed',
multiple: false,
required: true,
skip: true,
selectionTypes: ['path'],
selectionTypes: ['path', 'sweep'],
selectionFilter: ['object'],
hidden: (context) => Boolean(context.argumentsToSubmit.nodeToEdit),
},
variableName: {
inputType: 'string',
multiple: true,
required: true,
defaultValue: () => {
return findUniqueName(
kclManager.ast,
KCL_DEFAULT_CONSTANT_PREFIXES.CLONE
)
},
validation: async ({
data,
}: {
data: string
}) => {
// Be conservative and error out if there is an item or module with the same name.
const variableExists =
kclManager.variables[data] || kclManager.variables['__mod_' + data]
if (variableExists) {
return 'This variable name is already in use.'
}
return true
},
hidden: (context) => Boolean(context.argumentsToSubmit.nodeToEdit),
},
},
},

View File

@ -1,11 +1,6 @@
import { executeAstMock } from '@src/lang/langHelpers'
import {
type CallExpressionKw,
formatNumberValue,
parse,
resultIsOk,
} from '@src/lang/wasm'
import type { KclCommandValue, KclExpression } from '@src/lib/commandTypes'
import { formatNumberValue, parse, resultIsOk } from '@src/lang/wasm'
import type { KclExpression } from '@src/lib/commandTypes'
import { rustContext } from '@src/lib/singletons'
import { err } from '@src/lib/trap'
@ -74,23 +69,3 @@ export async function stringToKclExpression(value: string) {
valueText: value,
} satisfies KclExpression
}
export async function retrieveArgFromPipedCallExpression(
callExpression: CallExpressionKw,
name: string
): Promise<KclCommandValue | undefined> {
const arg = callExpression.arguments.find(
(a) => a.label?.type === 'Identifier' && a.label?.name === name
)
if (
arg?.type === 'LabeledArg' &&
(arg.arg.type === 'Name' || arg.arg.type === 'Literal')
) {
const value = arg.arg.type === 'Name' ? arg.arg.name.name : arg.arg.raw
const result = await stringToKclExpression(value)
if (!(err(result) || 'errors' in result)) {
return result
}
}
return undefined
}

View File

@ -3,8 +3,8 @@ import type { OpKclValue, Operation } from '@rust/kcl-lib/bindings/Operation'
import type { CustomIconName } from '@src/components/CustomIcon'
import {
getNodeFromPath,
findPipesWithImportAlias,
getSketchSelectionsFromOperation,
getObjectSelectionsFromOperation,
} from '@src/lang/queryAst'
import type { Artifact } from '@src/lang/std/artifactGraph'
import {
@ -26,10 +26,7 @@ import type {
ModelingCommandSchema,
} from '@src/lib/commandBarConfigs/modelingCommandConfig'
import type { KclCommandValue, KclExpression } from '@src/lib/commandTypes'
import {
stringToKclExpression,
retrieveArgFromPipedCallExpression,
} from '@src/lib/kclHelpers'
import { stringToKclExpression } from '@src/lib/kclHelpers'
import { isDefaultPlaneStr } from '@src/lib/planes'
import type { Selection, Selections } from '@src/lib/selections'
import { codeManager, kclManager, rustContext } from '@src/lib/singletons'
@ -1179,6 +1176,8 @@ export const stdLibMap: Record<string, StdLibCallInfo> = {
scale: {
label: 'Scale',
icon: 'scale',
prepareToEdit: prepareToEditScale,
supportsTransform: true,
},
shell: {
label: 'Shell',
@ -1526,69 +1525,182 @@ async function prepareToEditTranslate({ operation }: EnterEditFlowProps) {
name: 'Translate',
groupId: 'modeling',
}
const isModuleImport = operation.type === 'GroupBegin'
const isSupportedStdLibCall =
operation.type === 'StdLibCall' &&
stdLibMap[operation.name]?.supportsTransform
if (!isModuleImport && !isSupportedStdLibCall) {
if (!isSupportedStdLibCall) {
return {
reason: 'Unsupported operation type. Please edit in the code editor.',
}
}
const nodeToEdit = pathToNodeFromRustNodePath(operation.nodePath)
let x: KclExpression | undefined = undefined
let y: KclExpression | undefined = undefined
let z: 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 translate
const pipes = findPipesWithImportAlias(ast, nodeToEdit, 'translate')
pipe = pipes.at(-1)?.expression
} else {
pipe = pipeLookupFromOperation.node
if (err(objects)) {
return { reason: "Couldn't retrieve objects" }
}
if (pipe) {
const translate = pipe.body.find(
(n) => n.type === 'CallExpressionKw' && n.callee.name.name === 'translate'
// 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 (operation.labeledArgs.x) {
const result = await stringToKclExpression(
codeManager.code.slice(
operation.labeledArgs.x.sourceRange[0],
operation.labeledArgs.x.sourceRange[1]
)
)
if (translate?.type === 'CallExpressionKw') {
x = await retrieveArgFromPipedCallExpression(translate, 'x')
y = await retrieveArgFromPipedCallExpression(translate, 'y')
z = await retrieveArgFromPipedCallExpression(translate, 'z')
if (err(result) || 'errors' in result) {
return { reason: "Couldn't retrieve x argument" }
}
x = result
}
// Won't be used since we provide nodeToEdit
const selection: Selections = { graphSelections: [], otherSelections: [] }
const argDefaultValues = { nodeToEdit, selection, x, y, z }
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,
// with `nodeToEdit` set, which will let the actor know
// to edit the node that corresponds to the StdLibCall.
const argDefaultValues: ModelingCommandSchema['Translate'] = {
objects,
x,
y,
z,
global,
nodeToEdit: pathToNodeFromRustNodePath(operation.nodePath),
}
return {
...baseCommand,
argDefaultValues,
}
}
export async function enterTranslateFlow({
operation,
}: EnterEditFlowProps): Promise<Error | CommandBarMachineEvent> {
const data = await prepareToEditTranslate({ operation })
if ('reason' in data) {
return new Error(data.reason)
async function prepareToEditScale({ operation }: EnterEditFlowProps) {
const baseCommand = {
name: 'Scale',
groupId: 'modeling',
}
const isSupportedStdLibCall =
operation.type === 'StdLibCall' &&
stdLibMap[operation.name]?.supportsTransform
if (!isSupportedStdLibCall) {
return {
reason: 'Unsupported operation type. Please edit in the code editor.',
}
}
// 1. Map the unlabeled arguments to selections
const objects = getObjectSelectionsFromOperation(
operation,
kclManager.artifactGraph
)
if (err(objects)) {
return { reason: "Couldn't retrieve objects" }
}
// 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 (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,
// 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 {
type: 'Find and select command',
data,
...baseCommand,
argDefaultValues,
}
}
@ -1597,96 +1709,89 @@ async function prepareToEditRotate({ operation }: EnterEditFlowProps) {
name: 'Rotate',
groupId: 'modeling',
}
const isModuleImport = operation.type === 'GroupBegin'
const isSupportedStdLibCall =
operation.type === 'StdLibCall' &&
stdLibMap[operation.name]?.supportsTransform
if (!isModuleImport && !isSupportedStdLibCall) {
if (!isSupportedStdLibCall) {
return {
reason: 'Unsupported operation type. Please edit in the code editor.',
}
}
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'
// 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 (operation.labeledArgs.roll) {
const result = await stringToKclExpression(
codeManager.code.slice(
operation.labeledArgs.roll.sourceRange[0],
operation.labeledArgs.roll.sourceRange[1]
)
)
if (rotate?.type === 'CallExpressionKw') {
roll = await retrieveArgFromPipedCallExpression(rotate, 'roll')
pitch = await retrieveArgFromPipedCallExpression(rotate, 'pitch')
yaw = await retrieveArgFromPipedCallExpression(rotate, 'yaw')
if (err(result) || 'errors' in result) {
return { reason: "Couldn't retrieve roll argument" }
}
roll = result
}
// Won't be used since we provide nodeToEdit
const selection: Selections = { graphSelections: [], otherSelections: [] }
const argDefaultValues = { nodeToEdit, selection, roll, pitch, yaw }
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'
}
// 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,
}
}
export async function enterRotateFlow({
operation,
}: EnterEditFlowProps): Promise<Error | CommandBarMachineEvent> {
const data = await prepareToEditRotate({ operation })
if ('reason' in data) {
return new Error(data.reason)
}
return {
type: 'Find and select command',
data,
}
}
export async function enterCloneFlow({
operation,
}: EnterEditFlowProps): Promise<Error | CommandBarMachineEvent> {
const isModuleImport = operation.type === 'GroupBegin'
const isSupportedStdLibCall =
operation.type === 'StdLibCall' &&
stdLibMap[operation.name]?.supportsTransform
if (!isModuleImport && !isSupportedStdLibCall) {
return new Error(
'Unsupported operation type. Please edit in the code editor.'
)
}
const nodeToEdit = pathToNodeFromRustNodePath(operation.nodePath)
// Won't be used since we provide nodeToEdit
const selection: Selections = { graphSelections: [], otherSelections: [] }
const argDefaultValues = { nodeToEdit, selection }
return {
type: 'Find and select command',
data: {
name: 'Clone',
groupId: 'modeling',
argDefaultValues,
},
}
}

View File

@ -432,6 +432,24 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
},
],
},
{
id: 'scale',
onClick: () =>
commandBarActor.send({
type: 'Find and select command',
data: { name: 'Scale', groupId: 'modeling' },
}),
icon: 'scale',
status: 'available',
title: 'Scale',
description: 'Apply scaling to a solid or sketch.',
links: [
{
label: 'API docs',
url: 'https://zoo.dev/docs/kcl-std/functions/std-transform-scale',
},
],
},
{
id: 'clone',
onClick: () =>

View File

@ -12,13 +12,7 @@ import type { Artifact } from '@src/lang/std/artifactGraph'
import { getArtifactFromRange } from '@src/lang/std/artifactGraph'
import type { SourceRange } from '@src/lang/wasm'
import type { EnterEditFlowProps } from '@src/lib/operations'
import {
enterAppearanceFlow,
enterCloneFlow,
enterEditFlow,
enterTranslateFlow,
enterRotateFlow,
} from '@src/lib/operations'
import { enterAppearanceFlow, enterEditFlow } from '@src/lib/operations'
import { kclManager } from '@src/lib/singletons'
import { err } from '@src/lib/trap'
import { commandBarActor } from '@src/lib/singletons'
@ -52,6 +46,10 @@ type FeatureTreeEvent =
type: 'enterRotateFlow'
data: { targetSourceRange: SourceRange; currentOperation: Operation }
}
| {
type: 'enterScaleFlow'
data: { targetSourceRange: SourceRange; currentOperation: Operation }
}
| {
type: 'enterCloneFlow'
data: { targetSourceRange: SourceRange; currentOperation: Operation }
@ -126,75 +124,6 @@ export const featureTreeMachine = setup({
})
}
),
prepareTranslateCommand: fromPromise(
({
input,
}: {
input: EnterEditFlowProps & {
commandBarSend: (typeof commandBarActor)['send']
}
}) => {
return new Promise((resolve, reject) => {
const { commandBarSend, ...editFlowProps } = input
enterTranslateFlow(editFlowProps)
.then((result) => {
if (err(result)) {
reject(result)
return
}
input.commandBarSend(result)
resolve(result)
})
.catch(reject)
})
}
),
prepareRotateCommand: fromPromise(
({
input,
}: {
input: EnterEditFlowProps & {
commandBarSend: (typeof commandBarActor)['send']
}
}) => {
return new Promise((resolve, reject) => {
const { commandBarSend, ...editFlowProps } = input
enterRotateFlow(editFlowProps)
.then((result) => {
if (err(result)) {
reject(result)
return
}
input.commandBarSend(result)
resolve(result)
})
.catch(reject)
})
}
),
prepareCloneCommand: fromPromise(
({
input,
}: {
input: EnterEditFlowProps & {
commandBarSend: (typeof commandBarActor)['send']
}
}) => {
return new Promise((resolve, reject) => {
const { commandBarSend, ...editFlowProps } = input
enterCloneFlow(editFlowProps)
.then((result) => {
if (err(result)) {
reject(result)
return
}
input.commandBarSend(result)
resolve(result)
})
.catch(reject)
})
}
),
sendDeleteCommand: fromPromise(
({
input,
@ -252,6 +181,10 @@ export const featureTreeMachine = setup({
targetSourceRange: undefined,
}),
sendSelectionEvent: () => {},
sendTranslateCommand: () => {},
sendRotateCommand: () => {},
sendScaleCommand: () => {},
sendCloneCommand: () => {},
openCodePane: () => {},
scrollToError: () => {},
},
@ -285,17 +218,38 @@ export const featureTreeMachine = setup({
enterTranslateFlow: {
target: 'enteringTranslateFlow',
actions: ['saveTargetSourceRange', 'saveCurrentOperation'],
actions: [
'saveTargetSourceRange',
'saveCurrentOperation',
'sendSelectionEvent',
],
},
enterRotateFlow: {
target: 'enteringRotateFlow',
actions: ['saveTargetSourceRange', 'saveCurrentOperation'],
actions: [
'saveTargetSourceRange',
'saveCurrentOperation',
'sendSelectionEvent',
],
},
enterScaleFlow: {
target: 'enteringScaleFlow',
actions: [
'saveTargetSourceRange',
'saveCurrentOperation',
'sendSelectionEvent',
],
},
enterCloneFlow: {
target: 'enteringCloneFlow',
actions: ['saveTargetSourceRange', 'saveCurrentOperation'],
actions: [
'saveTargetSourceRange',
'saveCurrentOperation',
'sendSelectionEvent',
],
},
deleteOperation: {
@ -355,6 +309,82 @@ export const featureTreeMachine = setup({
initial: 'selecting',
},
enteringTranslateFlow: {
states: {
enteringTranslateFlow: {
on: {
selected: 'done',
},
entry: 'sendTranslateCommand',
},
done: {
always: '#featureTree.idle',
entry: 'clearContext',
},
},
initial: 'enteringTranslateFlow',
},
enteringRotateFlow: {
states: {
enteringRotateFlow: {
on: {
selected: 'done',
},
entry: 'sendRotateCommand',
},
done: {
always: '#featureTree.idle',
entry: 'clearContext',
},
},
initial: 'enteringRotateFlow',
},
enteringScaleFlow: {
states: {
enteringScaleFlow: {
on: {
selected: 'done',
},
entry: 'sendScaleCommand',
},
done: {
always: '#featureTree.idle',
entry: 'clearContext',
},
},
initial: 'enteringScaleFlow',
},
enteringCloneFlow: {
states: {
enteringCloneFlow: {
on: {
selected: 'done',
},
entry: 'sendCloneCommand',
},
done: {
always: '#featureTree.idle',
entry: 'clearContext',
},
},
initial: 'enteringCloneFlow',
},
enteringEditFlow: {
states: {
selecting: {
@ -463,168 +493,6 @@ export const featureTreeMachine = setup({
exit: ['clearContext'],
},
enteringTranslateFlow: {
states: {
selecting: {
on: {
selected: {
target: 'prepareTranslateCommand',
reenter: true,
},
},
},
done: {
always: '#featureTree.idle',
},
prepareTranslateCommand: {
invoke: {
src: 'prepareTranslateCommand',
input: ({ context }) => {
const artifact = context.targetSourceRange
? (getArtifactFromRange(
context.targetSourceRange,
kclManager.artifactGraph
) ?? undefined)
: undefined
return {
// currentOperation is guaranteed to be defined here
operation: context.currentOperation!,
artifact,
commandBarSend: commandBarActor.send,
}
},
onDone: {
target: 'done',
reenter: true,
},
onError: {
target: 'done',
reenter: true,
actions: ({ event }) => {
if ('error' in event && err(event.error)) {
toast.error(event.error.message)
}
},
},
},
},
},
initial: 'selecting',
entry: 'sendSelectionEvent',
exit: ['clearContext'],
},
enteringRotateFlow: {
states: {
selecting: {
on: {
selected: {
target: 'prepareRotateCommand',
reenter: true,
},
},
},
done: {
always: '#featureTree.idle',
},
prepareRotateCommand: {
invoke: {
src: 'prepareRotateCommand',
input: ({ context }) => {
const artifact = context.targetSourceRange
? (getArtifactFromRange(
context.targetSourceRange,
kclManager.artifactGraph
) ?? undefined)
: undefined
return {
// currentOperation is guaranteed to be defined here
operation: context.currentOperation!,
artifact,
commandBarSend: commandBarActor.send,
}
},
onDone: {
target: 'done',
reenter: true,
},
onError: {
target: 'done',
reenter: true,
actions: ({ event }) => {
if ('error' in event && err(event.error)) {
toast.error(event.error.message)
}
},
},
},
},
},
initial: 'selecting',
entry: 'sendSelectionEvent',
exit: ['clearContext'],
},
enteringCloneFlow: {
states: {
selecting: {
on: {
selected: {
target: 'prepareCloneCommand',
reenter: true,
},
},
},
done: {
always: '#featureTree.idle',
},
prepareCloneCommand: {
invoke: {
src: 'prepareCloneCommand',
input: ({ context }) => {
const artifact = context.targetSourceRange
? (getArtifactFromRange(
context.targetSourceRange,
kclManager.artifactGraph
) ?? undefined)
: undefined
return {
// currentOperation is guaranteed to be defined here
operation: context.currentOperation!,
artifact,
commandBarSend: commandBarActor.send,
}
},
onDone: {
target: 'done',
reenter: true,
},
onError: {
target: 'done',
reenter: true,
actions: ({ event }) => {
if ('error' in event && err(event.error)) {
toast.error(event.error.message)
}
},
},
},
},
},
initial: 'selecting',
entry: 'sendSelectionEvent',
exit: ['clearContext'],
},
deletingOperation: {
states: {
selecting: {

View File

@ -47,12 +47,10 @@ import { angleLengthInfo } from '@src/components/Toolbar/angleLengthInfo'
import { createLiteral, createLocalName } from '@src/lang/create'
import { updateModelingState } from '@src/lang/modelingWorkflows'
import {
addClone,
addHelix,
addOffsetPlane,
addShell,
insertNamedConstant,
insertVariableAndOffsetPathToNode,
replaceValueAtNodePath,
} from '@src/lang/modifyAst'
import type {
@ -72,7 +70,7 @@ import {
addRevolve,
addSweep,
getAxisExpressionAndIndex,
} from '@src/lang/modifyAst/addSweep'
} from '@src/lang/modifyAst/sweeps'
import {
applyIntersectFromTargetOperatorSelections,
applySubtractFromTargetOperatorSelections,
@ -84,15 +82,13 @@ import {
} from '@src/lang/modifyAst/deleteSelection'
import { setAppearance } from '@src/lang/modifyAst/setAppearance'
import {
setTranslate,
setRotate,
insertExpressionNode,
retrievePathToNodeFromTransformSelection,
} from '@src/lang/modifyAst/setTransform'
addTranslate,
addRotate,
addScale,
addClone,
} from '@src/lang/modifyAst/transforms'
import {
getNodeFromPath,
findPipesWithImportAlias,
findImportNodeAndAlias,
isNodeSafeToReplacePath,
stringifyPathToNode,
updatePathToNodesAfterEdit,
@ -115,7 +111,6 @@ import type {
Literal,
Name,
PathToNode,
PipeExpression,
Program,
VariableDeclaration,
VariableDeclarator,
@ -143,7 +138,6 @@ import {
import type { ToolbarModeName } from '@src/lib/toolbar'
import { err, reportRejection, trap } from '@src/lib/trap'
import { uuidv4 } from '@src/lib/utils'
import type { ImportStatement } from '@rust/kcl-lib/bindings/ImportStatement'
import { isDesktop } from '@src/lib/isDesktop'
import {
crossProduct,
@ -403,6 +397,7 @@ export type ModelingMachineEvent =
| { type: 'Appearance'; data: ModelingCommandSchema['Appearance'] }
| { type: 'Translate'; data: ModelingCommandSchema['Translate'] }
| { type: 'Rotate'; data: ModelingCommandSchema['Rotate'] }
| { type: 'Scale'; data: ModelingCommandSchema['Scale'] }
| { type: 'Clone'; data: ModelingCommandSchema['Clone'] }
| {
type:
@ -3317,57 +3312,11 @@ export const modelingMachine = setup({
}
const ast = kclManager.ast
const modifiedAst = structuredClone(ast)
const { x, y, z, nodeToEdit, selection } = input
let pathToNode = nodeToEdit
if (!(pathToNode && typeof pathToNode[1][0] === 'number')) {
const result = retrievePathToNodeFromTransformSelection(
selection,
kclManager.artifactGraph,
ast
)
if (err(result)) {
return Promise.reject(result)
}
pathToNode = result
}
// Look for the last pipe with the import alias and a call to translate, with a fallback to rotate.
// Otherwise create one
const importNodeAndAlias = findImportNodeAndAlias(ast, pathToNode)
if (importNodeAndAlias) {
const pipes = findPipesWithImportAlias(ast, pathToNode, 'translate')
const lastPipe = pipes.at(-1)
if (lastPipe && lastPipe.pathToNode) {
pathToNode = lastPipe.pathToNode
} else {
const otherRelevantPipes = findPipesWithImportAlias(
ast,
pathToNode,
'rotate'
)
const lastRelevantPipe = otherRelevantPipes.at(-1)
if (lastRelevantPipe && lastRelevantPipe.pathToNode) {
pathToNode = lastRelevantPipe.pathToNode
} else {
pathToNode = insertExpressionNode(
modifiedAst,
importNodeAndAlias.alias
)
}
}
}
insertVariableAndOffsetPathToNode(x, modifiedAst, pathToNode)
insertVariableAndOffsetPathToNode(y, modifiedAst, pathToNode)
insertVariableAndOffsetPathToNode(z, modifiedAst, pathToNode)
const result = setTranslate({
pathToNode,
modifiedAst,
x: valueOrVariable(x),
y: valueOrVariable(y),
z: valueOrVariable(z),
const artifactGraph = kclManager.artifactGraph
const result = addTranslate({
...input,
ast,
artifactGraph,
})
if (err(result)) {
return Promise.reject(result)
@ -3398,57 +3347,46 @@ export const modelingMachine = setup({
}
const ast = kclManager.ast
const modifiedAst = structuredClone(ast)
const { roll, pitch, yaw, nodeToEdit, selection } = input
let pathToNode = nodeToEdit
if (!(pathToNode && typeof pathToNode[1][0] === 'number')) {
const result = retrievePathToNodeFromTransformSelection(
selection,
kclManager.artifactGraph,
ast
)
if (err(result)) {
return Promise.reject(result)
}
pathToNode = result
const artifactGraph = kclManager.artifactGraph
const result = addRotate({
...input,
ast,
artifactGraph,
})
if (err(result)) {
return Promise.reject(result)
}
// Look for the last pipe with the import alias and a call to rotate, with a fallback to translate.
// Otherwise create one
const importNodeAndAlias = findImportNodeAndAlias(ast, pathToNode)
if (importNodeAndAlias) {
const pipes = findPipesWithImportAlias(ast, pathToNode, 'rotate')
const lastPipe = pipes.at(-1)
if (lastPipe && lastPipe.pathToNode) {
pathToNode = lastPipe.pathToNode
} else {
const otherRelevantPipes = findPipesWithImportAlias(
ast,
pathToNode,
'translate'
)
const lastRelevantPipe = otherRelevantPipes.at(-1)
if (lastRelevantPipe && lastRelevantPipe.pathToNode) {
pathToNode = lastRelevantPipe.pathToNode
} else {
pathToNode = insertExpressionNode(
modifiedAst,
importNodeAndAlias.alias
)
}
await updateModelingState(
result.modifiedAst,
EXECUTION_TYPE_REAL,
{
kclManager,
editorManager,
codeManager,
},
{
focusPath: [result.pathToNode],
}
)
}
),
scaleAstMod: fromPromise(
async ({
input,
}: {
input: ModelingCommandSchema['Scale'] | undefined
}) => {
if (!input) {
return Promise.reject(new Error(NO_INPUT_PROVIDED_MESSAGE))
}
insertVariableAndOffsetPathToNode(roll, modifiedAst, pathToNode)
insertVariableAndOffsetPathToNode(pitch, modifiedAst, pathToNode)
insertVariableAndOffsetPathToNode(yaw, modifiedAst, pathToNode)
const result = setRotate({
pathToNode,
modifiedAst,
roll: valueOrVariable(roll),
pitch: valueOrVariable(pitch),
yaw: valueOrVariable(yaw),
const ast = kclManager.ast
const artifactGraph = kclManager.artifactGraph
const result = addScale({
...input,
ast,
artifactGraph,
})
if (err(result)) {
return Promise.reject(result)
@ -3479,58 +3417,14 @@ export const modelingMachine = setup({
}
const ast = kclManager.ast
const { nodeToEdit, selection, variableName } = input
let pathToNode = nodeToEdit
if (!(pathToNode && typeof pathToNode[1][0] === 'number')) {
const result = retrievePathToNodeFromTransformSelection(
selection,
kclManager.artifactGraph,
ast
)
if (err(result)) {
return Promise.reject(result)
}
pathToNode = result
}
const returnEarly = true
const geometryNode = getNodeFromPath<
VariableDeclaration | ImportStatement | PipeExpression
>(
ast,
pathToNode,
['VariableDeclaration', 'ImportStatement', 'PipeExpression'],
returnEarly
)
if (err(geometryNode)) {
return Promise.reject(
new Error("Couldn't find corresponding path to node")
)
}
let geometryName: string | undefined
if (geometryNode.node.type === 'VariableDeclaration') {
geometryName = geometryNode.node.declaration.id.name
} else if (
geometryNode.node.type === 'ImportStatement' &&
geometryNode.node.selector.type === 'None' &&
geometryNode.node.selector.alias
) {
geometryName = geometryNode.node.selector.alias?.name
} else {
return Promise.reject(
new Error("Couldn't find corresponding geometry")
)
}
const artifactGraph = kclManager.artifactGraph
const result = addClone({
...input,
ast,
geometryName,
variableName,
artifactGraph,
})
if (err(result)) {
return Promise.reject(err(result))
return Promise.reject(result)
}
await updateModelingState(
@ -3863,6 +3757,12 @@ export const modelingMachine = setup({
guard: 'no kcl errors',
},
Scale: {
target: 'Applying scale',
reenter: true,
guard: 'no kcl errors',
},
Clone: {
target: 'Applying clone',
reenter: true,
@ -5345,6 +5245,22 @@ export const modelingMachine = setup({
},
},
'Applying scale': {
invoke: {
src: 'scaleAstMod',
id: 'scaleAstMod',
input: ({ event }) => {
if (event.type !== 'Scale') return undefined
return event.data
},
onDone: ['idle'],
onError: {
target: 'idle',
actions: 'toastError',
},
},
},
'Applying clone': {
invoke: {
src: 'cloneAstMod',