Add Point-and-Click Deletion for Fillets and Chamfers (#5098)

* ast mod

* point and click test

* tsc

* test test

* unit test edit

* topLevelRange

* disable unit test

* remove bad imports

* fix typo

* Fix cyclic dependency hell with getNodePathFromSourceRange

* tsc

* fix ImportStatement

* fix isValueZero

* pre-emptively ==> preemptively

* yarn fmt-check

* reenable the unit test

* fmt

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* add test

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* several treatments

* consolidate

* typos

* fix imports, consolidate

* consolidate import

* fix imports

* add tests

* stress test CI

* fix test

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* fix tests

* clean test for fillets

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* test chamfers

* comments

* simplify main tests

* typo

* typo2

* remove import

* clean up comments

---------

Co-authored-by: 49lf <ircsurfer33@gmail.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
max
2025-02-03 18:11:26 +01:00
committed by GitHub
parent a44516bc7e
commit 3e8ee3ffc4
4 changed files with 694 additions and 1 deletions

View File

@ -1296,6 +1296,167 @@ extrude001 = extrude(-12, sketch001)
lowTolerance lowTolerance
) )
}) })
// Test 3: Delete fillets
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 test.step('Delete fillet via feature tree selection', async () => {
await editor.expectEditor.toContain(secondFilletDeclaration)
const operationButton = await toolbar.getFeatureTreeOperation('Fillet', 1)
await operationButton.click({ button: 'left' })
await page.keyboard.press('Backspace')
await page.waitForTimeout(500)
await scene.expectPixelColor(edgeColorWhite, secondEdgeLocation, 15) // deleted
await editor.expectEditor.not.toContain(secondFilletDeclaration)
await scene.expectPixelColor(filletColor, firstEdgeLocation, 15) // stayed
})
})
})
test(`Fillet point-and-click delete`, async ({
context,
page,
homePage,
scene,
editor,
toolbar,
}) => {
// Code samples
const initialCode = `sketch001 = startSketchOn('XY')
|> startProfileAt([-12, -6], %)
|> line([0, 12], %)
|> line([24, 0], %, $seg02)
|> line([0, -12], %)
|> lineTo([profileStartX(%), profileStartY(%)], %, $seg01)
|> close(%)
extrude001 = extrude(-12, sketch001)
|> fillet({ radius = 5, tags = [seg01] }, %) // fillet01
|> fillet({ radius = 5, tags = [seg02] }, %) // fillet02
fillet03 = fillet({ radius = 5, tags = [getOppositeEdge(seg01)]}, extrude001)
fillet04 = fillet({ radius = 5, tags = [getOppositeEdge(seg02)]}, extrude001)
`
const pipedFilletDeclaration = 'fillet({ radius = 5, tags = [seg01] }, %)'
const secondPipedFilletDeclaration =
'fillet({ radius = 5, tags = [seg02] }, %)'
const standaloneFilletDeclaration =
'fillet03 = fillet({ radius = 5, tags = [getOppositeEdge(seg01)]}, extrude001)'
const secondStandaloneFilletDeclaration =
'fillet04 = fillet({ radius = 5, tags = [getOppositeEdge(seg02)]}, extrude001)'
// Locators
const pipedFilletEdgeLocation = { x: 600, y: 193 }
const standaloneFilletEdgeLocation = { x: 600, y: 383 }
const bodyLocation = { x: 630, y: 290 }
// Colors
const edgeColorWhite: [number, number, number] = [248, 248, 248]
const bodyColor: [number, number, number] = [155, 155, 155]
const filletColor: [number, number, number] = [127, 127, 127]
const backgroundColor: [number, number, number] = [30, 30, 30]
const lowTolerance = 20
const highTolerance = 40
// Setup
await test.step(`Initial test setup`, async () => {
await context.addInitScript((initialCode) => {
localStorage.setItem('persistCode', initialCode)
}, initialCode)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
// verify modeling scene is loaded
await scene.expectPixelColor(
backgroundColor,
standaloneFilletEdgeLocation,
lowTolerance
)
// wait for stream to load
await scene.expectPixelColor(bodyColor, bodyLocation, highTolerance)
})
// Test
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 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(secondPipedFilletDeclaration)
await editor.expectEditor.toContain(standaloneFilletDeclaration)
await editor.expectEditor.toContain(secondStandaloneFilletDeclaration)
})
await test.step('Verify test fillets are present in the scene', async () => {
await scene.expectPixelColor(
filletColor,
pipedFilletEdgeLocation,
lowTolerance
)
await scene.expectPixelColor(
backgroundColor,
standaloneFilletEdgeLocation,
lowTolerance
)
})
await test.step('Delete piped fillet', async () => {
const operationButton = await toolbar.getFeatureTreeOperation(
'Fillet',
0
)
await operationButton.click({ button: 'left' })
await page.keyboard.press('Backspace')
await page.waitForTimeout(500)
})
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.toContain(secondPipedFilletDeclaration)
await editor.expectEditor.toContain(standaloneFilletDeclaration)
await editor.expectEditor.toContain(secondStandaloneFilletDeclaration)
})
await test.step('Verify piped fillet is deleted but non-piped is not (in the scene)', async () => {
await scene.expectPixelColor(
edgeColorWhite, // you see edge because fillet is deleted
pipedFilletEdgeLocation,
lowTolerance
)
await scene.expectPixelColor(
backgroundColor, // you see background because fillet is not deleted
standaloneFilletEdgeLocation,
lowTolerance
)
})
})
await test.step('Delete non-piped fillet via feature tree selection', async () => {
await test.step('Delete non-piped fillet', async () => {
const operationButton = await toolbar.getFeatureTreeOperation(
'Fillet',
1
)
await operationButton.click({ button: 'left' })
await page.keyboard.press('Backspace')
await page.waitForTimeout(500)
})
await test.step('Verify non-piped 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 test.step('Verify non-piped fillet is deleted but piped is not (in the scene)', async () => {
await scene.expectPixelColor(
edgeColorWhite,
standaloneFilletEdgeLocation,
lowTolerance
)
})
})
})
}) })
test(`Chamfer point-and-click`, async ({ test(`Chamfer point-and-click`, async ({
@ -1511,6 +1672,163 @@ extrude001 = extrude(-12, sketch001)
lowTolerance lowTolerance
) )
}) })
// Test 3: Delete chamfer via feature tree selection
await test.step('Open Feature Tree Pane', async () => {
await toolbar.openPane('feature-tree')
await page.waitForTimeout(500)
})
await test.step('Delete chamfer via feature tree selection', async () => {
const operationButton = await toolbar.getFeatureTreeOperation('Chamfer', 1)
await operationButton.click({ button: 'left' })
await page.keyboard.press('Backspace')
await page.waitForTimeout(500)
await scene.expectPixelColor(edgeColorWhite, secondEdgeLocation, 15) // deleted
await scene.expectPixelColor(chamferColor, firstEdgeLocation, 15) // stayed
})
})
test(`Chamfer point-and-click delete`, async ({
context,
page,
homePage,
scene,
editor,
toolbar,
}) => {
// Code samples
const initialCode = `sketch001 = startSketchOn('XY')
|> startProfileAt([-12, -6], %)
|> line([0, 12], %)
|> line([24, 0], %, $seg02)
|> line([0, -12], %)
|> lineTo([profileStartX(%), profileStartY(%)], %, $seg01)
|> close(%)
extrude001 = extrude(-12, sketch001)
|> chamfer({ length = 5, tags = [seg01] }, %) // chamfer01
|> chamfer({ length = 5, tags = [seg02] }, %) // chamfer02
chamfer03 = chamfer({ length = 5, tags = [getOppositeEdge(seg01)]}, extrude001)
chamfer04 = chamfer({ length = 5, tags = [getOppositeEdge(seg02)]}, extrude001)
`
const pipedChamferDeclaration = 'chamfer({ length = 5, tags = [seg01] }, %)'
const secondPipedChamferDeclaration =
'chamfer({ length = 5, tags = [seg02] }, %)'
const standaloneChamferDeclaration =
'chamfer03 = chamfer({ length = 5, tags = [getOppositeEdge(seg01)]}, extrude001)'
const secondStandaloneChamferDeclaration =
'chamfer04 = chamfer({ length = 5, tags = [getOppositeEdge(seg02)]}, extrude001)'
// Locators
const pipedChamferEdgeLocation = { x: 600, y: 193 }
const standaloneChamferEdgeLocation = { x: 600, y: 383 }
const bodyLocation = { x: 630, y: 290 }
// Colors
const edgeColorWhite: [number, number, number] = [248, 248, 248]
const bodyColor: [number, number, number] = [155, 155, 155]
const chamferColor: [number, number, number] = [168, 168, 168]
const backgroundColor: [number, number, number] = [30, 30, 30]
const lowTolerance = 20
const highTolerance = 40
// Setup
await test.step(`Initial test setup`, async () => {
await context.addInitScript((initialCode) => {
localStorage.setItem('persistCode', initialCode)
}, initialCode)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
// verify modeling scene is loaded
await scene.expectPixelColor(
backgroundColor,
standaloneChamferEdgeLocation,
lowTolerance
)
// wait for stream to load
await scene.expectPixelColor(bodyColor, bodyLocation, highTolerance)
})
// Test
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 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(secondPipedChamferDeclaration)
await editor.expectEditor.toContain(standaloneChamferDeclaration)
await editor.expectEditor.toContain(secondStandaloneChamferDeclaration)
})
await test.step('Verify test chamfers are present in the scene', async () => {
await scene.expectPixelColor(
chamferColor,
pipedChamferEdgeLocation,
lowTolerance
)
await scene.expectPixelColor(
backgroundColor,
standaloneChamferEdgeLocation,
lowTolerance
)
})
await test.step('Delete piped chamfer', async () => {
const operationButton = await toolbar.getFeatureTreeOperation(
'Chamfer',
0
)
await operationButton.click({ button: 'left' })
await page.keyboard.press('Backspace')
await page.waitForTimeout(500)
})
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.toContain(secondPipedChamferDeclaration)
await editor.expectEditor.toContain(standaloneChamferDeclaration)
await editor.expectEditor.toContain(secondStandaloneChamferDeclaration)
})
await test.step('Verify piped chamfer is deleted but non-piped is not (in the scene)', async () => {
await scene.expectPixelColor(
edgeColorWhite, // you see edge color because chamfer is deleted
pipedChamferEdgeLocation,
lowTolerance
)
await scene.expectPixelColor(
backgroundColor, // you see background color instead of edge because it's chamfered
standaloneChamferEdgeLocation,
lowTolerance
)
})
})
await test.step('Delete non-piped chamfer via feature tree selection', async () => {
await test.step('Delete non-piped chamfer', async () => {
const operationButton = await toolbar.getFeatureTreeOperation(
'Chamfer',
1
)
await operationButton.click({ button: 'left' })
await page.keyboard.press('Backspace')
await page.waitForTimeout(500)
})
await test.step('Verify non-piped 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.toContain(secondStandaloneChamferDeclaration)
})
await test.step('Verify non-piped chamfer is deleted but piped is not (in the scene)', async () => {
await scene.expectPixelColor(
edgeColorWhite,
standaloneChamferEdgeLocation,
lowTolerance
)
})
})
})
}) })
const shellPointAndClickCapCases = [ const shellPointAndClickCapCases = [

View File

@ -47,6 +47,7 @@ import { Models } from '@kittycad/lib'
import { ExtrudeFacePlane } from 'machines/modelingMachine' import { ExtrudeFacePlane } from 'machines/modelingMachine'
import { Node } from 'wasm-lib/kcl/bindings/Node' import { Node } from 'wasm-lib/kcl/bindings/Node'
import { KclExpressionWithVariable } from 'lib/commandTypes' import { KclExpressionWithVariable } from 'lib/commandTypes'
import { deleteEdgeTreatment } from './modifyAst/addEdgeTreatment'
export function startSketchOnDefault( export function startSketchOnDefault(
node: Node<Program>, node: Node<Program>,
@ -1371,6 +1372,8 @@ export async function deleteFromSelection(
} }
// await prom // await prom
return astClone return astClone
} else if (selection.artifact?.type === 'edgeCut') {
return deleteEdgeTreatment(astClone, selection)
} else if (varDec.node.init.type === 'PipeExpression') { } else if (varDec.node.init.type === 'PipeExpression') {
const pipeBody = varDec.node.init.body const pipeBody = varDec.node.init.body
if ( if (

View File

@ -20,6 +20,7 @@ import {
FilletParameters, FilletParameters,
ChamferParameters, ChamferParameters,
EdgeTreatmentParameters, EdgeTreatmentParameters,
deleteEdgeTreatment,
} from './addEdgeTreatment' } from './addEdgeTreatment'
import { getNodeFromPath } from '../queryAst' import { getNodeFromPath } from '../queryAst'
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils' import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
@ -287,7 +288,7 @@ const runModifyAstCloneWithEdgeTreatmentAndTag = async (
otherSelections: [], otherSelections: [],
} }
// apply edge treatment to seleciton // apply edge treatment to selection
const result = modifyAstWithEdgeTreatmentAndTag(ast, selection, parameters) const result = modifyAstWithEdgeTreatmentAndTag(ast, selection, parameters)
if (err(result)) { if (err(result)) {
return result return result
@ -298,6 +299,46 @@ const runModifyAstCloneWithEdgeTreatmentAndTag = async (
expect(newCode).toContain(expectedCode) expect(newCode).toContain(expectedCode)
} }
const runDeleteEdgeTreatmentTest = async (
code: string,
edgeTreatmentSnippet: string,
expectedCode: string
) => {
// parse ast
const ast = assertParse(code)
// update artifact graph
await kclManager.executeAst({ ast })
const artifactGraph = engineCommandManager.artifactGraph
// define snippet range
const edgeTreatmentRange = topLevelRange(
code.indexOf(edgeTreatmentSnippet),
code.indexOf(edgeTreatmentSnippet) + edgeTreatmentSnippet.length
)
// find artifact
const maybeArtifact = [...artifactGraph].find(([, artifact]) => {
if (!('codeRef' in artifact)) return false
return isOverlap(artifact.codeRef.range, edgeTreatmentRange)
})
// build selection
const selection: Selection = {
codeRef: codeRefFromRange(edgeTreatmentRange, ast),
artifact: maybeArtifact ? maybeArtifact[1] : undefined,
}
// delete edge treatment
const result = await deleteEdgeTreatment(ast, selection)
if (err(result)) {
return result
}
// recast and check
const newCode = recast(result)
expect(newCode).toContain(expectedCode)
}
const createFilletParameters = (radiusValue: number): FilletParameters => ({ const createFilletParameters = (radiusValue: number): FilletParameters => ({
type: EdgeTreatmentType.Fillet, type: EdgeTreatmentType.Fillet,
radius: { radius: {
@ -574,6 +615,191 @@ extrude002 = extrude(-25, sketch002)
) )
}) })
}) })
describe(`Testing deleteEdgeTreatment with ${edgeTreatmentType}s`, () => {
// simple cases
it(`should delete a piped ${edgeTreatmentType} from a single segment`, async () => {
const code = `sketch001 = startSketchOn('XY')
|> startProfileAt([-10, 10], %)
|> line([20, 0], %)
|> line([0, -20], %)
|> line([-20, 0], %, $seg01)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
extrude001 = extrude(-15, sketch001)
|> ${edgeTreatmentType}({ ${parameterName} = 3, tags = [seg01] }, %)`
const edgeTreatmentSnippet = `${edgeTreatmentType}({ ${parameterName} = 3, tags = [seg01] }, %)`
const expectedCode = `sketch001 = startSketchOn('XY')
|> startProfileAt([-10, 10], %)
|> line([20, 0], %)
|> line([0, -20], %)
|> line([-20, 0], %, $seg01)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
extrude001 = extrude(-15, sketch001)`
await runDeleteEdgeTreatmentTest(
code,
edgeTreatmentSnippet,
expectedCode
)
})
it(`should delete a non-piped ${edgeTreatmentType} from a single segment`, async () => {
const code = `sketch001 = startSketchOn('XY')
|> startProfileAt([-10, 10], %)
|> line([20, 0], %)
|> line([0, -20], %)
|> line([-20, 0], %, $seg01)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
extrude001 = extrude(-15, sketch001)
fillet001 = ${edgeTreatmentType}({ ${parameterName} = 3, tags = [seg01] }, extrude001)`
const edgeTreatmentSnippet = `fillet001 = ${edgeTreatmentType}({ ${parameterName} = 3, tags = [seg01] }, extrude001)`
const expectedCode = `sketch001 = startSketchOn('XY')
|> startProfileAt([-10, 10], %)
|> line([20, 0], %)
|> line([0, -20], %)
|> line([-20, 0], %, $seg01)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
extrude001 = extrude(-15, sketch001)`
await runDeleteEdgeTreatmentTest(
code,
edgeTreatmentSnippet,
expectedCode
)
})
// getOppositeEdge and getNextAdjacentEdge cases
it(`should delete a piped ${edgeTreatmentType} tagged with getOppositeEdge`, async () => {
const code = `sketch001 = startSketchOn('XY')
|> startProfileAt([-10, 10], %)
|> line([20, 0], %)
|> line([0, -20], %)
|> line([-20, 0], %, $seg01)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
extrude001 = extrude(-15, sketch001)
fillet001 = ${edgeTreatmentType}({ ${parameterName} = 3, tags = [getOppositeEdge(seg01)] }, extrude001)`
const edgeTreatmentSnippet = `fillet001 = ${edgeTreatmentType}({ ${parameterName} = 3, tags = [getOppositeEdge(seg01)] }, extrude001)`
const expectedCode = `sketch001 = startSketchOn('XY')
|> startProfileAt([-10, 10], %)
|> line([20, 0], %)
|> line([0, -20], %)
|> line([-20, 0], %, $seg01)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
extrude001 = extrude(-15, sketch001)`
await runDeleteEdgeTreatmentTest(
code,
edgeTreatmentSnippet,
expectedCode
)
})
it(`should delete a non-piped ${edgeTreatmentType} tagged with getNextAdjacentEdge`, async () => {
const code = `sketch001 = startSketchOn('XY')
|> startProfileAt([-10, 10], %)
|> line([20, 0], %)
|> line([0, -20], %)
|> line([-20, 0], %, $seg01)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
extrude001 = extrude(-15, sketch001)
fillet001 = ${edgeTreatmentType}({ ${parameterName} = 3, tags = [getNextAdjacentEdge(seg01)] }, extrude001)`
const edgeTreatmentSnippet = `fillet001 = ${edgeTreatmentType}({ ${parameterName} = 3, tags = [getNextAdjacentEdge(seg01)] }, extrude001)`
const expectedCode = `sketch001 = startSketchOn('XY')
|> startProfileAt([-10, 10], %)
|> line([20, 0], %)
|> line([0, -20], %)
|> line([-20, 0], %, $seg01)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
extrude001 = extrude(-15, sketch001)`
await runDeleteEdgeTreatmentTest(
code,
edgeTreatmentSnippet,
expectedCode
)
})
// cases with several edge treatments
it(`should delete a piped ${edgeTreatmentType} from a body with multiple treatments`, async () => {
const code = `sketch001 = startSketchOn('XY')
|> startProfileAt([-10, 10], %)
|> line([20, 0], %, $seg01)
|> line([0, -20], %)
|> line([-20, 0], %, $seg02)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
extrude001 = extrude(-15, sketch001)
|> ${edgeTreatmentType}({ ${parameterName} = 3, tags = [seg01] }, %)
|> fillet({ radius = 5, tags = [getOppositeEdge(seg02)] }, %)
fillet001 = ${edgeTreatmentType}({ ${parameterName} = 6, tags = [seg02] }, extrude001)
chamfer001 = chamfer({ length = 5, tags = [getOppositeEdge(seg01)] }, extrude001)`
const edgeTreatmentSnippet = `${edgeTreatmentType}({ ${parameterName} = 3, tags = [seg01] }, %)`
const expectedCode = `sketch001 = startSketchOn('XY')
|> startProfileAt([-10, 10], %)
|> line([20, 0], %, $seg01)
|> line([0, -20], %)
|> line([-20, 0], %, $seg02)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
extrude001 = extrude(-15, sketch001)
|> fillet({
radius = 5,
tags = [getOppositeEdge(seg02)]
}, %)
fillet001 = ${edgeTreatmentType}({ ${parameterName} = 6, tags = [seg02] }, extrude001)
chamfer001 = chamfer({
length = 5,
tags = [getOppositeEdge(seg01)]
}, extrude001)`
await runDeleteEdgeTreatmentTest(
code,
edgeTreatmentSnippet,
expectedCode
)
})
it(`should delete a non-piped ${edgeTreatmentType} from a body with multiple treatments`, async () => {
const code = `sketch001 = startSketchOn('XY')
|> startProfileAt([-10, 10], %)
|> line([20, 0], %, $seg01)
|> line([0, -20], %)
|> line([-20, 0], %, $seg02)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
extrude001 = extrude(-15, sketch001)
|> ${edgeTreatmentType}({ ${parameterName} = 3, tags = [seg01] }, %)
|> fillet({ radius = 5, tags = [getOppositeEdge(seg02)] }, %)
fillet001 = ${edgeTreatmentType}({ ${parameterName} = 6, tags = [seg02] }, extrude001)
chamfer001 = chamfer({ length = 5, tags = [getOppositeEdge(seg01)] }, extrude001)`
const edgeTreatmentSnippet = `fillet001 = ${edgeTreatmentType}({ ${parameterName} = 6, tags = [seg02] }, extrude001)`
const expectedCode = `sketch001 = startSketchOn('XY')
|> startProfileAt([-10, 10], %)
|> line([20, 0], %, $seg01)
|> line([0, -20], %)
|> line([-20, 0], %, $seg02)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
extrude001 = extrude(-15, sketch001)
|> ${edgeTreatmentType}({ ${parameterName} = 3, tags = [seg01] }, %)
|> fillet({
radius = 5,
tags = [getOppositeEdge(seg02)]
}, %)
chamfer001 = chamfer({
length = 5,
tags = [getOppositeEdge(seg01)]
}, extrude001)`
await runDeleteEdgeTreatmentTest(
code,
edgeTreatmentSnippet,
expectedCode
)
})
})
} }
) )

View File

@ -5,6 +5,7 @@ import {
Identifier, Identifier,
ObjectExpression, ObjectExpression,
PathToNode, PathToNode,
PipeExpression,
Program, Program,
VariableDeclaration, VariableDeclaration,
VariableDeclarator, VariableDeclarator,
@ -722,3 +723,148 @@ export const isTagUsedInEdgeTreatment = ({
return edges return edges
} }
// Delete Edge Treatment
export async function deleteEdgeTreatment(
ast: Node<Program>,
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.
*
* Supported cases:
* [+] fillet and chamfer
* [+] piped and non-piped 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)
*/
// 1. Validate Selection Type
const { artifact } = selection
if (!artifact || artifact.type !== 'edgeCut') {
return new Error('Selection is not an edge cut')
}
const { subType: edgeTreatmentType } = artifact
if (
!edgeTreatmentType ||
!['fillet', 'chamfer'].includes(edgeTreatmentType)
) {
return new Error('Unsupported or missing edge treatment type')
}
// 2. Clone ast and retrieve the VariableDeclarator
const astClone = structuredClone(ast)
const varDec = getNodeFromPath<VariableDeclarator>(
ast,
selection?.codeRef?.pathToNode,
'VariableDeclarator'
)
if (err(varDec)) return varDec
// 3: Check if edge treatment is in a pipe
const inPipe = varDec.node.init.type === 'PipeExpression'
// 4A. Handle standalone edge treatment
if (!inPipe) {
const varDecPathStep = varDec.shallowPath[1]
if (
!Array.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<CallExpression>(
ast,
selection?.codeRef?.pathToNode,
'CallExpression'
) ?? 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'
) {
callIndex = nextStep[0]
}
break
}
}
if (!pipeExpressionPath) {
return new Error('PipeExpression not found in path')
}
if (callIndex === null) {
return new Error('Failed to extract CallExpression index')
}
// Retrieve the PipeExpression node
const pipeExpressionNode = getNodeFromPath<PipeExpression>(
astClone,
pipeExpressionPath,
'PipeExpression'
)
if (err(pipeExpressionNode)) return pipeExpressionNode
// Ensure that the PipeExpression.body is an array
if (!Array.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 (
!Array.isArray(varDecPathStep) ||
typeof varDecPathStep[0] !== 'number'
) {
return new Error(
'Invalid shallowPath structure: expected a number at shallowPath[1][0]'
)
}
const varDecIndex: number = varDecPathStep[0]
astClone.body.splice(varDecIndex, 1)
}
return astClone
}
return Error('Delete fillets not implemented')
}