From 3e8ee3ffc401dde733a4e61e39f35df29f877650 Mon Sep 17 00:00:00 2001 From: max Date: Mon, 3 Feb 2025 18:11:26 +0100 Subject: [PATCH] Add Point-and-Click Deletion for Fillets and Chamfers (#5098) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 Co-authored-by: github-actions[bot] --- e2e/playwright/point-click.spec.ts | 318 ++++++++++++++++++++ src/lang/modifyAst.ts | 3 + src/lang/modifyAst/addEdgeTreatment.test.ts | 228 +++++++++++++- src/lang/modifyAst/addEdgeTreatment.ts | 146 +++++++++ 4 files changed, 694 insertions(+), 1 deletion(-) diff --git a/e2e/playwright/point-click.spec.ts b/e2e/playwright/point-click.spec.ts index 967ffde930..68ac12e2d6 100644 --- a/e2e/playwright/point-click.spec.ts +++ b/e2e/playwright/point-click.spec.ts @@ -1296,6 +1296,167 @@ extrude001 = extrude(-12, sketch001) 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 ({ @@ -1511,6 +1672,163 @@ extrude001 = extrude(-12, sketch001) 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 = [ diff --git a/src/lang/modifyAst.ts b/src/lang/modifyAst.ts index 809d6a181b..01c1ca0c22 100644 --- a/src/lang/modifyAst.ts +++ b/src/lang/modifyAst.ts @@ -47,6 +47,7 @@ import { Models } from '@kittycad/lib' import { ExtrudeFacePlane } from 'machines/modelingMachine' import { Node } from 'wasm-lib/kcl/bindings/Node' import { KclExpressionWithVariable } from 'lib/commandTypes' +import { deleteEdgeTreatment } from './modifyAst/addEdgeTreatment' export function startSketchOnDefault( node: Node, @@ -1371,6 +1372,8 @@ export async function deleteFromSelection( } // await prom return astClone + } else if (selection.artifact?.type === 'edgeCut') { + return deleteEdgeTreatment(astClone, selection) } else if (varDec.node.init.type === 'PipeExpression') { const pipeBody = varDec.node.init.body if ( diff --git a/src/lang/modifyAst/addEdgeTreatment.test.ts b/src/lang/modifyAst/addEdgeTreatment.test.ts index ea3ddd5c16..155dd7c053 100644 --- a/src/lang/modifyAst/addEdgeTreatment.test.ts +++ b/src/lang/modifyAst/addEdgeTreatment.test.ts @@ -20,6 +20,7 @@ import { FilletParameters, ChamferParameters, EdgeTreatmentParameters, + deleteEdgeTreatment, } from './addEdgeTreatment' import { getNodeFromPath } from '../queryAst' import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils' @@ -287,7 +288,7 @@ const runModifyAstCloneWithEdgeTreatmentAndTag = async ( otherSelections: [], } - // apply edge treatment to seleciton + // apply edge treatment to selection const result = modifyAstWithEdgeTreatmentAndTag(ast, selection, parameters) if (err(result)) { return result @@ -298,6 +299,46 @@ const runModifyAstCloneWithEdgeTreatmentAndTag = async ( 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 => ({ type: EdgeTreatmentType.Fillet, 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 + ) + }) + }) } ) diff --git a/src/lang/modifyAst/addEdgeTreatment.ts b/src/lang/modifyAst/addEdgeTreatment.ts index 0b91125ad2..455dbf0e61 100644 --- a/src/lang/modifyAst/addEdgeTreatment.ts +++ b/src/lang/modifyAst/addEdgeTreatment.ts @@ -5,6 +5,7 @@ import { Identifier, ObjectExpression, PathToNode, + PipeExpression, Program, VariableDeclaration, VariableDeclarator, @@ -722,3 +723,148 @@ export const isTagUsedInEdgeTreatment = ({ return edges } + +// Delete Edge Treatment +export async function deleteEdgeTreatment( + ast: Node, + selection: Selection +): Promise | 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( + 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( + 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( + 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') +}