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

View File

@ -64,7 +64,7 @@ import { KCL_DEFAULT_CONSTANT_PREFIXES } from '@src/lib/constants'
import type { DefaultPlaneStr } from '@src/lib/planes' import type { DefaultPlaneStr } from '@src/lib/planes'
import { err, trap } from '@src/lib/trap' 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 type { ExtrudeFacePlane } from '@src/machines/modelingMachine'
import { ARG_AT } from '@src/lang/constants' import { ARG_AT } from '@src/lang/constants'
@ -949,6 +949,27 @@ export function deleteSegmentFromPipeExpression(
return _modifiedAst 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( export function removeSingleConstraintInfo(
pathToCallExp: PathToNode, pathToCallExp: PathToNode,
argDetails: SimplifiedArgDetails, argDetails: SimplifiedArgDetails,

View File

@ -801,7 +801,7 @@ extrude001 = extrude(sketch001, length = -15)`
expectedCode expectedCode
) )
}, 10_000) }, 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) const code = `sketch001 = startSketchOn(XY)
|> startProfile(at = [-10, 10]) |> startProfile(at = [-10, 10])
|> line(end = [20, 0]) |> line(end = [20, 0])
@ -810,8 +810,34 @@ extrude001 = extrude(sketch001, length = -15)`
|> line(endAbsolute = [profileStartX(%), profileStartY(%)]) |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close() |> close()
extrude001 = extrude(sketch001, length = -15) extrude001 = extrude(sketch001, length = -15)
fillet001 = ${edgeTreatmentType}(extrude001, ${parameterName} = 3, tags = [seg01])` ${edgeTreatmentType}001 = ${edgeTreatmentType}(extrude001, ${parameterName} = 3, tags = [seg01])`
const edgeTreatmentSnippet = `fillet001 = ${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) const expectedCode = `sketch001 = startSketchOn(XY)
|> startProfile(at = [-10, 10]) |> startProfile(at = [-10, 10])
|> line(end = [20, 0]) |> line(end = [20, 0])

View File

@ -16,6 +16,7 @@ import {
getNodeFromPath, getNodeFromPath,
hasSketchPipeBeenExtruded, hasSketchPipeBeenExtruded,
traverse, traverse,
locateVariableWithCallOrPipe,
} from '@src/lang/queryAst' } from '@src/lang/queryAst'
import { getNodePathFromSourceRange } from '@src/lang/queryAstNodePathUtils' import { getNodePathFromSourceRange } from '@src/lang/queryAstNodePathUtils'
import type { Artifact } from '@src/lang/std/artifactGraph' import type { Artifact } from '@src/lang/std/artifactGraph'
@ -26,25 +27,25 @@ import {
sketchLineHelperMapKw, sketchLineHelperMapKw,
} from '@src/lang/std/sketch' } from '@src/lang/std/sketch'
import { findKwArg } from '@src/lang/util' import { findKwArg } from '@src/lang/util'
import type { import {
ArtifactGraph, type ArtifactGraph,
CallExpressionKw, type CallExpressionKw,
Expr, type Expr,
ObjectExpression, type ObjectExpression,
PathToNode, type PathToNode,
PipeExpression, type Program,
Program, type VariableDeclarator,
VariableDeclaration, type ExpressionStatement,
VariableDeclarator,
} from '@src/lang/wasm' } from '@src/lang/wasm'
import type { KclCommandValue } from '@src/lib/commandTypes' import type { KclCommandValue } from '@src/lib/commandTypes'
import type { Selection, Selections } from '@src/lib/selections' import type { Selection, Selections } from '@src/lib/selections'
import { err } from '@src/lib/trap' import { err } from '@src/lib/trap'
import { isArray } from '@src/lib/utils'
import { import {
createTagExpressions, createTagExpressions,
modifyAstWithTagsForSelection, modifyAstWithTagsForSelection,
} from '@src/lang/modifyAst/tagManagement' } from '@src/lang/modifyAst/tagManagement'
import { deleteNodeInExtrudePipe } from '@src/lang/modifyAst/deleteNodeInExtrudePipe'
import { deleteTopLevelStatement } from '@src/lang/modifyAst'
// Edge Treatment Types // Edge Treatment Types
export enum EdgeTreatmentType { export enum EdgeTreatmentType {
@ -164,12 +165,12 @@ export async function modifyAstWithEdgeTreatmentAndTag(
) )
// Locate the extrude call // Locate the extrude call
const locatedExtrudeDeclarator = locateExtrudeDeclarator( const locatedExtrudeDeclarator = locateVariableWithCallOrPipe(
clonedAst, clonedAst,
pathToExtrudeNode pathToExtrudeNode
) )
if (err(locatedExtrudeDeclarator)) return locatedExtrudeDeclarator if (err(locatedExtrudeDeclarator)) return locatedExtrudeDeclarator
const { extrudeDeclarator } = locatedExtrudeDeclarator const { variableDeclarator } = locatedExtrudeDeclarator
// Modify the extrude expression to include this edge treatment expression // Modify the extrude expression to include this edge treatment expression
// CallExpression - no edge treatment // CallExpression - no edge treatment
@ -177,33 +178,33 @@ export async function modifyAstWithEdgeTreatmentAndTag(
let pathToEdgeTreatmentNode: PathToNode let pathToEdgeTreatmentNode: PathToNode
if (extrudeDeclarator.init.type === 'CallExpressionKw') { if (variableDeclarator.init.type === 'CallExpressionKw') {
// 1. case when no edge treatment exists // 1. case when no edge treatment exists
// modify ast with new edge treatment call by mutating the extrude node // modify ast with new edge treatment call by mutating the extrude node
extrudeDeclarator.init = createPipeExpression([ variableDeclarator.init = createPipeExpression([
extrudeDeclarator.init, variableDeclarator.init,
edgeTreatmentCall, edgeTreatmentCall,
]) ])
// get path to the edge treatment node // get path to the edge treatment node
pathToEdgeTreatmentNode = getPathToNodeOfEdgeTreatmentLiteral( pathToEdgeTreatmentNode = getPathToNodeOfEdgeTreatmentLiteral(
pathToExtrudeNode, pathToExtrudeNode,
extrudeDeclarator, variableDeclarator,
firstTag, firstTag,
parameters parameters
) )
pathToEdgeTreatmentNodes.push(pathToEdgeTreatmentNode) 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 // 2. case when edge treatment exists or extrude in sketch pipe
// mutate the extrude node with the new edge treatment call // 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 // get path to the edge treatment node
pathToEdgeTreatmentNode = getPathToNodeOfEdgeTreatmentLiteral( pathToEdgeTreatmentNode = getPathToNodeOfEdgeTreatmentLiteral(
pathToExtrudeNode, pathToExtrudeNode,
extrudeDeclarator, variableDeclarator,
firstTag, firstTag,
parameters parameters
) )
@ -330,38 +331,6 @@ export function getEdgeTagCall(
return tagCall 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( function getPathToNodeOfEdgeTreatmentLiteral(
pathToExtrudeNode: PathToNode, pathToExtrudeNode: PathToNode,
extrudeDeclarator: VariableDeclarator, extrudeDeclarator: VariableDeclarator,
@ -484,7 +453,7 @@ function getParameterNameAndValue(
: parameters.length.valueAst : parameters.length.valueAst
return { parameterName: 'length', parameterValue } return { parameterName: 'length', parameterValue }
} else { } 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 selection: Selection
): Promise<Node<Program> | Error> { ): Promise<Node<Program> | Error> {
/** /**
* Deletes an edge treatment (fillet or chamfer) * Deletes an edge treatment (fillet or chamfer) from the AST
* from the AST based on the selection.
* Handles both standalone treatments
* and those within a PipeExpression.
* *
* Supported cases: * Supported cases:
* [+] fillet and chamfer * [+] 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) * [-] delete single tag from array of tags (currently whole expression is deleted)
* [-] multiple selections with different edge treatments (currently single selection is supported) * [-] 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') return new Error('Selection is not an edge cut')
} }
const { subType: edgeTreatmentType } = artifact const { subType } = artifact
if ( if (!isEdgeTreatmentType(subType)) {
!edgeTreatmentType ||
!['fillet', 'chamfer'].includes(edgeTreatmentType)
) {
return new Error('Unsupported or missing edge treatment type') 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 astClone = structuredClone(ast)
const varDec = getNodeFromPath<VariableDeclarator>( const edgeTreatmentNode = getNodeFromPath<
ast, VariableDeclarator | ExpressionStatement
selection?.codeRef?.pathToNode, >(astClone, selection?.codeRef?.pathToNode, [
'VariableDeclarator' 'VariableDeclarator',
) 'ExpressionStatement',
if (err(varDec)) return varDec ])
if (err(edgeTreatmentNode)) return edgeTreatmentNode
// 3: Check if edge treatment is in a pipe // 3: Delete edge treatments
const inPipe = varDec.node.init.type === 'PipeExpression' // 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 ( if (
nextStep && edgeTreatmentNode.node.type === 'ExpressionStatement' || // unassigned
nextStep[1] === 'index' && (edgeTreatmentNode.node.type === 'VariableDeclarator' && // assigned
typeof nextStep[0] === 'number' edgeTreatmentNode.node.init?.type !== 'PipeExpression')
) { ) {
callIndex = nextStep[0] // Handle both standalone cases (assigned and unassigned)
} const deleteResult = deleteTopLevelStatement(
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>(
astClone, astClone,
pipeExpressionPath, selection.codeRef.pathToNode
'PipeExpression'
) )
if (err(pipeExpressionNode)) return pipeExpressionNode if (err(deleteResult)) return deleteResult
return astClone
// Ensure that the PipeExpression.body is an array } else {
if (!isArray(pipeExpressionNode.node.body)) { const deleteResult = deleteNodeInExtrudePipe(
return new Error('PipeExpression body is not an array') astClone,
} selection.codeRef.pathToNode
// 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
const varDecIndex: number = varDecPathStep[0]
astClone.body.splice(varDecIndex, 1)
}
return astClone return astClone
} }
return Error('Delete fillets not implemented')
} }
// Edit Edge Treatment // Edit Edge Treatment
@ -786,7 +682,7 @@ export async function editEdgeTreatment(
edgeTreatmentCall.node.arguments[index] = newArg edgeTreatmentCall.node.arguments[index] = newArg
} }
let pathToEdgeTreatmentNode = selection?.codeRef?.pathToNode const pathToEdgeTreatmentNode = selection?.codeRef?.pathToNode
return { modifiedAst, pathToEdgeTreatmentNode } return { modifiedAst, pathToEdgeTreatmentNode }
} }

View File

@ -1,26 +1,26 @@
import type { Node } from '@rust/kcl-lib/bindings/Node' import type { Node } from '@rust/kcl-lib/bindings/Node'
import type { PathToNode, Program } from '@src/lang/wasm' 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' import { err } from '@src/lib/trap'
export function deleteNodeInExtrudePipe( export function deleteNodeInExtrudePipe(
node: PathToNode, ast: Node<Program>,
ast: Node<Program> node: PathToNode
): Error | void { ): Error | void {
const pipeIndex = node.findIndex(([_, type]) => type === 'PipeExpression') + 1 const pipeIndex = node.findIndex(([_, type]) => type === 'PipeExpression') + 1
if (!(node[pipeIndex][0] && typeof node[pipeIndex][0] === 'number')) { if (!(node[pipeIndex][0] && typeof node[pipeIndex][0] === 'number')) {
return new Error("Couldn't find node to delete in ast") return new Error("Couldn't find node to delete in ast")
} }
const lookup = locateExtrudeDeclarator(ast, node) const lookup = locateVariableWithCallOrPipe(ast, node)
if (err(lookup)) { if (err(lookup)) {
return 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") 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, createPipeExpression,
createPipeSubstitution, createPipeSubstitution,
} from '@src/lang/create' } 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 type { PathToNode, Program } from '@src/lang/wasm'
import { COMMAND_APPEARANCE_COLOR_DEFAULT } from '@src/lib/commandBarConfigs/modelingCommandConfig' import { COMMAND_APPEARANCE_COLOR_DEFAULT } from '@src/lib/commandBarConfigs/modelingCommandConfig'
import { err } from '@src/lib/trap' import { err } from '@src/lib/trap'
@ -23,13 +23,13 @@ export function setAppearance({
}): Error | { modifiedAst: Node<Program>; pathToNode: PathToNode } { }): Error | { modifiedAst: Node<Program>; pathToNode: PathToNode } {
const modifiedAst = structuredClone(ast) const modifiedAst = structuredClone(ast)
// Locate the call (not necessarily an extrude here) // Locate the call
const result = locateExtrudeDeclarator(modifiedAst, nodeToEdit) const result = locateVariableWithCallOrPipe(modifiedAst, nodeToEdit)
if (err(result)) { if (err(result)) {
return result return result
} }
const declarator = result.extrudeDeclarator const declarator = result.variableDeclarator
const call = createCallExpressionStdLibKw( const call = createCallExpressionStdLibKw(
'appearance', 'appearance',
createPipeSubstitution(), 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( export function findImportNodeAndAlias(
ast: Node<Program>, ast: Node<Program>,
pathToNode: PathToNode pathToNode: PathToNode