Compare commits

...

17 Commits

Author SHA1 Message Date
792a101a2f Merge branch 'max/undeletable-unassigned-fillet' of https://github.com/KittyCAD/modeling-app into max/undeletable-unassigned-fillet 2025-05-10 18:36:16 +02:00
b0371ccc20 edit the comment 2025-05-10 18:36:04 +02:00
max
f5fdad05a4 Merge branch 'main' into max/undeletable-unassigned-fillet 2025-05-10 18:29:30 +02:00
19d4ad6332 fmt 2025-05-10 18:29:09 +02:00
66fba3b6fb locateExtrudeDeclarator > locateVariableWithCallOrPipe 2025-05-10 18:28:40 +02:00
dc49105606 Merge branch 'max/undeletable-unassigned-fillet' of https://github.com/KittyCAD/modeling-app into max/undeletable-unassigned-fillet 2025-05-10 17:59:27 +02:00
4434e8abbe unfuck circular dep - move locateExtrudeDeclarator 2025-05-10 17:59:24 +02:00
max
1da7d09ab3 Merge branch 'main' into max/undeletable-unassigned-fillet 2025-05-10 12:39:40 +02:00
a2343fcfda scene.settled instead of page.waitForTimeout 2025-05-10 12:39:21 +02:00
f6a02357ca add playwright test for chamfers 2025-05-10 12:32:02 +02:00
max
6875ad5022 Merge branch 'main' into max/undeletable-unassigned-fillet 2025-05-09 22:20:05 +02:00
d2c5e7d6ce typos 2025-05-09 22:06:51 +02:00
51e1e940d3 astMod edits 2025-05-09 22:06:33 +02:00
17d0f8cf30 little swap 2025-05-09 21:58:53 +02:00
8ccc5653ab deleteTopLevelStatement 2025-05-09 21:58:40 +02:00
0c0165d515 tests 2025-05-09 21:54:53 +02:00
168672588d oops, make it nicer for no reason 2025-05-09 12:56:13 +02:00
7 changed files with 264 additions and 216 deletions

View File

@ -2388,6 +2388,7 @@ fillet001 = fillet(extrude001, radius = 5, tags = [getOppositeEdge(seg01)])
scene,
editor,
toolbar,
cmdBar,
}) => {
// Code samples
const initialCode = `sketch001 = startSketchOn(XY)
@ -2401,14 +2402,14 @@ extrude001 = extrude(sketch001, length = -12)
|> fillet(radius = 5, tags = [seg01]) // fillet01
|> fillet(radius = 5, tags = [seg02]) // fillet02
fillet03 = fillet(extrude001, radius = 5, tags = [getOppositeEdge(seg01)])
fillet04 = fillet(extrude001, radius = 5, tags = [getOppositeEdge(seg02)])
fillet(extrude001, radius = 5, tags = [getOppositeEdge(seg02)])
`
const pipedFilletDeclaration = 'fillet(radius = 5, tags = [seg01])'
const firstPipedFilletDeclaration = 'fillet(radius = 5, tags = [seg01])'
const secondPipedFilletDeclaration = 'fillet(radius = 5, tags = [seg02])'
const standaloneFilletDeclaration =
const standaloneAssignedFilletDeclaration =
'fillet03 = fillet(extrude001, radius = 5, tags = [getOppositeEdge(seg01)])'
const secondStandaloneFilletDeclaration =
'fillet04 = fillet(extrude001, radius = 5, tags = [getOppositeEdge(seg02)])'
const standaloneUnassignedFilletDeclaration =
'fillet(extrude001, radius = 5, tags = [getOppositeEdge(seg02)])'
// Locators
const pipedFilletEdgeLocation = { x: 600, y: 193 }
@ -2430,6 +2431,7 @@ fillet04 = fillet(extrude001, radius = 5, tags = [getOppositeEdge(seg02)])
}, initialCode)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.settled(cmdBar)
// verify modeling scene is loaded
await scene.expectPixelColor(
@ -2446,15 +2448,19 @@ fillet04 = fillet(extrude001, radius = 5, tags = [getOppositeEdge(seg02)])
await test.step('Delete fillet via feature tree selection', async () => {
await test.step('Open Feature Tree Pane', async () => {
await toolbar.openPane('feature-tree')
await page.waitForTimeout(500)
await scene.settled(cmdBar)
})
await test.step('Delete piped fillet via feature tree selection', async () => {
await test.step('Verify all fillets are present in the editor', async () => {
await editor.expectEditor.toContain(pipedFilletDeclaration)
await editor.expectEditor.toContain(firstPipedFilletDeclaration)
await editor.expectEditor.toContain(secondPipedFilletDeclaration)
await editor.expectEditor.toContain(standaloneFilletDeclaration)
await editor.expectEditor.toContain(secondStandaloneFilletDeclaration)
await editor.expectEditor.toContain(
standaloneAssignedFilletDeclaration
)
await editor.expectEditor.toContain(
standaloneUnassignedFilletDeclaration
)
})
await test.step('Verify test fillets are present in the scene', async () => {
await scene.expectPixelColor(
@ -2475,13 +2481,17 @@ fillet04 = fillet(extrude001, radius = 5, tags = [getOppositeEdge(seg02)])
)
await operationButton.click({ button: 'left' })
await page.keyboard.press('Delete')
await page.waitForTimeout(500)
await scene.settled(cmdBar)
})
await test.step('Verify piped fillet is deleted but other fillets are not (in the editor)', async () => {
await editor.expectEditor.not.toContain(pipedFilletDeclaration)
await editor.expectEditor.not.toContain(firstPipedFilletDeclaration)
await editor.expectEditor.toContain(secondPipedFilletDeclaration)
await editor.expectEditor.toContain(standaloneFilletDeclaration)
await editor.expectEditor.toContain(secondStandaloneFilletDeclaration)
await editor.expectEditor.toContain(
standaloneAssignedFilletDeclaration
)
await editor.expectEditor.toContain(
standaloneUnassignedFilletDeclaration
)
})
await test.step('Verify piped fillet is deleted but non-piped is not (in the scene)', async () => {
await scene.expectPixelColor(
@ -2497,22 +2507,51 @@ fillet04 = fillet(extrude001, radius = 5, tags = [getOppositeEdge(seg02)])
})
})
await test.step('Delete non-piped fillet via feature tree selection', async () => {
await test.step('Delete non-piped fillet', async () => {
await test.step('Delete standalone assigned fillet via feature tree selection', async () => {
await test.step('Delete standalone assigned fillet', async () => {
const operationButton = await toolbar.getFeatureTreeOperation(
'Fillet',
1
)
await operationButton.click({ button: 'left' })
await page.keyboard.press('Delete')
await page.waitForTimeout(500)
await scene.settled(cmdBar)
})
await test.step('Verify non-piped fillet is deleted but other two fillets are not (in the editor)', async () => {
await test.step('Verify standalone assigned fillet is deleted but other two fillets are not (in the editor)', async () => {
await editor.expectEditor.toContain(secondPipedFilletDeclaration)
await editor.expectEditor.not.toContain(standaloneFilletDeclaration)
await editor.expectEditor.toContain(secondStandaloneFilletDeclaration)
await editor.expectEditor.not.toContain(
standaloneAssignedFilletDeclaration
)
await editor.expectEditor.toContain(
standaloneUnassignedFilletDeclaration
)
})
await test.step('Verify non-piped fillet is deleted but piped is not (in the scene)', async () => {
await test.step('Verify standalone assigned fillet is deleted but piped is not (in the scene)', async () => {
await scene.expectPixelColor(
edgeColorWhite,
standaloneFilletEdgeLocation,
lowTolerance
)
})
})
await test.step('Delete standalone unassigned fillet via feature tree selection', async () => {
await test.step('Delete standalone unassigned fillet', async () => {
const operationButton = await toolbar.getFeatureTreeOperation(
'Fillet',
1
)
await operationButton.click({ button: 'left' })
await page.keyboard.press('Delete')
await scene.settled(cmdBar)
})
await test.step('Verify standalone unassigned fillet is deleted but other fillet is not (in the editor)', async () => {
await editor.expectEditor.toContain(secondPipedFilletDeclaration)
await editor.expectEditor.not.toContain(
standaloneUnassignedFilletDeclaration
)
})
await test.step('Verify standalone unassigned fillet is deleted but piped is not (in the scene)', async () => {
await scene.expectPixelColor(
edgeColorWhite,
standaloneFilletEdgeLocation,
@ -2964,14 +3003,14 @@ extrude001 = extrude(sketch001, length = -12)
|> chamfer(length = 5, tags = [seg01]) // chamfer01
|> chamfer(length = 5, tags = [seg02]) // chamfer02
chamfer03 = chamfer(extrude001, length = 5, tags = [getOppositeEdge(seg01)])
chamfer04 = chamfer(extrude001, length = 5, tags = [getOppositeEdge(seg02)])
chamfer(extrude001, length = 5, tags = [getOppositeEdge(seg02)])
`
const pipedChamferDeclaration = 'chamfer(length = 5, tags = [seg01])'
const firstPipedChamferDeclaration = 'chamfer(length = 5, tags = [seg01])'
const secondPipedChamferDeclaration = 'chamfer(length = 5, tags = [seg02])'
const standaloneChamferDeclaration =
const standaloneAssignedChamferDeclaration =
'chamfer03 = chamfer(extrude001, length = 5, tags = [getOppositeEdge(seg01)])'
const secondStandaloneChamferDeclaration =
'chamfer04 = chamfer(extrude001, length = 5, tags = [getOppositeEdge(seg02)])'
const standaloneUnassignedChamferDeclaration =
'chamfer(extrude001, length = 5, tags = [getOppositeEdge(seg02)])'
// Locators
const pipedChamferEdgeLocation = { x: 600, y: 193 }
@ -3010,16 +3049,18 @@ chamfer04 = chamfer(extrude001, length = 5, tags = [getOppositeEdge(seg02)])
await test.step('Delete chamfer via feature tree selection', async () => {
await test.step('Open Feature Tree Pane', async () => {
await toolbar.openPane('feature-tree')
await page.waitForTimeout(500)
await scene.settled(cmdBar)
})
await test.step('Delete piped chamfer via feature tree selection', async () => {
await test.step('Verify all chamfers are present in the editor', async () => {
await editor.expectEditor.toContain(pipedChamferDeclaration)
await editor.expectEditor.toContain(firstPipedChamferDeclaration)
await editor.expectEditor.toContain(secondPipedChamferDeclaration)
await editor.expectEditor.toContain(standaloneChamferDeclaration)
await editor.expectEditor.toContain(
secondStandaloneChamferDeclaration
standaloneAssignedChamferDeclaration
)
await editor.expectEditor.toContain(
standaloneUnassignedChamferDeclaration
)
})
await test.step('Verify test chamfers are present in the scene', async () => {
@ -3041,14 +3082,16 @@ chamfer04 = chamfer(extrude001, length = 5, tags = [getOppositeEdge(seg02)])
)
await operationButton.click({ button: 'left' })
await page.keyboard.press('Delete')
await page.waitForTimeout(500)
await scene.settled(cmdBar)
})
await test.step('Verify piped chamfer is deleted but other chamfers are not (in the editor)', async () => {
await editor.expectEditor.not.toContain(pipedChamferDeclaration)
await editor.expectEditor.not.toContain(firstPipedChamferDeclaration)
await editor.expectEditor.toContain(secondPipedChamferDeclaration)
await editor.expectEditor.toContain(standaloneChamferDeclaration)
await editor.expectEditor.toContain(
secondStandaloneChamferDeclaration
standaloneAssignedChamferDeclaration
)
await editor.expectEditor.toContain(
standaloneUnassignedChamferDeclaration
)
})
await test.step('Verify piped chamfer is deleted but non-piped is not (in the scene)', async () => {
@ -3065,24 +3108,51 @@ chamfer04 = chamfer(extrude001, length = 5, tags = [getOppositeEdge(seg02)])
})
})
await test.step('Delete non-piped chamfer via feature tree selection', async () => {
await test.step('Delete non-piped chamfer', async () => {
await test.step('Delete standalone assigned chamfer via feature tree selection', async () => {
await test.step('Delete standalone assigned chamfer', async () => {
const operationButton = await toolbar.getFeatureTreeOperation(
'Chamfer',
1
)
await operationButton.click({ button: 'left' })
await page.keyboard.press('Delete')
await page.waitForTimeout(500)
await scene.settled(cmdBar)
})
await test.step('Verify non-piped chamfer is deleted but other two chamfers are not (in the editor)', async () => {
await test.step('Verify standalone assigned chamfer is deleted but other two chamfers are not (in the editor)', async () => {
await editor.expectEditor.toContain(secondPipedChamferDeclaration)
await editor.expectEditor.not.toContain(standaloneChamferDeclaration)
await editor.expectEditor.not.toContain(
standaloneAssignedChamferDeclaration
)
await editor.expectEditor.toContain(
secondStandaloneChamferDeclaration
standaloneUnassignedChamferDeclaration
)
})
await test.step('Verify non-piped chamfer is deleted but piped is not (in the scene)', async () => {
await test.step('Verify standalone assigned chamfer is deleted but piped is not (in the scene)', async () => {
await scene.expectPixelColor(
edgeColorWhite,
standaloneChamferEdgeLocation,
lowTolerance
)
})
})
await test.step('Delete standalone unassigned chamfer via feature tree selection', async () => {
await test.step('Delete standalone unassigned chamfer', async () => {
const operationButton = await toolbar.getFeatureTreeOperation(
'Chamfer',
1
)
await operationButton.click({ button: 'left' })
await page.keyboard.press('Delete')
await scene.settled(cmdBar)
})
await test.step('Verify standalone unassigned chamfer is deleted but piped chamfer is not (in the editor)', async () => {
await editor.expectEditor.toContain(secondPipedChamferDeclaration)
await editor.expectEditor.not.toContain(
standaloneUnassignedChamferDeclaration
)
})
await test.step('Verify standalone unassigned chamfer is deleted but piped is not (in the scene)', async () => {
await scene.expectPixelColor(
edgeColorWhite,
standaloneChamferEdgeLocation,

View File

@ -64,7 +64,7 @@ import { KCL_DEFAULT_CONSTANT_PREFIXES } from '@src/lib/constants'
import type { DefaultPlaneStr } from '@src/lib/planes'
import { err, trap } from '@src/lib/trap'
import { isOverlap, roundOff } from '@src/lib/utils'
import { isArray, isOverlap, roundOff } from '@src/lib/utils'
import type { ExtrudeFacePlane } from '@src/machines/modelingMachine'
import { ARG_AT } from '@src/lang/constants'
@ -949,6 +949,27 @@ export function deleteSegmentFromPipeExpression(
return _modifiedAst
}
/**
* Deletes a standalone top level statement from the AST
* Used for removing both unassigned statements and variable declarations
*
* @param ast The AST to modify
* @param pathToNode The path to the node to delete
*/
export function deleteTopLevelStatement(
ast: Node<Program>,
pathToNode: PathToNode
): Error | void {
const pathStep = pathToNode[1]
if (!isArray(pathStep) || typeof pathStep[0] !== 'number') {
return new Error(
'Invalid pathToNode structure: expected a number at path[1][0]'
)
}
const statementIndex: number = pathStep[0]
ast.body.splice(statementIndex, 1)
}
export function removeSingleConstraintInfo(
pathToCallExp: PathToNode,
argDetails: SimplifiedArgDetails,

View File

@ -801,7 +801,7 @@ extrude001 = extrude(sketch001, length = -15)`
expectedCode
)
}, 10_000)
it(`should delete a non-piped ${edgeTreatmentType} from a single segment`, async () => {
it(`should delete a standalone assigned ${edgeTreatmentType} from a single segment`, async () => {
const code = `sketch001 = startSketchOn(XY)
|> startProfile(at = [-10, 10])
|> line(end = [20, 0])
@ -810,8 +810,34 @@ extrude001 = extrude(sketch001, length = -15)`
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
extrude001 = extrude(sketch001, length = -15)
fillet001 = ${edgeTreatmentType}(extrude001, ${parameterName} = 3, tags = [seg01])`
const edgeTreatmentSnippet = `fillet001 = ${edgeTreatmentType}(extrude001, ${parameterName} = 3, tags = [seg01])`
${edgeTreatmentType}001 = ${edgeTreatmentType}(extrude001, ${parameterName} = 3, tags = [seg01])`
const edgeTreatmentSnippet = `${edgeTreatmentType}001 = ${edgeTreatmentType}(extrude001, ${parameterName} = 3, tags = [seg01])`
const expectedCode = `sketch001 = startSketchOn(XY)
|> startProfile(at = [-10, 10])
|> line(end = [20, 0])
|> line(end = [0, -20])
|> line(end = [-20, 0], tag = $seg01)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
extrude001 = extrude(sketch001, length = -15)`
await runDeleteEdgeTreatmentTest(
code,
edgeTreatmentSnippet,
expectedCode
)
}, 10_000)
it(`should delete a standalone ${edgeTreatmentType} without assignment from a single segment`, async () => {
const code = `sketch001 = startSketchOn(XY)
|> startProfile(at = [-10, 10])
|> line(end = [20, 0])
|> line(end = [0, -20])
|> line(end = [-20, 0], tag = $seg01)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
extrude001 = extrude(sketch001, length = -15)
${edgeTreatmentType}(extrude001, ${parameterName} = 5, tags = [seg01])`
const edgeTreatmentSnippet = `${edgeTreatmentType}(extrude001, ${parameterName} = 5, tags = [seg01])`
const expectedCode = `sketch001 = startSketchOn(XY)
|> startProfile(at = [-10, 10])
|> line(end = [20, 0])

View File

@ -16,6 +16,7 @@ import {
getNodeFromPath,
hasSketchPipeBeenExtruded,
traverse,
locateVariableWithCallOrPipe,
} from '@src/lang/queryAst'
import { getNodePathFromSourceRange } from '@src/lang/queryAstNodePathUtils'
import type { Artifact } from '@src/lang/std/artifactGraph'
@ -26,25 +27,25 @@ import {
sketchLineHelperMapKw,
} from '@src/lang/std/sketch'
import { findKwArg } from '@src/lang/util'
import type {
ArtifactGraph,
CallExpressionKw,
Expr,
ObjectExpression,
PathToNode,
PipeExpression,
Program,
VariableDeclaration,
VariableDeclarator,
import {
type ArtifactGraph,
type CallExpressionKw,
type Expr,
type ObjectExpression,
type PathToNode,
type Program,
type VariableDeclarator,
type ExpressionStatement,
} from '@src/lang/wasm'
import type { KclCommandValue } from '@src/lib/commandTypes'
import type { Selection, Selections } from '@src/lib/selections'
import { err } from '@src/lib/trap'
import { isArray } from '@src/lib/utils'
import {
createTagExpressions,
modifyAstWithTagsForSelection,
} from '@src/lang/modifyAst/tagManagement'
import { deleteNodeInExtrudePipe } from '@src/lang/modifyAst/deleteNodeInExtrudePipe'
import { deleteTopLevelStatement } from '@src/lang/modifyAst'
// Edge Treatment Types
export enum EdgeTreatmentType {
@ -164,12 +165,12 @@ export async function modifyAstWithEdgeTreatmentAndTag(
)
// Locate the extrude call
const locatedExtrudeDeclarator = locateExtrudeDeclarator(
const locatedExtrudeDeclarator = locateVariableWithCallOrPipe(
clonedAst,
pathToExtrudeNode
)
if (err(locatedExtrudeDeclarator)) return locatedExtrudeDeclarator
const { extrudeDeclarator } = locatedExtrudeDeclarator
const { variableDeclarator } = locatedExtrudeDeclarator
// Modify the extrude expression to include this edge treatment expression
// CallExpression - no edge treatment
@ -177,33 +178,33 @@ export async function modifyAstWithEdgeTreatmentAndTag(
let pathToEdgeTreatmentNode: PathToNode
if (extrudeDeclarator.init.type === 'CallExpressionKw') {
if (variableDeclarator.init.type === 'CallExpressionKw') {
// 1. case when no edge treatment exists
// modify ast with new edge treatment call by mutating the extrude node
extrudeDeclarator.init = createPipeExpression([
extrudeDeclarator.init,
variableDeclarator.init = createPipeExpression([
variableDeclarator.init,
edgeTreatmentCall,
])
// get path to the edge treatment node
pathToEdgeTreatmentNode = getPathToNodeOfEdgeTreatmentLiteral(
pathToExtrudeNode,
extrudeDeclarator,
variableDeclarator,
firstTag,
parameters
)
pathToEdgeTreatmentNodes.push(pathToEdgeTreatmentNode)
} else if (extrudeDeclarator.init.type === 'PipeExpression') {
} else if (variableDeclarator.init.type === 'PipeExpression') {
// 2. case when edge treatment exists or extrude in sketch pipe
// mutate the extrude node with the new edge treatment call
extrudeDeclarator.init.body.push(edgeTreatmentCall)
variableDeclarator.init.body.push(edgeTreatmentCall)
// get path to the edge treatment node
pathToEdgeTreatmentNode = getPathToNodeOfEdgeTreatmentLiteral(
pathToExtrudeNode,
extrudeDeclarator,
variableDeclarator,
firstTag,
parameters
)
@ -330,38 +331,6 @@ export function getEdgeTagCall(
return tagCall
}
export function locateExtrudeDeclarator(
node: Program,
pathToExtrudeNode: PathToNode
): { extrudeDeclarator: VariableDeclarator; shallowPath: PathToNode } | Error {
const nodeOfExtrudeCall = getNodeFromPath<VariableDeclaration>(
node,
pathToExtrudeNode,
'VariableDeclaration'
)
if (err(nodeOfExtrudeCall)) return nodeOfExtrudeCall
const { node: extrudeVarDecl } = nodeOfExtrudeCall
const extrudeDeclarator = extrudeVarDecl.declaration
if (!extrudeDeclarator) {
return new Error('Extrude Declarator not found.')
}
const extrudeInit = extrudeDeclarator?.init
if (!extrudeInit) {
return new Error('Extrude Init not found.')
}
if (
extrudeInit.type !== 'CallExpressionKw' &&
extrudeInit.type !== 'PipeExpression'
) {
return new Error('Extrude must be a PipeExpression or CallExpressionKw')
}
return { extrudeDeclarator, shallowPath: nodeOfExtrudeCall.shallowPath }
}
function getPathToNodeOfEdgeTreatmentLiteral(
pathToExtrudeNode: PathToNode,
extrudeDeclarator: VariableDeclarator,
@ -484,7 +453,7 @@ function getParameterNameAndValue(
: parameters.length.valueAst
return { parameterName: 'length', parameterValue }
} else {
return new Error('Unsupported edge treatment type}')
return new Error('Unsupported edge treatment type')
}
}
@ -614,14 +583,11 @@ export async function deleteEdgeTreatment(
selection: Selection
): Promise<Node<Program> | Error> {
/**
* Deletes an edge treatment (fillet or chamfer)
* from the AST based on the selection.
* Handles both standalone treatments
* and those within a PipeExpression.
* Deletes an edge treatment (fillet or chamfer) from the AST
*
* Supported cases:
* [+] fillet and chamfer
* [+] piped and non-piped edge treatments
* [+] piped, standalone (assigned and unassigned) edge treatments
* [-] delete single tag from array of tags (currently whole expression is deleted)
* [-] multiple selections with different edge treatments (currently single selection is supported)
*/
@ -632,119 +598,49 @@ export async function deleteEdgeTreatment(
return new Error('Selection is not an edge cut')
}
const { subType: edgeTreatmentType } = artifact
if (
!edgeTreatmentType ||
!['fillet', 'chamfer'].includes(edgeTreatmentType)
) {
const { subType } = artifact
if (!isEdgeTreatmentType(subType)) {
return new Error('Unsupported or missing edge treatment type')
}
// 2. Clone ast and retrieve the VariableDeclarator
// 2. Clone ast and retrieve the edge treatment node
const astClone = structuredClone(ast)
const varDec = getNodeFromPath<VariableDeclarator>(
ast,
selection?.codeRef?.pathToNode,
'VariableDeclarator'
)
if (err(varDec)) return varDec
const edgeTreatmentNode = getNodeFromPath<
VariableDeclarator | ExpressionStatement
>(astClone, selection?.codeRef?.pathToNode, [
'VariableDeclarator',
'ExpressionStatement',
])
if (err(edgeTreatmentNode)) return edgeTreatmentNode
// 3: Check if edge treatment is in a pipe
const inPipe = varDec.node.init.type === 'PipeExpression'
// 3: Delete edge treatments
// There 3 possible cases:
// - piped: const body = extrude(...) |> fillet(...)
// - assigned to variables: fillet0001 = fillet(...)
// - unassigned standalone statements: fillet(...)
// piped and assigned nodes are in the variable declarator
// unassigned nodes are in the expression statement
// 4A. Handle standalone edge treatment
if (!inPipe) {
const varDecPathStep = varDec.shallowPath[1]
if (!isArray(varDecPathStep) || typeof varDecPathStep[0] !== 'number') {
return new Error(
'Invalid shallowPath structure: expected a number at shallowPath[1][0]'
)
}
const varDecIndex: number = varDecPathStep[0]
// Remove entire VariableDeclarator from the ast
astClone.body.splice(varDecIndex, 1)
return astClone
}
// 4B. Handle edge treatment within pipe
if (inPipe) {
// Retrieve the CallExpression path
const callExp =
getNodeFromPath<CallExpressionKw>(
ast,
selection?.codeRef?.pathToNode,
'CallExpressionKw'
) ?? null
if (err(callExp)) return callExp
const shallowPath = callExp.shallowPath
// Initialize variables to hold the PipeExpression path and callIndex
let pipeExpressionPath: PathToNode | null = null
let callIndex: number | null = null
// Iterate through the shallowPath to find the PipeExpression and callIndex
for (let i = 0; i < shallowPath.length - 1; i++) {
const [key, value] = shallowPath[i]
if (key === 'body' && value === 'PipeExpression') {
pipeExpressionPath = shallowPath.slice(0, i + 1)
const nextStep = shallowPath[i + 1]
if (
nextStep &&
nextStep[1] === 'index' &&
typeof nextStep[0] === 'number'
edgeTreatmentNode.node.type === 'ExpressionStatement' || // unassigned
(edgeTreatmentNode.node.type === 'VariableDeclarator' && // assigned
edgeTreatmentNode.node.init?.type !== 'PipeExpression')
) {
callIndex = nextStep[0]
}
break
}
}
if (!pipeExpressionPath) {
return new Error('PipeExpression not found in path')
}
if (callIndex === null) {
return new Error('Failed to extract CallExpressionKw index')
}
// Retrieve the PipeExpression node
const pipeExpressionNode = getNodeFromPath<PipeExpression>(
// Handle both standalone cases (assigned and unassigned)
const deleteResult = deleteTopLevelStatement(
astClone,
pipeExpressionPath,
'PipeExpression'
selection.codeRef.pathToNode
)
if (err(pipeExpressionNode)) return pipeExpressionNode
// Ensure that the PipeExpression.body is an array
if (!isArray(pipeExpressionNode.node.body)) {
return new Error('PipeExpression body is not an array')
}
// Remove the CallExpression at the specified index
pipeExpressionNode.node.body.splice(callIndex, 1)
// Remove VariableDeclarator if PipeExpression.body is empty
if (pipeExpressionNode.node.body.length === 0) {
const varDecPathStep = varDec.shallowPath[1]
if (!isArray(varDecPathStep) || typeof varDecPathStep[0] !== 'number') {
return new Error(
'Invalid shallowPath structure: expected a number at shallowPath[1][0]'
if (err(deleteResult)) return deleteResult
return astClone
} else {
const deleteResult = deleteNodeInExtrudePipe(
astClone,
selection.codeRef.pathToNode
)
}
const varDecIndex: number = varDecPathStep[0]
astClone.body.splice(varDecIndex, 1)
}
if (err(deleteResult)) return deleteResult
return astClone
}
return Error('Delete fillets not implemented')
}
// Edit Edge Treatment
@ -786,7 +682,7 @@ export async function editEdgeTreatment(
edgeTreatmentCall.node.arguments[index] = newArg
}
let pathToEdgeTreatmentNode = selection?.codeRef?.pathToNode
const pathToEdgeTreatmentNode = selection?.codeRef?.pathToNode
return { modifiedAst, pathToEdgeTreatmentNode }
}

View File

@ -1,26 +1,26 @@
import type { Node } from '@rust/kcl-lib/bindings/Node'
import type { PathToNode, Program } from '@src/lang/wasm'
import { locateExtrudeDeclarator } from '@src/lang/modifyAst/addEdgeTreatment'
import { locateVariableWithCallOrPipe } from '@src/lang/queryAst'
import { err } from '@src/lib/trap'
export function deleteNodeInExtrudePipe(
node: PathToNode,
ast: Node<Program>
ast: Node<Program>,
node: PathToNode
): Error | void {
const pipeIndex = node.findIndex(([_, type]) => type === 'PipeExpression') + 1
if (!(node[pipeIndex][0] && typeof node[pipeIndex][0] === 'number')) {
return new Error("Couldn't find node to delete in ast")
}
const lookup = locateExtrudeDeclarator(ast, node)
const lookup = locateVariableWithCallOrPipe(ast, node)
if (err(lookup)) {
return lookup
}
if (lookup.extrudeDeclarator.init.type !== 'PipeExpression') {
if (lookup.variableDeclarator.init.type !== 'PipeExpression') {
return new Error("Couldn't find node to delete in looked up extrusion")
}
lookup.extrudeDeclarator.init.body.splice(node[pipeIndex][0], 1)
lookup.variableDeclarator.init.body.splice(node[pipeIndex][0], 1)
}

View File

@ -7,7 +7,7 @@ import {
createPipeExpression,
createPipeSubstitution,
} from '@src/lang/create'
import { locateExtrudeDeclarator } from '@src/lang/modifyAst/addEdgeTreatment'
import { locateVariableWithCallOrPipe } from '@src/lang/queryAst'
import type { PathToNode, Program } from '@src/lang/wasm'
import { COMMAND_APPEARANCE_COLOR_DEFAULT } from '@src/lib/commandBarConfigs/modelingCommandConfig'
import { err } from '@src/lib/trap'
@ -23,13 +23,13 @@ export function setAppearance({
}): Error | { modifiedAst: Node<Program>; pathToNode: PathToNode } {
const modifiedAst = structuredClone(ast)
// Locate the call (not necessarily an extrude here)
const result = locateExtrudeDeclarator(modifiedAst, nodeToEdit)
// Locate the call
const result = locateVariableWithCallOrPipe(modifiedAst, nodeToEdit)
if (err(result)) {
return result
}
const declarator = result.extrudeDeclarator
const declarator = result.variableDeclarator
const call = createCallExpressionStdLibKw(
'appearance',
createPipeSubstitution(),

View File

@ -1175,6 +1175,41 @@ export function getSketchSelectionsFromOperation(
}
}
export function locateVariableWithCallOrPipe(
ast: Program,
pathToNode: PathToNode
): { variableDeclarator: VariableDeclarator; shallowPath: PathToNode } | Error {
const variableDeclarationNode = getNodeFromPath<VariableDeclaration>(
ast,
pathToNode,
'VariableDeclaration'
)
if (err(variableDeclarationNode)) return variableDeclarationNode
const { node: variableDecl } = variableDeclarationNode
const variableDeclarator = variableDecl.declaration
if (!variableDeclarator) {
return new Error('Variable Declarator not found.')
}
const initializer = variableDeclarator?.init
if (!initializer) {
return new Error('Initializer not found.')
}
if (
initializer.type !== 'CallExpressionKw' &&
initializer.type !== 'PipeExpression'
) {
return new Error('Initializer must be a PipeExpression or CallExpressionKw')
}
return {
variableDeclarator,
shallowPath: variableDeclarationNode.shallowPath,
}
}
export function findImportNodeAndAlias(
ast: Node<Program>,
pathToNode: PathToNode