diff --git a/e2e/playwright/flow-tests.spec.ts b/e2e/playwright/flow-tests.spec.ts index 00c828bcb..dbca47d38 100644 --- a/e2e/playwright/flow-tests.spec.ts +++ b/e2e/playwright/flow-tests.spec.ts @@ -3099,6 +3099,49 @@ const sketch002 = startSketchOn(extrude001, $seg01) ).not.toBeDisabled() }) + test('Fillet button states test', async ({ page }) => { + const u = await getUtils(page) + await page.addInitScript(async () => { + localStorage.setItem( + 'persistCode', + `const sketch001 = startSketchOn('XZ') + |> startProfileAt([-5, -5], %) + |> line([0, 10], %) + |> line([10, 0], %) + |> line([0, -10], %) + |> lineTo([profileStartX(%), profileStartY(%)], %) + |> close(%)` + ) + }) + + await page.setViewportSize({ width: 1000, height: 500 }) + await u.waitForAuthSkipAppStart() + await u.openDebugPanel() + await u.expectCmdLog('[data-message-type="execution-done"]') + await u.closeDebugPanel() + + const selectSegment = () => page.getByText(`line([10, 0], %)`).click() + const selectClose = () => page.getByText(`close(%)`).click() + const clickEmpty = () => page.mouse.click(950, 100) + + // expect fillet button without any bodies in the scene + await selectSegment() + await expect(page.getByRole('button', { name: 'Fillet' })).toBeDisabled() + await clickEmpty() + await expect(page.getByRole('button', { name: 'Fillet' })).toBeDisabled() + + // test fillet button with the body in the scene + const codeToAdd = `${await u.codeLocator.allInnerTexts()} +const extrude001 = extrude(10, sketch001)` + await u.codeLocator.fill(codeToAdd) + await selectSegment() + await expect(page.getByRole('button', { name: 'Fillet' })).toBeEnabled() + await selectClose() + await expect(page.getByRole('button', { name: 'Fillet' })).toBeDisabled() + await clickEmpty() + await expect(page.getByRole('button', { name: 'Fillet' })).toBeEnabled() + }) + const removeAfterFirstParenthesis = (inputString: string) => { const index = inputString.indexOf('(') if (index !== -1) { @@ -3500,6 +3543,44 @@ test.describe('Command bar tests', () => { `const extrude001 = extrude(${KCL_DEFAULT_LENGTH}, sketch001)` ) }) + + test('Fillet from command bar', async ({ page }) => { + await page.addInitScript(async () => { + localStorage.setItem( + 'persistCode', + `const sketch001 = startSketchOn('XY') + |> startProfileAt([-5, -5], %) + |> line([0, 10], %) + |> line([10, 0], %) + |> line([0, -10], %) + |> lineTo([profileStartX(%), profileStartY(%)], %) + |> close(%) +const extrude001 = extrude(-10, sketch001)` + ) + }) + + const u = await getUtils(page) + await page.setViewportSize({ width: 1000, height: 500 }) + await u.waitForAuthSkipAppStart() + await u.openDebugPanel() + await u.expectCmdLog('[data-message-type="execution-done"]') + await u.closeDebugPanel() + + const selectSegment = () => page.getByText(`line([0, -10], %)`).click() + + await selectSegment() + await page.waitForTimeout(100) + await page.getByRole('button', { name: 'Fillet' }).click() + await page.waitForTimeout(100) + await page.keyboard.press('Enter') + await page.waitForTimeout(100) + await page.keyboard.press('Enter') + await page.waitForTimeout(100) + await expect(page.locator('.cm-activeLine')).toContainText( + `fillet({ radius: ${KCL_DEFAULT_LENGTH}, tags: [seg01] }, %)` + ) + }) + test('Command bar can change a setting, and switch back and forth between arguments', async ({ page, }) => { diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-2-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-2-Google-Chrome-linux.png index a2d7f266a..6d31f181a 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-2-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-2-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-2-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-2-Google-Chrome-linux.png index 23b361cb9..90dfc7f93 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-2-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-2-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Zoom-to-fit-on-load---solid-2d-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Zoom-to-fit-on-load---solid-2d-1-Google-Chrome-linux.png index 4a8db3c76..4ba057504 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Zoom-to-fit-on-load---solid-2d-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Zoom-to-fit-on-load---solid-2d-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Zoom-to-fit-on-load---solid-3d-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Zoom-to-fit-on-load---solid-3d-1-Google-Chrome-linux.png index 5180abf83..348c794df 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Zoom-to-fit-on-load---solid-3d-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Zoom-to-fit-on-load---solid-3d-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--XY-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--XY-1-Google-Chrome-linux.png index eb93ec249..650d39a4f 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--XY-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--XY-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--XZ-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--XZ-1-Google-Chrome-linux.png index 1831ddf06..d307e79b1 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--XZ-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--XZ-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--YZ-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--YZ-1-Google-Chrome-linux.png index c87da2cc2..ef04e7ab1 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--YZ-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--YZ-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-XY-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-XY-1-Google-Chrome-linux.png index 94d2559c5..e41d1e2cc 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-XY-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-XY-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-XZ-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-XZ-1-Google-Chrome-linux.png index 6ae5e67ff..e983df08a 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-XZ-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-XZ-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-YZ-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-YZ-1-Google-Chrome-linux.png index 027eab299..3f07e8153 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-YZ-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-YZ-1-Google-Chrome-linux.png differ diff --git a/src/Toolbar.tsx b/src/Toolbar.tsx index 4e0f41875..591c44c9a 100644 --- a/src/Toolbar.tsx +++ b/src/Toolbar.tsx @@ -16,6 +16,7 @@ import { canRectangleTool, isEditingExistingSketch, } from 'machines/modelingMachine' +import { DEV } from 'env' export function Toolbar({ className = '', @@ -118,6 +119,16 @@ export function Toolbar({ }), { enabled: !disableAllButtons, scopes: ['modeling'] } ) + const disableFillet = !state.can('Fillet') || disableAllButtons + useHotkeys( + 'f', + () => + commandBarSend({ + type: 'Find and select command', + data: { name: 'Fillet', groupId: 'modeling' }, + }), + { enabled: !disableFillet, scopes: ['modeling'] } + ) function handleToolbarButtonsWheelEvent(ev: WheelEvent) { const span = toolbarButtonsRef.current @@ -404,6 +415,36 @@ export function Toolbar({ )} + {state.matches('idle') && (DEV || (window as any)._enableFillet) && ( +
  • + + commandBarSend({ + type: 'Find and select command', + data: { name: 'Fillet', groupId: 'modeling' }, + }) + } + disabled={disableFillet} + title={disableFillet ? 'fillet' : "edge can't be filleted"} + iconStart={{ + icon: 'fillet', // todo: add fillet icon + iconClassName, + bgClassName, + }} + > + Fillet + + Shortcut: F + + +
  • + )} ) diff --git a/src/components/CommandBar/CommandBarSelectionInput.tsx b/src/components/CommandBar/CommandBarSelectionInput.tsx index 945dadebd..26551edf9 100644 --- a/src/components/CommandBar/CommandBarSelectionInput.tsx +++ b/src/components/CommandBar/CommandBarSelectionInput.tsx @@ -11,6 +11,25 @@ import { modelingMachine } from 'machines/modelingMachine' import { useCallback, useEffect, useRef, useState } from 'react' import { StateFrom } from 'xstate' +const semanticEntityNames = { + face: ['extrude-wall', 'start-cap', 'end-cap'], + edge: ['edge', 'line', 'arc'], + point: ['point', 'line-end', 'line-mid'], +} + +function getSemanticSelectionType(selectionType: string[]) { + const semanticSelectionType = new Set() + selectionType.forEach((type) => { + Object.entries(semanticEntityNames).forEach(([entity, entityTypes]) => { + if (entityTypes.includes(type)) { + semanticSelectionType.add(entity) + } + }) + }) + + return Array.from(semanticSelectionType) +} + const selectionSelector = (snapshot: StateFrom) => snapshot.context.selectionRanges @@ -85,7 +104,9 @@ function CommandBarSelectionInput({ > {canSubmitSelection ? getSelectionTypeDisplayText(selection) + ' selected' - : `Please select ${arg.multiple ? 'one or more faces' : 'one face'}`} + : `Please select ${ + arg.multiple ? 'one or more ' : 'one ' + }${getSemanticSelectionType(arg.selectionTypes).join(' or ')}`} ), + fillet: ( + + + + + ), file: ( = { state: StateFrom @@ -444,6 +446,12 @@ export const ModelingMachineProvider = ({ if (selectionRanges.codeBasedSelections.length <= 0) return false return true }, + 'has valid fillet selection': ({ selectionRanges }) => + hasValidFilletSelection({ + selectionRanges, + ast: kclManager.ast, + code: codeManager.code, + }), 'Selection is on face': ({ selectionRanges }, { data }) => { if (data?.forceNewSketch) return false if (!isSingleCursorInPipe(selectionRanges, kclManager.ast)) @@ -494,7 +502,6 @@ export const ModelingMachineProvider = ({ kclManager.ast, data.sketchPathToNode, data.extrudePathToNode, - kclManager.programMemory, data.cap ) if (trap(sketched)) return Promise.reject(sketched) diff --git a/src/lang/modifyAst.test.ts b/src/lang/modifyAst.test.ts index e83794f6f..c09218c52 100644 --- a/src/lang/modifyAst.test.ts +++ b/src/lang/modifyAst.test.ts @@ -304,7 +304,6 @@ describe('testing sketchOnExtrudedFace', () => { const ast = parse(code) if (err(ast)) throw ast - const programMemory = await enginelessExecutor(ast) const segmentSnippet = `line([9.7, 9.19], %)` const segmentRange: [number, number] = [ code.indexOf(segmentSnippet), @@ -321,8 +320,7 @@ describe('testing sketchOnExtrudedFace', () => { const extruded = sketchOnExtrudedFace( ast, segmentPathToNode, - extrudePathToNode, - programMemory + extrudePathToNode ) if (err(extruded)) throw extruded const { modifiedAst } = extruded @@ -345,7 +343,6 @@ const sketch001 = startSketchOn(part001, seg01)`) |> extrude(5 + 7, %)` const ast = parse(code) if (err(ast)) throw ast - const programMemory = await enginelessExecutor(ast) const segmentSnippet = `close(%)` const segmentRange: [number, number] = [ code.indexOf(segmentSnippet), @@ -362,8 +359,7 @@ const sketch001 = startSketchOn(part001, seg01)`) const extruded = sketchOnExtrudedFace( ast, segmentPathToNode, - extrudePathToNode, - programMemory + extrudePathToNode ) if (err(extruded)) throw extruded const { modifiedAst } = extruded @@ -386,7 +382,6 @@ const sketch001 = startSketchOn(part001, seg01)`) |> extrude(5 + 7, %)` const ast = parse(code) if (err(ast)) throw ast - const programMemory = await enginelessExecutor(ast) const sketchSnippet = `startProfileAt([3.58, 2.06], %)` const sketchRange: [number, number] = [ code.indexOf(sketchSnippet), @@ -404,7 +399,6 @@ const sketch001 = startSketchOn(part001, seg01)`) ast, sketchPathToNode, extrudePathToNode, - programMemory, 'end' ) if (err(extruded)) throw extruded @@ -436,7 +430,6 @@ const sketch001 = startSketchOn(part001, 'END')`) const part001 = extrude(5 + 7, sketch001)` const ast = parse(code) if (err(ast)) throw ast - const programMemory = await enginelessExecutor(ast) const segmentSnippet = `line([4.99, -0.46], %)` const segmentRange: [number, number] = [ code.indexOf(segmentSnippet), @@ -453,8 +446,7 @@ const sketch001 = startSketchOn(part001, 'END')`) const updatedAst = sketchOnExtrudedFace( ast, segmentPathToNode, - extrudePathToNode, - programMemory + extrudePathToNode ) if (err(updatedAst)) throw updatedAst const newCode = recast(updatedAst.modifiedAst) diff --git a/src/lang/modifyAst.ts b/src/lang/modifyAst.ts index 255bdf282..d39fe1f54 100644 --- a/src/lang/modifyAst.ts +++ b/src/lang/modifyAst.ts @@ -349,7 +349,6 @@ export function sketchOnExtrudedFace( node: Program, sketchPathToNode: PathToNode, extrudePathToNode: PathToNode, - programMemory: ProgramMemory, cap: 'none' | 'start' | 'end' = 'none' ): { modifiedAst: Program; pathToNode: PathToNode } | Error { let _node = { ...node } @@ -388,7 +387,6 @@ export function sketchOnExtrudedFace( if (cap === 'none') { const __tag = addTagForSketchOnFace( { - previousProgramMemory: programMemory, pathToNode: sketchPathToNode, node: _node, }, diff --git a/src/lang/modifyAst/addFillet.test.ts b/src/lang/modifyAst/addFillet.test.ts new file mode 100644 index 000000000..c5b4a5604 --- /dev/null +++ b/src/lang/modifyAst/addFillet.test.ts @@ -0,0 +1,315 @@ +import { + parse, + recast, + initPromise, + PathToNode, + Value, + Program, + CallExpression, +} from '../wasm' +import { addFillet, isTagUsedInFillet } from './addFillet' +import { getNodeFromPath, getNodePathFromSourceRange } from '../queryAst' +import { createLiteral } from 'lang/modifyAst' +import { err } from 'lib/trap' + +beforeAll(async () => { + await initPromise // Initialize the WASM environment before running tests +}) + +const runFilletTest = async ( + code: string, + segmentSnippet: string, + extrudeSnippet: string, + radius = createLiteral(5) as Value, + expectedCode: string +) => { + const astOrError = parse(code) + if (astOrError instanceof Error) { + return new Error('AST not found') + } + + const ast = astOrError as Program + + const segmentRange: [number, number] = [ + code.indexOf(segmentSnippet), + code.indexOf(segmentSnippet) + segmentSnippet.length, + ] + const pathToSegmentNode: PathToNode = getNodePathFromSourceRange( + ast, + segmentRange + ) + + const extrudeRange: [number, number] = [ + code.indexOf(extrudeSnippet), + code.indexOf(extrudeSnippet) + extrudeSnippet.length, + ] + + const pathToExtrudeNode: PathToNode = getNodePathFromSourceRange( + ast, + extrudeRange + ) + if (pathToExtrudeNode instanceof Error) { + return new Error('Path to extrude node not found') + } + + // const radius = createLiteral(5) as Value + + const result = addFillet(ast, pathToSegmentNode, pathToExtrudeNode, radius) + if (result instanceof Error) { + return result + } + const { modifiedAst } = result + const newCode = recast(modifiedAst) + + expect(newCode).toContain(expectedCode) +} + +describe('Testing addFillet', () => { + /** + * 1. Ideal Case + */ + + it('should add a fillet to a specific segment after extrusion, clean', async () => { + const code = ` + const sketch001 = startSketchOn('XZ') + |> startProfileAt([2.16, 49.67], %) + |> line([101.49, 139.93], %) + |> line([60.04, -55.72], %) + |> line([1.29, -115.74], %) + |> line([-87.24, -47.08], %) + |> tangentialArcTo([56.15, -94.58], %) + |> tangentialArcTo([14.68, -104.52], %) + |> lineTo([profileStartX(%), profileStartY(%)], %) + |> close(%) + const extrude001 = extrude(50, sketch001) + ` + const segmentSnippet = `line([60.04, -55.72], %)` + const extrudeSnippet = `const extrude001 = extrude(50, sketch001)` + const radius = createLiteral(5) as Value + const expectedCode = `const sketch001 = startSketchOn('XZ') + |> startProfileAt([2.16, 49.67], %) + |> line([101.49, 139.93], %) + |> line([60.04, -55.72], %, $seg01) + |> line([1.29, -115.74], %) + |> line([-87.24, -47.08], %) + |> tangentialArcTo([56.15, -94.58], %) + |> tangentialArcTo([14.68, -104.52], %) + |> lineTo([profileStartX(%), profileStartY(%)], %) + |> close(%) +const extrude001 = extrude(50, sketch001) + |> fillet({ radius: 5, tags: [seg01] }, %)` + + await runFilletTest( + code, + segmentSnippet, + extrudeSnippet, + radius, + expectedCode + ) + }) + + /** + * 2. Case of existing tag in the other line + */ + + it('should add a fillet to a specific segment after extrusion with existing tag in any other line', async () => { + const code = ` + const sketch001 = startSketchOn('XZ') + |> startProfileAt([2.16, 49.67], %) + |> line([101.49, 139.93], %) + |> line([60.04, -55.72], %) + |> line([1.29, -115.74], %) + |> line([-87.24, -47.08], %, $seg01) + |> tangentialArcTo([56.15, -94.58], %) + |> tangentialArcTo([14.68, -104.52], %) + |> lineTo([profileStartX(%), profileStartY(%)], %) + |> close(%) + const extrude001 = extrude(50, sketch001) + ` + const segmentSnippet = `line([60.04, -55.72], %)` + const extrudeSnippet = `const extrude001 = extrude(50, sketch001)` + const radius = createLiteral(5) as Value + const expectedCode = `const sketch001 = startSketchOn('XZ') + |> startProfileAt([2.16, 49.67], %) + |> line([101.49, 139.93], %) + |> line([60.04, -55.72], %, $seg02) + |> line([1.29, -115.74], %) + |> line([-87.24, -47.08], %, $seg01) + |> tangentialArcTo([56.15, -94.58], %) + |> tangentialArcTo([14.68, -104.52], %) + |> lineTo([profileStartX(%), profileStartY(%)], %) + |> close(%) +const extrude001 = extrude(50, sketch001) + |> fillet({ radius: 5, tags: [seg02] }, %)` + + await runFilletTest( + code, + segmentSnippet, + extrudeSnippet, + radius, + expectedCode + ) + }) + + /** + * 3. Case of existing tag in the fillet line + */ + + it('should add a fillet to a specific segment after extrusion with existing tag in that exact line', async () => { + const code = ` + const sketch001 = startSketchOn('XZ') + |> startProfileAt([2.16, 49.67], %) + |> line([101.49, 139.93], %) + |> line([60.04, -55.72], %) + |> line([1.29, -115.74], %) + |> line([-87.24, -47.08], %, $seg03) + |> tangentialArcTo([56.15, -94.58], %) + |> tangentialArcTo([14.68, -104.52], %) + |> lineTo([profileStartX(%), profileStartY(%)], %) + |> close(%) + const extrude001 = extrude(50, sketch001) + ` + const segmentSnippet = `line([-87.24, -47.08], %, $seg03)` + const extrudeSnippet = `const extrude001 = extrude(50, sketch001)` + const radius = createLiteral(5) as Value + const expectedCode = `const sketch001 = startSketchOn('XZ') + |> startProfileAt([2.16, 49.67], %) + |> line([101.49, 139.93], %) + |> line([60.04, -55.72], %) + |> line([1.29, -115.74], %) + |> line([-87.24, -47.08], %, $seg03) + |> tangentialArcTo([56.15, -94.58], %) + |> tangentialArcTo([14.68, -104.52], %) + |> lineTo([profileStartX(%), profileStartY(%)], %) + |> close(%) +const extrude001 = extrude(50, sketch001) + |> fillet({ radius: 5, tags: [seg03] }, %)` + + await runFilletTest( + code, + segmentSnippet, + extrudeSnippet, + radius, + expectedCode + ) + }) + + /** + * 4. Case of existing fillet on some other segment + */ + + it('should add another fillet after the existing fillet', async () => { + const code = `const sketch001 = startSketchOn('XZ') + |> startProfileAt([2.16, 49.67], %) + |> line([101.49, 139.93], %) + |> line([60.04, -55.72], %) + |> line([1.29, -115.74], %) + |> line([-87.24, -47.08], %, $seg03) + |> tangentialArcTo([56.15, -94.58], %) + |> tangentialArcTo([14.68, -104.52], %) + |> lineTo([profileStartX(%), profileStartY(%)], %) + |> close(%) + const extrude001 = extrude(50, sketch001) + |> fillet({ radius: 10, tags: [seg03] }, %)` + const segmentSnippet = `line([60.04, -55.72], %)` + const extrudeSnippet = `const extrude001 = extrude(50, sketch001)` + const radius = createLiteral(5) as Value + const expectedCode = `const sketch001 = startSketchOn('XZ') + |> startProfileAt([2.16, 49.67], %) + |> line([101.49, 139.93], %) + |> line([60.04, -55.72], %, $seg01) + |> line([1.29, -115.74], %) + |> line([-87.24, -47.08], %, $seg03) + |> tangentialArcTo([56.15, -94.58], %) + |> tangentialArcTo([14.68, -104.52], %) + |> lineTo([profileStartX(%), profileStartY(%)], %) + |> close(%) +const extrude001 = extrude(50, sketch001) + |> fillet({ radius: 10, tags: [seg03] }, %) + |> fillet({ radius: 5, tags: [seg01] }, %)` + + await runFilletTest( + code, + segmentSnippet, + extrudeSnippet, + radius, + expectedCode + ) + }) +}) + +describe('Testing isTagUsedInFillet', () => { + const code = `const sketch001 = startSketchOn('XZ') + |> startProfileAt([7.72, 4.13], %) + |> line([7.11, 3.48], %, $seg01) + |> line([-3.29, -13.85], %) + |> line([-6.37, 3.88], %, $seg02) + |> close(%) +const extrude001 = extrude(-5, sketch001) + |> fillet({ + radius: 1.11, + tags: [ + getOppositeEdge(seg01, %), + seg01, + getPreviousAdjacentEdge(seg02, %) + ] + }, %) +` + it('should correctly identify getOppositeEdge and baseEdge edges', () => { + const ast = parse(code) + if (err(ast)) return + const lineOfInterest = `line([7.11, 3.48], %, $seg01)` + const range: [number, number] = [ + code.indexOf(lineOfInterest), + code.indexOf(lineOfInterest) + lineOfInterest.length, + ] + const pathToNode = getNodePathFromSourceRange(ast, range) + if (err(pathToNode)) return + const callExp = getNodeFromPath( + ast, + pathToNode, + 'CallExpression' + ) + if (err(callExp)) return + const edges = isTagUsedInFillet({ ast, callExp: callExp.node }) + expect(edges).toEqual(['getOppositeEdge', 'baseEdge']) + }) + it('should correctly identify getPreviousAdjacentEdge edges', () => { + const ast = parse(code) + if (err(ast)) return + const lineOfInterest = `line([-6.37, 3.88], %, $seg02)` + const range: [number, number] = [ + code.indexOf(lineOfInterest), + code.indexOf(lineOfInterest) + lineOfInterest.length, + ] + const pathToNode = getNodePathFromSourceRange(ast, range) + if (err(pathToNode)) return + const callExp = getNodeFromPath( + ast, + pathToNode, + 'CallExpression' + ) + if (err(callExp)) return + const edges = isTagUsedInFillet({ ast, callExp: callExp.node }) + expect(edges).toEqual(['getPreviousAdjacentEdge']) + }) + it('should correctly identify no edges', () => { + const ast = parse(code) + if (err(ast)) return + const lineOfInterest = `line([-3.29, -13.85], %)` + const range: [number, number] = [ + code.indexOf(lineOfInterest), + code.indexOf(lineOfInterest) + lineOfInterest.length, + ] + const pathToNode = getNodePathFromSourceRange(ast, range) + if (err(pathToNode)) return + const callExp = getNodeFromPath( + ast, + pathToNode, + 'CallExpression' + ) + if (err(callExp)) return + const edges = isTagUsedInFillet({ ast, callExp: callExp.node }) + expect(edges).toEqual([]) + }) +}) diff --git a/src/lang/modifyAst/addFillet.ts b/src/lang/modifyAst/addFillet.ts new file mode 100644 index 000000000..09c03a768 --- /dev/null +++ b/src/lang/modifyAst/addFillet.ts @@ -0,0 +1,405 @@ +import { + ArrayExpression, + CallExpression, + ObjectExpression, + PathToNode, + Program, + Value, + VariableDeclaration, + VariableDeclarator, +} from '../wasm' +import { + createCallExpressionStdLib, + createLiteral, + createPipeSubstitution, + createObjectExpression, + createArrayExpression, + createIdentifier, + createPipeExpression, +} from '../modifyAst' +import { + getNodeFromPath, + getNodePathFromSourceRange, + hasSketchPipeBeenExtruded, + traverse, +} from '../queryAst' +import { + addTagForSketchOnFace, + getTagFromCallExpression, + sketchLineHelperMap, +} from '../std/sketch' +import { err } from 'lib/trap' +import { Selections, canFilletSelection } from 'lib/selections' +// import { forEach } from 'jszip' + +export function addFillet( + node: Program, + pathToSegmentNode: PathToNode, + pathToExtrudeNode: PathToNode, + radius = createLiteral(5) as Value + // shouldPipe = false, // TODO: Implement this feature +): { modifiedAst: Program; pathToFilletNode: PathToNode } | Error { + // close ast to make mutations safe + let _node: Program = JSON.parse(JSON.stringify(node)) + + /** + * Add Tag to the Segment Expression + */ + + // Find the specific sketch segment to tag with the new tag + const sketchSegmentChunk = getNodeFromPath( + _node, + pathToSegmentNode, + 'CallExpression' + ) + if (err(sketchSegmentChunk)) return sketchSegmentChunk + const { node: sketchSegmentNode } = sketchSegmentChunk as { + node: CallExpression + } + + // Check whether selection is a valid segment from sketchLineHelpersMap + if (!(sketchSegmentNode.callee.name in sketchLineHelperMap)) { + return new Error('Selection is not a sketch segment') + } + + // Add tag to the sketch segment or use existing tag + const taggedSegment = addTagForSketchOnFace( + { + // previousProgramMemory: programMemory, + pathToNode: pathToSegmentNode, + node: _node, + }, + sketchSegmentNode.callee.name + ) + if (err(taggedSegment)) return taggedSegment + const { tag } = taggedSegment + + /** + * Find Extrude Expression automatically + */ + + // 1. Get the sketch name + + /** + * Add Fillet to the Extrude expression + */ + + // Create the fillet call expression in one line + const filletCall = createCallExpressionStdLib('fillet', [ + createObjectExpression({ + radius: radius, + tags: createArrayExpression([createIdentifier(tag)]), + }), + createPipeSubstitution(), + ]) + + // Locate the extrude call + const extrudeChunk = getNodeFromPath( + _node, + pathToExtrudeNode, + 'VariableDeclaration' + ) + if (err(extrudeChunk)) return extrudeChunk + const { node: extrudeVarDecl } = extrudeChunk + + const extrudeDeclarator = extrudeVarDecl.declarations[0] + const extrudeInit = extrudeDeclarator.init + + if ( + !extrudeDeclarator || + (extrudeInit.type !== 'CallExpression' && + extrudeInit.type !== 'PipeExpression') + ) { + return new Error('Extrude PipeExpression / CallExpression not found.') + } + + // determine if extrude is in a PipeExpression or CallExpression + + // CallExpression - no fillet + // PipeExpression - fillet exists + + const getPathToNodeOfFilletLiteral = ( + pathToExtrudeNode: PathToNode, + extrudeDeclarator: VariableDeclarator, + tag: string + ): PathToNode => { + let pathToFilletObj: any + let inFillet = false + traverse(extrudeDeclarator.init, { + enter(node, path) { + if (node.type === 'CallExpression' && node.callee.name === 'fillet') { + inFillet = true + } + if (inFillet && node.type === 'ObjectExpression') { + const hasTag = node.properties.some((prop) => { + const isTagProp = prop.key.name === 'tags' + if (isTagProp && prop.value.type === 'ArrayExpression') { + return prop.value.elements.some( + (element) => + element.type === 'Identifier' && element.name === tag + ) + } + return false + }) + if (!hasTag) return false + pathToFilletObj = path + node.properties.forEach((prop, index) => { + if (prop.key.name === 'radius') { + pathToFilletObj.push( + ['properties', 'ObjectExpression'], + [index, 'index'], + ['value', 'Property'] + ) + } + }) + } + }, + leave(node) { + if (node.type === 'CallExpression' && node.callee.name === 'fillet') { + inFillet = false + } + }, + }) + let indexOfPipeExpression = pathToExtrudeNode.findIndex( + (path) => path[1] === 'PipeExpression' + ) + indexOfPipeExpression = + indexOfPipeExpression === -1 + ? pathToExtrudeNode.length + : indexOfPipeExpression + + return [ + ...pathToExtrudeNode.slice(0, indexOfPipeExpression), + ...pathToFilletObj, + ] + } + + if (extrudeInit.type === 'CallExpression') { + // 1. no fillet case + extrudeDeclarator.init = createPipeExpression([extrudeInit, filletCall]) + return { + modifiedAst: _node, + pathToFilletNode: getPathToNodeOfFilletLiteral( + pathToExtrudeNode, + extrudeDeclarator, + tag + ), + } + } else if (extrudeInit.type === 'PipeExpression') { + // 2. fillet case + + // there are 2 options here: + + const existingFilletCall = extrudeInit.body.find((node) => { + return node.type === 'CallExpression' && node.callee.name === 'fillet' + }) + + if (!existingFilletCall || existingFilletCall.type !== 'CallExpression') { + return new Error('Fillet CallExpression not found.') + } + + // check if the existing fillet has the same tag as the new fillet + let filletTag = null + if (existingFilletCall.arguments[0].type === 'ObjectExpression') { + const properties = (existingFilletCall.arguments[0] as ObjectExpression) + .properties + const tagsProperty = properties.find((prop) => prop.key.name === 'tags') + if (tagsProperty && tagsProperty.value.type === 'ArrayExpression') { + const elements = (tagsProperty.value as ArrayExpression).elements + if (elements.length > 0 && elements[0].type === 'Identifier') { + filletTag = elements[0].name + } + } + } else { + return new Error('Expected an ObjectExpression node') + } + + if (filletTag !== tag) { + extrudeInit.body.push(filletCall) + return { + modifiedAst: _node, + pathToFilletNode: getPathToNodeOfFilletLiteral( + pathToExtrudeNode, + extrudeDeclarator, + tag + ), + } + } + } else { + return new Error('Unsupported extrude type.') + } + + return new Error('Unsupported extrude type.') +} + +export const hasValidFilletSelection = ({ + selectionRanges, + ast, + code, +}: { + selectionRanges: Selections + ast: Program + code: string +}) => { + // case 0: check if there is anything filletable in the scene + let extrudeExists = false + traverse(ast, { + enter(node) { + if (node.type === 'CallExpression' && node.callee.name === 'extrude') { + extrudeExists = true + } + }, + }) + if (!extrudeExists) return false + + // case 1: nothing selected, test whether the extrusion exists + if (selectionRanges) { + if (selectionRanges.codeBasedSelections.length === 0) { + return true + } + const range0 = selectionRanges.codeBasedSelections[0].range[0] + const codeLength = code.length + if (range0 === codeLength) { + return true + } + } + + // case 2: sketch segment selected, test whether it is extruded + // TODO: add loft / sweep check + if (selectionRanges.codeBasedSelections.length > 0) { + const isExtruded = hasSketchPipeBeenExtruded( + selectionRanges.codeBasedSelections[0], + ast + ) + if (isExtruded) { + const pathToSelectedNode = getNodePathFromSourceRange( + ast, + selectionRanges.codeBasedSelections[0].range + ) + const segmentNode = getNodeFromPath( + ast, + pathToSelectedNode, + 'CallExpression' + ) + if (err(segmentNode)) return false + if (segmentNode.node.type === 'CallExpression') { + const segmentName = segmentNode.node.callee.name + if (segmentName in sketchLineHelperMap) { + const edges = isTagUsedInFillet({ + ast, + callExp: segmentNode.node, + }) + // edge has already been filleted + if ( + ['edge', 'default'].includes( + selectionRanges.codeBasedSelections[0].type + ) && + edges.includes('baseEdge') + ) + return false + return true + } else { + return false + } + } + } else { + return false + } + } + + return canFilletSelection(selectionRanges) +} + +type EdgeTypes = + | 'baseEdge' + | 'getNextAdjacentEdge' + | 'getPreviousAdjacentEdge' + | 'getOppositeEdge' + +export const isTagUsedInFillet = ({ + ast, + callExp, +}: { + ast: Program + callExp: CallExpression +}): Array => { + const tag = getTagFromCallExpression(callExp) + if (err(tag)) return [] + + let inFillet = false + let inObj = false + let inTagHelper: EdgeTypes | '' = '' + const edges: Array = [] + traverse(ast, { + enter: (node) => { + if (node.type === 'CallExpression' && node.callee.name === 'fillet') { + inFillet = true + } + if (inFillet && node.type === 'ObjectExpression') { + node.properties.forEach((prop) => { + if ( + prop.key.name === 'tags' && + prop.value.type === 'ArrayExpression' + ) { + inObj = true + } + }) + } + if ( + inObj && + inFillet && + node.type === 'CallExpression' && + (node.callee.name === 'getOppositeEdge' || + node.callee.name === 'getNextAdjacentEdge' || + node.callee.name === 'getPreviousAdjacentEdge') + ) { + inTagHelper = node.callee.name + } + if ( + inObj && + inFillet && + !inTagHelper && + node.type === 'Identifier' && + node.name === tag + ) { + edges.push('baseEdge') + } + if ( + inObj && + inFillet && + inTagHelper && + node.type === 'Identifier' && + node.name === tag + ) { + edges.push(inTagHelper) + } + }, + leave: (node) => { + if (node.type === 'CallExpression' && node.callee.name === 'fillet') { + inFillet = false + } + if (inFillet && node.type === 'ObjectExpression') { + node.properties.forEach((prop) => { + if ( + prop.key.name === 'tags' && + prop.value.type === 'ArrayExpression' + ) { + inObj = true + } + }) + } + if ( + inObj && + inFillet && + node.type === 'CallExpression' && + (node.callee.name === 'getOppositeEdge' || + node.callee.name === 'getNextAdjacentEdge' || + node.callee.name === 'getPreviousAdjacentEdge') + ) { + inTagHelper = '' + } + }, + }) + + return edges +} diff --git a/src/lang/std/sketch.test.ts b/src/lang/std/sketch.test.ts index 57150a361..397d3c228 100644 --- a/src/lang/std/sketch.test.ts +++ b/src/lang/std/sketch.test.ts @@ -221,7 +221,7 @@ describe('testing addTagForSketchOnFace', () => { const pathToNode = getNodePathFromSourceRange(ast, sourceRange) const sketchOnFaceRetVal = addTagForSketchOnFace( { - previousProgramMemory: programMemory, + // previousProgramMemory: programMemory, // redundant? pathToNode, node: ast, }, diff --git a/src/lang/std/sketch.ts b/src/lang/std/sketch.ts index 8729ebb50..fb39c878a 100644 --- a/src/lang/std/sketch.ts +++ b/src/lang/std/sketch.ts @@ -28,7 +28,6 @@ import { createPipeExpression, splitPathAtPipeExpression } from '../modifyAst' import { SketchLineHelper, - ModifyAstBase, TransformCallback, ConstrainInfo, RawValues, @@ -37,6 +36,7 @@ import { SingleValueInput, VarValueKeys, ArrayOrObjItemInput, + AddTagInfo, } from 'lang/std/stdTypes' import { @@ -308,6 +308,18 @@ function singleRawValueHelper( ] } +function getTag(index = 2): SketchLineHelper['getTag'] { + return (callExp: CallExpression) => { + if (callExp.type !== 'CallExpression') + return new Error('Not a CallExpression') + const arg = callExp.arguments?.[index] + if (!arg) return new Error('No argument') + if (arg.type !== 'TagDeclarator') + return new Error('Tag not a TagDeclarator') + return arg.value + } +} + export const lineTo: SketchLineHelper = { add: ({ node, @@ -377,6 +389,7 @@ export const lineTo: SketchLineHelper = { pathToNode, } }, + getTag: getTag(), addTag: addTag(), getConstraintInfo: (callExp, ...args) => commonConstraintInfoHelper( @@ -503,6 +516,7 @@ export const line: SketchLineHelper = { pathToNode, } }, + getTag: getTag(), addTag: addTag(), getConstraintInfo: (callExp, ...args) => commonConstraintInfoHelper( @@ -563,6 +577,7 @@ export const xLineTo: SketchLineHelper = { pathToNode, } }, + getTag: getTag(), addTag: addTag(), getConstraintInfo: (callExp, ...args) => horzVertConstraintInfoHelper( @@ -623,6 +638,7 @@ export const yLineTo: SketchLineHelper = { pathToNode, } }, + getTag: getTag(), addTag: addTag(), getConstraintInfo: (callExp, ...args) => horzVertConstraintInfoHelper( @@ -682,6 +698,7 @@ export const xLine: SketchLineHelper = { pathToNode, } }, + getTag: getTag(), addTag: addTag(), getConstraintInfo: (callExp, ...args) => horzVertConstraintInfoHelper( @@ -738,6 +755,7 @@ export const yLine: SketchLineHelper = { pathToNode, } }, + getTag: getTag(), addTag: addTag(), getConstraintInfo: (callExp, ...args) => horzVertConstraintInfoHelper( @@ -830,6 +848,7 @@ export const tangentialArcTo: SketchLineHelper = { pathToNode, } }, + getTag: getTag(), addTag: addTag(), getConstraintInfo: (callExp: CallExpression, code, pathToNode) => { if (callExp.type !== 'CallExpression') return [] @@ -948,6 +967,7 @@ export const angledLine: SketchLineHelper = { pathToNode, } }, + getTag: getTag(), addTag: addTag(), getConstraintInfo: (callExp, ...args) => commonConstraintInfoHelper( @@ -1044,6 +1064,7 @@ export const angledLineOfXLength: SketchLineHelper = { pathToNode, } }, + getTag: getTag(), addTag: addTag(), getConstraintInfo: (callExp, ...args) => commonConstraintInfoHelper( @@ -1140,6 +1161,7 @@ export const angledLineOfYLength: SketchLineHelper = { pathToNode, } }, + getTag: getTag(), addTag: addTag(), getConstraintInfo: (callExp, ...args) => commonConstraintInfoHelper( @@ -1227,6 +1249,7 @@ export const angledLineToX: SketchLineHelper = { pathToNode, } }, + getTag: getTag(), addTag: addTag(), getConstraintInfo: (callExp, ...args) => commonConstraintInfoHelper( @@ -1316,6 +1339,7 @@ export const angledLineToY: SketchLineHelper = { pathToNode, } }, + getTag: getTag(), addTag: addTag(), getConstraintInfo: (callExp, ...args) => commonConstraintInfoHelper( @@ -1440,6 +1464,7 @@ export const angledLineThatIntersects: SketchLineHelper = { pathToNode, } }, + getTag: getTag(), addTag: addTag(), getConstraintInfo: (callExp: CallExpression, code, pathToNode) => { if (callExp.type !== 'CallExpression') return [] @@ -1792,10 +1817,7 @@ export function replaceSketchLine({ return { modifiedAst, valueUsedInTransform, pathToNode } } -export function addTagForSketchOnFace( - a: ModifyAstBase, - expressionName: string -) { +export function addTagForSketchOnFace(a: AddTagInfo, expressionName: string) { if (expressionName === 'close') { return addTag(1)(a) } @@ -1806,6 +1828,17 @@ export function addTagForSketchOnFace( return new Error(`"${expressionName}" is not a sketch line helper`) } +export function getTagFromCallExpression( + callExp: CallExpression +): string | Error { + if (callExp.callee.name === 'close') return getTag(1)(callExp) + if (callExp.callee.name in sketchLineHelperMap) { + const { getTag } = sketchLineHelperMap[callExp.callee.name] + return getTag(callExp) + } + return new Error(`"${callExp.callee.name}" is not a sketch line helper`) +} + function isAngleLiteral(lineArugement: Value): boolean { return lineArugement?.type === 'ArrayExpression' ? isLiteralArrayOrStatic(lineArugement.elements[0]) @@ -1816,9 +1849,7 @@ function isAngleLiteral(lineArugement: Value): boolean { : false } -type addTagFn = ( - a: ModifyAstBase -) => { modifiedAst: Program; tag: string } | Error +type addTagFn = (a: AddTagInfo) => { modifiedAst: Program; tag: string } | Error function addTag(tagIndex = 2): addTagFn { return ({ node, pathToNode }) => { diff --git a/src/lang/std/stdTypes.ts b/src/lang/std/stdTypes.ts index 0f2c619b0..400c7d9dc 100644 --- a/src/lang/std/stdTypes.ts +++ b/src/lang/std/stdTypes.ts @@ -32,6 +32,11 @@ export interface ModifyAstBase { pathToNode: PathToNode } +export interface AddTagInfo { + node: Program + pathToNode: PathToNode +} + interface addCall extends ModifyAstBase { to: [number, number] from: [number, number] @@ -127,7 +132,8 @@ export interface SketchLineHelper { pathToNode: PathToNode } | Error - addTag: (a: ModifyAstBase) => + getTag: (a: CallExpression) => string | Error + addTag: (a: AddTagInfo) => | { modifiedAst: Program tag: string diff --git a/src/lib/commandBarConfigs/modelingCommandConfig.ts b/src/lib/commandBarConfigs/modelingCommandConfig.ts index 9555eded4..9bbd30f01 100644 --- a/src/lib/commandBarConfigs/modelingCommandConfig.ts +++ b/src/lib/commandBarConfigs/modelingCommandConfig.ts @@ -27,6 +27,11 @@ export type ModelingCommandSchema = { // result: (typeof EXTRUSION_RESULTS)[number] distance: KclCommandValue } + Fillet: { + // todo + selection: Selections + radius: KclCommandValue + } 'change tool': { tool: SketchTool } @@ -185,4 +190,36 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig< }, }, }, + Fillet: { + // todo + description: 'Fillet edge', + icon: 'fillet', + needsReview: true, + args: { + selection: { + inputType: 'selection', + selectionTypes: [ + 'default', + 'line-end', + 'line-mid', + 'extrude-wall', // to fix: accespts only this selection type + 'start-cap', + 'end-cap', + 'point', + 'edge', + 'line', + 'arc', + 'all', + ], + multiple: true, // TODO: multiple selection like in extrude command + required: true, + skip: true, + }, + radius: { + inputType: 'kcl', + defaultValue: KCL_DEFAULT_LENGTH, + required: true, + }, + }, + }, } diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 0e4c8ab19..092dc1582 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -51,6 +51,7 @@ export const ONBOARDING_PROJECT_NAME = 'Tutorial Project $nn' export const KCL_DEFAULT_CONSTANT_PREFIXES = { SKETCH: 'sketch', EXTRUDE: 'extrude', + SEGMENT: 'seg', } as const /** The default KCL length expression */ export const KCL_DEFAULT_LENGTH = `5` diff --git a/src/lib/selections.ts b/src/lib/selections.ts index 32274967e..fa1fad614 100644 --- a/src/lib/selections.ts +++ b/src/lib/selections.ts @@ -406,6 +406,17 @@ export function canExtrudeSelection(selection: Selections) { ) } +export function canFilletSelection(selection: Selections) { + const commonNodes = selection.codeBasedSelections.map((_, i) => + buildCommonNodeFromSelection(selection, i) + ) // TODO FILLET DUMMY PLACEHOLDER + return ( + !!isSketchPipe(selection) && + commonNodes.every((n) => nodeHasClose(n)) && + commonNodes.every((n) => !nodeHasExtrude(n)) + ) +} + function canExtrudeSelectionItem(selection: Selections, i: number) { const commonNode = buildCommonNodeFromSelection(selection, i) diff --git a/src/lib/singletons.ts b/src/lib/singletons.ts index 01f4e6838..607a912a7 100644 --- a/src/lib/singletons.ts +++ b/src/lib/singletons.ts @@ -37,4 +37,7 @@ if (typeof window !== 'undefined') { document.addEventListener('mousemove', (e) => console.log(`await page.mouse.click(${e.clientX}, ${e.clientY})`) ) + ;(window as any).enableFillet = () => { + ;(window as any)._enableFillet = true + } } diff --git a/src/lib/useCalculateKclExpression.ts b/src/lib/useCalculateKclExpression.ts index dcc0b7e62..ed0e48f44 100644 --- a/src/lib/useCalculateKclExpression.ts +++ b/src/lib/useCalculateKclExpression.ts @@ -6,7 +6,7 @@ import { PrevVariable, findAllPreviousVariables } from 'lang/queryAst' import { Value, parse } from 'lang/wasm' import { useEffect, useRef, useState } from 'react' import { executeAst } from 'lang/langHelpers' -import { trap } from 'lib/trap' +import { err, trap } from 'lib/trap' const isValidVariableName = (name: string) => /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name) @@ -86,6 +86,7 @@ export function useCalculateKclExpression({ const execAstAndSetResult = async () => { const _code = `const __result__ = ${value}` const ast = parse(_code) + if (err(ast)) return if (trap(ast, { suppress: true })) return const _programMem: any = { root: {}, return: null } diff --git a/src/machines/modelingMachine.ts b/src/machines/modelingMachine.ts index c19dce644..9663be425 100644 --- a/src/machines/modelingMachine.ts +++ b/src/machines/modelingMachine.ts @@ -38,6 +38,7 @@ import { deleteFromSelection, extrudeSketch, } from 'lang/modifyAst' +import { addFillet } from 'lang/modifyAst/addFillet' import { getNodeFromPath } from '../lang/queryAst' import { applyConstraintEqualAngle, @@ -207,6 +208,7 @@ export type ModelingMachineEvent = | { type: 'Re-execute' } | { type: 'Export'; data: ModelingCommandSchema['Export'] } | { type: 'Extrude'; data?: ModelingCommandSchema['Extrude'] } + | { type: 'Fillet'; data?: ModelingCommandSchema['Fillet'] } | { type: 'Add rectangle origin' data: [x: number, y: number] @@ -317,6 +319,13 @@ export const modelingMachine = createMachine( internal: true, }, + Fillet: { + target: 'idle', + cond: 'has valid fillet selection', // TODO: fix selections + actions: ['AST fillet'], + internal: true, + }, + Export: { target: 'idle', internal: true, @@ -1102,6 +1111,71 @@ export const modelingMachine = createMachine( await kclManager.updateAst(modifiedAst, true) }, + 'AST fillet': async (_, event) => { + if (!event.data) return + + const { selection, radius } = event.data + let ast = kclManager.ast + + if ( + 'variableName' in radius && + radius.variableName && + radius.insertIndex !== undefined + ) { + const newBody = [...ast.body] + newBody.splice(radius.insertIndex, 0, radius.variableDeclarationAst) + ast.body = newBody + } + + const pathToSegmentNode = getNodePathFromSourceRange( + ast, + selection.codeBasedSelections[0].range + ) + + const varDecNode = getNodeFromPath( + ast, + pathToSegmentNode, + 'VariableDeclaration' + ) + if (err(varDecNode)) return + const sketchVar = varDecNode.node.declarations[0].id.name + const sketchGroup = kclManager.programMemory.root[sketchVar] + if (sketchGroup.type !== 'SketchGroup') return + const idArtifact = engineCommandManager.artifactMap[sketchGroup.id] + if (idArtifact.commandType !== 'start_path') return + const extrusionArtifactId = (idArtifact as any)?.extrusions?.[0] + if (typeof extrusionArtifactId !== 'string') return + const extrusionArtifact = (engineCommandManager.artifactMap as any)[ + extrusionArtifactId + ] + if (!extrusionArtifact) return + const pathToExtrudeNode = getNodePathFromSourceRange( + ast, + extrusionArtifact.range + ) + + // we assume that there is only one body related to the sketch + // and apply the fillet to it + + const addFilletResult = addFillet( + ast, + pathToSegmentNode, + pathToExtrudeNode, + 'variableName' in radius + ? radius.variableIdentifierAst + : radius.valueAst + ) + + if (trap(addFilletResult)) return + const { modifiedAst, pathToFilletNode } = addFilletResult + + const updatedAst = await kclManager.updateAst(modifiedAst, true, { + focusPath: pathToFilletNode, + }) + if (updatedAst?.selections) { + editorManager.selectRange(updatedAst?.selections) + } + }, 'conditionally equip line tool': (_, { type }) => { if (type === 'done.invoke.animate-to-face') { sceneInfra.modelingSend({