Compare commits
34 Commits
main
...
pierremtb/
Author | SHA1 | Date | |
---|---|---|---|
3ae4b045ad | |||
cd672d52f6 | |||
30c4f79285 | |||
c3c8b727bd | |||
65168eb139 | |||
6a9870b6f3 | |||
78f885c3d1 | |||
a40ba06641 | |||
c035398ad7 | |||
1387c2fbcb | |||
a9f95f7574 | |||
b2b7ac5bf8 | |||
172e01529c | |||
5a4a32c044 | |||
b955184191 | |||
d7914219da | |||
ead4c1286b | |||
a0fe33260e | |||
8955b5fcd3 | |||
5708b8c64b | |||
5b8284e737 | |||
dd9b0ec5f0 | |||
c467568ee4 | |||
cb976ec31b | |||
cc9eb65456 | |||
298583181b | |||
a589f56e73 | |||
e668cb55f9 | |||
9942a65612 | |||
187925ff21 | |||
4c1564e2b0 | |||
50ac0244fd | |||
8d7858978f | |||
74d73ece7b |
@ -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}
|
||||
|
@ -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)`)
|
||||
})
|
||||
})
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
252
src/lang/modifyAst/sweeps.test.ts
Normal file
252
src/lang/modifyAst/sweeps.test.ts
Normal 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
|
||||
})
|
@ -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,
|
127
src/lang/modifyAst/transforms.test.ts
Normal file
127
src/lang/modifyAst/transforms.test.ts
Normal 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
|
||||
})
|
259
src/lang/modifyAst/transforms.ts
Normal file
259
src/lang/modifyAst/transforms.ts
Normal 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,
|
||||
}
|
||||
}
|
@ -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', ''],
|
||||
])
|
||||
})
|
||||
})
|
||||
|
@ -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
|
||||
|
@ -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),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -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: () =>
|
||||
|
@ -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: {
|
||||
|
@ -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',
|
||||
|
Reference in New Issue
Block a user