Assemblies: Point-and-click Clone (#6478)

* WIP: Assemblies: Point-and-click Clone
Fixes #6209

* Make assemblies commands available for release
Fixes #6497

* Break insert out of the group, new icon, add Clone disabled

* Add rotate thanks to @franknoirot

* Update relevant snapshots

* Fix pathToNode

* Add clone to stdlibmap

* Cleaned more things

* Add custom icon

* Add variable name for clone

* Add e2e test for import and translated import

* Remove stale comment

* Add test for selection based extrude clone

* First batch of suggestions from @lee-at-zoo-corp, tysm!

* Clean up test names

* Second batch of @lee-at-zoo-corp's suggestion for modelingMachine error handling
This commit is contained in:
Pierre Jacquier
2025-04-26 18:26:39 -04:00
committed by GitHub
parent 25bb95a66e
commit d7e80b3cc7
14 changed files with 691 additions and 46 deletions

View File

@ -45,6 +45,7 @@ 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,
@ -99,9 +100,11 @@ import type {
CallExpression,
CallExpressionKw,
Expr,
ExpressionStatement,
Literal,
Name,
PathToNode,
PipeExpression,
VariableDeclaration,
VariableDeclarator,
} from '@src/lang/wasm'
@ -379,6 +382,7 @@ export type ModelingMachineEvent =
| { type: 'Appearance'; data: ModelingCommandSchema['Appearance'] }
| { type: 'Translate'; data: ModelingCommandSchema['Translate'] }
| { type: 'Rotate'; data: ModelingCommandSchema['Rotate'] }
| { type: 'Clone'; data: ModelingCommandSchema['Clone'] }
| {
type:
| 'Add circle origin'
@ -2865,6 +2869,96 @@ export const modelingMachine = setup({
)
}
),
cloneAstMod: fromPromise(
async ({
input,
}: {
input: ModelingCommandSchema['Clone'] | undefined
}) => {
if (!input) return Promise.reject(new Error('No input provided'))
const ast = kclManager.ast
const { nodeToEdit, selection, variableName } = input
let pathToNode = nodeToEdit
if (!(pathToNode && typeof pathToNode[1][0] === 'number')) {
if (selection?.graphSelections[0].artifact) {
const children = findAllChildrenAndOrderByPlaceInCode(
selection?.graphSelections[0].artifact,
kclManager.artifactGraph
)
const variable = getLastVariable(children, ast)
if (!variable) {
return Promise.reject(
new Error("Couldn't find corresponding path to node")
)
}
pathToNode = variable.pathToNode
} else if (selection?.graphSelections[0].codeRef.pathToNode) {
pathToNode = selection?.graphSelections[0].codeRef.pathToNode
} else {
return Promise.reject(
new Error("Couldn't find corresponding path to node")
)
}
}
const returnEarly = true
const geometryNode = getNodeFromPath<
VariableDeclaration | ExpressionStatement | PipeExpression
>(
ast,
pathToNode,
['VariableDeclaration', 'ExpressionStatement', '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 === 'ExpressionStatement' &&
geometryNode.node.expression.type === 'Name'
) {
geometryName = geometryNode.node.expression.name.name
} else if (
geometryNode.node.type === 'ExpressionStatement' &&
geometryNode.node.expression.type === 'PipeExpression' &&
geometryNode.node.expression.body[0].type === 'Name'
) {
geometryName = geometryNode.node.expression.body[0].name.name
} else {
return Promise.reject(
new Error("Couldn't find corresponding geometry")
)
}
const result = addClone({
ast,
geometryName,
variableName,
})
if (err(result)) {
return Promise.reject(err(result))
}
await updateModelingState(
result.modifiedAst,
EXECUTION_TYPE_REAL,
{
kclManager,
editorManager,
codeManager,
},
{
focusPath: [result.pathToNode],
}
)
}
),
exportFromEngine: fromPromise(
async ({}: { input?: ModelingCommandSchema['Export'] }) => {
return undefined as Error | undefined
@ -3144,6 +3238,11 @@ export const modelingMachine = setup({
reenter: true,
},
Clone: {
target: 'Applying clone',
reenter: true,
},
'Boolean Subtract': 'Boolean subtracting',
'Boolean Union': 'Boolean uniting',
'Boolean Intersect': 'Boolean intersecting',
@ -4582,6 +4681,22 @@ export const modelingMachine = setup({
},
},
'Applying clone': {
invoke: {
src: 'cloneAstMod',
id: 'cloneAstMod',
input: ({ event }) => {
if (event.type !== 'Clone') return undefined
return event.data
},
onDone: ['idle'],
onError: {
target: 'idle',
actions: 'toastError',
},
},
},
Exporting: {
invoke: {
src: 'exportFromEngine',