diff --git a/e2e/playwright/command-bar-tests.spec.ts b/e2e/playwright/command-bar-tests.spec.ts index b8bca5d3e..9ff85fb57 100644 --- a/e2e/playwright/command-bar-tests.spec.ts +++ b/e2e/playwright/command-bar-tests.spec.ts @@ -10,6 +10,8 @@ test.describe('Command bar tests', () => { test('Extrude from command bar selects extrude line after', async ({ page, homePage, + toolbar, + cmdBar, }) => { await page.addInitScript(async () => { localStorage.setItem( @@ -35,20 +37,35 @@ test.describe('Command bar tests', () => { // Click the line of code for xLine. await page.getByText(`close()`).click() // TODO remove this and reinstate // await topHorzSegmentClick() - await page.waitForTimeout(100) - await page.getByRole('button', { name: 'Extrude' }).click() - await page.waitForTimeout(200) - await page.keyboard.press('Enter') - await page.waitForTimeout(200) - await page.keyboard.press('Enter') - await page.waitForTimeout(200) + await toolbar.extrudeButton.click() + await cmdBar.expectState({ + stage: 'arguments', + commandName: 'Extrude', + currentArgKey: 'sketches', + currentArgValue: '', + headerArguments: { + Sketches: '', + Length: '', + }, + highlightedHeaderArg: 'sketches', + }) + await cmdBar.progressCmdBar() + await cmdBar.progressCmdBar() + await cmdBar.expectState({ + stage: 'review', + commandName: 'Extrude', + headerArguments: { + Sketches: '1 segment', + Length: '5', + }, + }) + await cmdBar.progressCmdBar() await expect(page.locator('.cm-activeLine')).toHaveText( `extrude001 = extrude(sketch001, length = ${KCL_DEFAULT_LENGTH})` ) }) - // TODO: fix this test after the electron migration test('Fillet from command bar', async ({ page, homePage }) => { await page.addInitScript(async () => { localStorage.setItem( @@ -269,21 +286,22 @@ test.describe('Command bar tests', () => { await cmdBar.cmdOptions.getByText('Extrude').click() // Assert that we're on the selection step - await expect(page.getByRole('button', { name: 'selection' })).toBeDisabled() + await expect(page.getByRole('button', { name: 'sketches' })).toBeDisabled() // Select a face await page.mouse.move(700, 200) await page.mouse.click(700, 200) + await cmdBar.progressCmdBar() // Assert that we're on the distance step await expect( - page.getByRole('button', { name: 'distance', exact: false }) + page.getByRole('button', { name: 'length', exact: false }) ).toBeDisabled() // Assert that the an alternative variable name is chosen, // since the default variable name is already in use (distance) await page.getByRole('button', { name: 'Create new variable' }).click() await expect(page.getByPlaceholder('Variable name')).toHaveValue( - 'distance001' + 'length001' ) const continueButton = page.getByRole('button', { name: 'Continue' }) @@ -297,7 +315,7 @@ test.describe('Command bar tests', () => { // Assert we're back on the distance step await expect( - page.getByRole('button', { name: 'distance', exact: false }) + page.getByRole('button', { name: 'length', exact: false }) ).toBeDisabled() await continueButton.click() @@ -306,7 +324,7 @@ test.describe('Command bar tests', () => { await u.waitForCmdReceive('extrude') await expect(page.locator('.cm-content')).toContainText( - 'extrude001 = extrude(sketch001, length = distance001)' + 'extrude001 = extrude(sketch001, length = length001)' ) }) diff --git a/e2e/playwright/editor-tests.spec.ts b/e2e/playwright/editor-tests.spec.ts index 1528406dd..261693f89 100644 --- a/e2e/playwright/editor-tests.spec.ts +++ b/e2e/playwright/editor-tests.spec.ts @@ -1085,6 +1085,9 @@ sketch001 = startSketchOn(XZ) page, context, homePage, + toolbar, + cmdBar, + scene, }) => { const u = await getUtils(page) await context.addInitScript(async () => { @@ -1128,17 +1131,30 @@ sketch001 = startSketchOn(XZ) await page.waitForTimeout(100) await page.getByText('startProfile(at = [4.61, -14.01])').click() - await expect(page.getByRole('button', { name: 'Extrude' })).toBeVisible() - await page.getByRole('button', { name: 'Extrude' }).click() - - await expect(page.getByTestId('command-bar')).toBeVisible() - await page.waitForTimeout(100) - - await page.getByRole('button', { name: 'arrow right Continue' }).click() - await page.waitForTimeout(100) - await expect(page.getByText('Confirm Extrude')).toBeVisible() - await page.getByRole('button', { name: 'checkmark Submit command' }).click() - await page.waitForTimeout(100) + await toolbar.extrudeButton.click() + await cmdBar.progressCmdBar() + await cmdBar.expectState({ + stage: 'arguments', + currentArgKey: 'length', + currentArgValue: '5', + headerArguments: { + Sketches: '1 face', + Length: '', + }, + highlightedHeaderArg: 'length', + commandName: 'Extrude', + }) + await cmdBar.progressCmdBar() + await cmdBar.expectState({ + stage: 'review', + headerArguments: { + Sketches: '1 face', + Length: '5', + }, + commandName: 'Extrude', + }) + await cmdBar.progressCmdBar() + await scene.settled(cmdBar) // expect the code to have changed await expect(page.locator('.cm-content')).toHaveText( diff --git a/e2e/playwright/feature-tree-pane.spec.ts b/e2e/playwright/feature-tree-pane.spec.ts index c5d6a67e9..210eea92e 100644 --- a/e2e/playwright/feature-tree-pane.spec.ts +++ b/e2e/playwright/feature-tree-pane.spec.ts @@ -229,7 +229,7 @@ test.describe('Feature Tree pane', () => { const initialCode = `sketch001 = startSketchOn(XZ) |> circle(center = [0, 0], radius = 5) renamedExtrude = extrude(sketch001, length = ${initialInput})` - const newConstantName = 'distance001' + const newConstantName = 'length001' const expectedCode = `${newConstantName} = 23 sketch001 = startSketchOn(XZ) |> circle(center = [0, 0], radius = 5) @@ -270,12 +270,12 @@ test.describe('Feature Tree pane', () => { await cmdBar.expectState({ commandName: 'Extrude', stage: 'arguments', - currentArgKey: 'distance', + currentArgKey: 'length', currentArgValue: initialInput, headerArguments: { - Distance: initialInput, + Length: initialInput, }, - highlightedHeaderArg: 'distance', + highlightedHeaderArg: 'length', }) }) @@ -290,7 +290,7 @@ test.describe('Feature Tree pane', () => { stage: 'review', headerArguments: { // The calculated value is shown in the argument summary - Distance: initialInput, + Length: initialInput, }, commandName: 'Extrude', }) diff --git a/e2e/playwright/point-click.spec.ts b/e2e/playwright/point-click.spec.ts index 4b071f155..84fcf9bf7 100644 --- a/e2e/playwright/point-click.spec.ts +++ b/e2e/playwright/point-click.spec.ts @@ -6,6 +6,7 @@ import type { EditorFixture } from '@e2e/playwright/fixtures/editorFixture' import type { SceneFixture } from '@e2e/playwright/fixtures/sceneFixture' import type { ToolbarFixture } from '@e2e/playwright/fixtures/toolbarFixture' import { expect, test } from '@e2e/playwright/zoo-test' +import { bracket } from '@e2e/playwright/fixtures/bracket' // test file is for testing point an click code gen functionality that's not sketch mode related @@ -75,10 +76,19 @@ test.describe('Point-and-click tests', () => { await toolbar.extrudeButton.click() await cmdBar.expectState({ stage: 'arguments', - currentArgKey: 'distance', + currentArgKey: 'sketches', + currentArgValue: '', + headerArguments: { Sketches: '', Length: '' }, + highlightedHeaderArg: 'sketches', + commandName: 'Extrude', + }) + await cmdBar.progressCmdBar() + await cmdBar.expectState({ + stage: 'arguments', + currentArgKey: 'length', currentArgValue: '5', - headerArguments: { Selection: '1 face', Distance: '' }, - highlightedHeaderArg: 'distance', + headerArguments: { Sketches: '1 face', Length: '' }, + highlightedHeaderArg: 'length', commandName: 'Extrude', }) await cmdBar.progressCmdBar() @@ -88,7 +98,7 @@ test.describe('Point-and-click tests', () => { await cmdBar.expectState({ stage: 'review', - headerArguments: { Selection: '1 face', Distance: '5' }, + headerArguments: { Sketches: '1 face', Length: '5' }, commandName: 'Extrude', }) await cmdBar.progressCmdBar() @@ -97,6 +107,102 @@ test.describe('Point-and-click tests', () => { }) }) + test('Verify in-pipe extrudes in bracket can be edited', async ({ + tronApp, + context, + editor, + homePage, + page, + scene, + toolbar, + cmdBar, + }) => { + await context.addInitScript((initialCode) => { + localStorage.setItem('persistCode', initialCode) + }, bracket) + await homePage.goToModelingScene() + await scene.settled(cmdBar) + + await test.step(`Edit first extrude via feature tree`, async () => { + await (await toolbar.getFeatureTreeOperation('Extrude', 0)).dblclick() + await cmdBar.expectState({ + stage: 'arguments', + currentArgKey: 'length', + currentArgValue: 'width', + headerArguments: { + Length: '5', + }, + highlightedHeaderArg: 'length', + commandName: 'Extrude', + }) + await page.keyboard.insertText('width - 0.001') + await cmdBar.progressCmdBar() + await cmdBar.expectState({ + stage: 'review', + headerArguments: { + Length: '4.999', + }, + commandName: 'Extrude', + }) + await cmdBar.progressCmdBar() + await editor.expectEditor.toContain('extrude(length = width - 0.001)') + }) + + await test.step(`Edit second extrude via feature tree`, async () => { + await (await toolbar.getFeatureTreeOperation('Extrude', 1)).dblclick() + await cmdBar.expectState({ + stage: 'arguments', + currentArgKey: 'length', + currentArgValue: '-thickness - .01', + headerArguments: { + Length: '-0.3949', + }, + highlightedHeaderArg: 'length', + commandName: 'Extrude', + }) + await page.keyboard.insertText('-thickness - .01 - 0.001') + await cmdBar.progressCmdBar() + await cmdBar.expectState({ + stage: 'review', + headerArguments: { + Length: '-0.3959', + }, + commandName: 'Extrude', + }) + await cmdBar.progressCmdBar() + await editor.expectEditor.toContain( + 'extrude(length = -thickness - .01 - 0.001)' + ) + }) + + await test.step(`Edit third extrude via feature tree`, async () => { + await (await toolbar.getFeatureTreeOperation('Extrude', 2)).dblclick() + await cmdBar.expectState({ + stage: 'arguments', + currentArgKey: 'length', + currentArgValue: '-thickness - 0.1', + headerArguments: { + Length: '-0.4849', + }, + highlightedHeaderArg: 'length', + commandName: 'Extrude', + }) + await page.keyboard.insertText('-thickness - 0.1 - 0.001') + await cmdBar.progressCmdBar() + await cmdBar.expectState({ + stage: 'review', + headerArguments: { + Length: '-0.4859', + }, + commandName: 'Extrude', + }) + await cmdBar.progressCmdBar() + await editor.expectEditor.toContain( + 'extrude(length = -thickness - 0.1 - 0.001)' + ) + }) + }) + test.describe('verify sketch on chamfer works', () => { const _sketchOnAChamfer = ( @@ -1483,11 +1589,11 @@ extrude001 = extrude(profile001, length = 100) cmdBar, }) => { const initialCode = `sketch001 = startSketchOn(XZ) - |> circle(center = [0, 0], radius = 30) - plane001 = offsetPlane(XZ, offset = 50) - sketch002 = startSketchOn(plane001) - |> circle(center = [0, 0], radius = 20) -` + |> circle(center = [0, 0], radius = 30) +plane001 = offsetPlane(XZ, offset = 50) +sketch002 = startSketchOn(plane001) + |> circle(center = [0, 0], radius = 20) + ` await context.addInitScript((initialCode) => { localStorage.setItem('persistCode', initialCode) }, initialCode) @@ -1523,14 +1629,20 @@ extrude001 = extrude(profile001, length = 100) .toBe(1) await cmdBar.expectState({ stage: 'arguments', - currentArgKey: 'selection', + currentArgKey: 'sketches', currentArgValue: '', - headerArguments: { Selection: '' }, - highlightedHeaderArg: 'selection', + headerArguments: { Sketches: '' }, + highlightedHeaderArg: 'sketches', commandName: 'Loft', }) await selectSketches() await cmdBar.progressCmdBar() + await cmdBar.expectState({ + stage: 'review', + headerArguments: { Sketches: '2 faces' }, + commandName: 'Loft', + }) + await cmdBar.submit() }) } else { await test.step(`Preselect the two sketches`, async () => { @@ -1539,7 +1651,21 @@ extrude001 = extrude(profile001, length = 100) await test.step(`Go through the command bar flow with preselected sketches`, async () => { await toolbar.loftButton.click() + await cmdBar.expectState({ + stage: 'arguments', + currentArgKey: 'sketches', + currentArgValue: '', + headerArguments: { Sketches: '' }, + highlightedHeaderArg: 'sketches', + commandName: 'Loft', + }) await cmdBar.progressCmdBar() + await cmdBar.expectState({ + stage: 'review', + headerArguments: { Sketches: '2 faces' }, + commandName: 'Loft', + }) + await cmdBar.submit() }) } @@ -1681,8 +1807,7 @@ sketch002 = startSketchOn(XZ) testPoint.x - 50, testPoint.y ) - const sweepDeclaration = - 'sweep001 = sweep(profile001, path = sketch002, sectional = false)' + const sweepDeclaration = 'sweep001 = sweep(profile001, path = sketch002)' const editedSweepDeclaration = 'sweep001 = sweep(profile001, path = sketch002, sectional = true)' @@ -1698,37 +1823,49 @@ sketch002 = startSketchOn(XZ) .toBe(1) await cmdBar.expectState({ commandName: 'Sweep', - currentArgKey: 'target', + currentArgKey: 'sketches', currentArgValue: '', headerArguments: { Sectional: '', - Target: '', - Trajectory: '', + Sketches: '', + Path: '', }, - highlightedHeaderArg: 'target', + highlightedHeaderArg: 'sketches', stage: 'arguments', }) await clickOnSketch1() + await cmdBar.progressCmdBar() await cmdBar.expectState({ commandName: 'Sweep', - currentArgKey: 'trajectory', + currentArgKey: 'path', currentArgValue: '', headerArguments: { Sectional: '', - Target: '1 face', - Trajectory: '', + Sketches: '1 face', + Path: '', }, - highlightedHeaderArg: 'trajectory', + highlightedHeaderArg: 'path', stage: 'arguments', }) await clickOnSketch2() - await page.waitForTimeout(500) + await cmdBar.expectState({ + commandName: 'Sweep', + currentArgKey: 'path', + currentArgValue: '', + headerArguments: { + Sectional: '', + Sketches: '1 face', + Path: '', + }, + highlightedHeaderArg: 'path', + stage: 'arguments', + }) await cmdBar.progressCmdBar() await cmdBar.expectState({ commandName: 'Sweep', headerArguments: { - Target: '1 face', - Trajectory: '1 segment', + Sketches: '1 face', + Path: '1 segment', Sectional: '', }, stage: 'review', @@ -1837,31 +1974,31 @@ sketch002 = startSketchOn(XZ) .toBe(1) await cmdBar.expectState({ commandName: 'Sweep', - currentArgKey: 'target', + currentArgKey: 'sketches', currentArgValue: '', headerArguments: { Sectional: '', - Target: '', - Trajectory: '', + Sketches: '', + Path: '', }, - highlightedHeaderArg: 'target', + highlightedHeaderArg: 'sketches', stage: 'arguments', }) await clickOnSketch1() + await cmdBar.progressCmdBar() await cmdBar.expectState({ commandName: 'Sweep', - currentArgKey: 'trajectory', + currentArgKey: 'path', currentArgValue: '', headerArguments: { Sectional: '', - Target: '1 face', - Trajectory: '', + Sketches: '1 face', + Path: '', }, - highlightedHeaderArg: 'trajectory', + highlightedHeaderArg: 'path', stage: 'arguments', }) await clickOnSketch2() - await page.waitForTimeout(500) await cmdBar.progressCmdBar() await expect( page.getByText('Unable to sweep with the current selection. Reason:') @@ -3513,6 +3650,7 @@ tag=$rectangleSegmentC002, await cmdBar.progressCmdBar() await cmdBar.progressCmdBar() await cmdBar.progressCmdBar() + await cmdBar.progressCmdBar() const newCodeToFind = `revolve001 = revolve(sketch002, angle = 360, axis = X)` expect(editor.expectEditor.toContain(newCodeToFind)).toBeTruthy() @@ -3585,6 +3723,7 @@ sketch002 = startSketchOn(extrude001, face = rectangleSegmentA001) await editor.scrollToText(codeToSelection) await page.getByText(codeToSelection).click() await toolbar.revolveButton.click() + await cmdBar.progressCmdBar() await page.getByText('Edge', { exact: true }).click() const lineCodeToSelection = `angledLine(angle = 0, length = 202.6, tag = $rectangleSegmentA001)` await page.getByText(lineCodeToSelection).click() @@ -3677,6 +3816,7 @@ sketch002 = startSketchOn(extrude001, face = rectangleSegmentA001) await page.waitForTimeout(1000) await editor.scrollToText(codeToSelection) await page.getByText(codeToSelection).click() + await cmdBar.progressCmdBar() await expect.poll(() => page.getByText('AxisOrEdge').count()).toBe(2) await page.getByText('Edge', { exact: true }).click() const lineCodeToSelection = `length = 2.6` @@ -4393,4 +4533,319 @@ extrude001 = extrude(profile001, length = 1) ) }) }) + + const multiProfileSweepsCode = `sketch001 = startSketchOn(XY) +profile001 = circle(sketch001, center = [3, 0], radius = 1) +profile002 = circle(sketch001, center = [6, 0], radius = 1) +path001 = startProfile(sketch001, at = [0, 0]) + |> yLine(length = 2) + ` + const profile001Point = { x: 470, y: 270 } + const profile002Point = { x: 670, y: 270 } + + test('Point-and-click multi-profile sweeps: extrude', async ({ + context, + page, + homePage, + scene, + editor, + toolbar, + cmdBar, + }) => { + await context.addInitScript((initialCode) => { + localStorage.setItem('persistCode', initialCode) + }, multiProfileSweepsCode) + await page.setBodyDimensions({ width: 1000, height: 500 }) + await homePage.goToModelingScene() + await scene.settled(cmdBar) + + await test.step('Select through scene', async () => { + // Unfortunately can't select thru code for multi profile yet + const [clickProfile001Point] = scene.makeMouseHelpers( + profile001Point.x, + profile001Point.y + ) + const [clickProfile002Point] = scene.makeMouseHelpers( + profile002Point.x, + profile002Point.y + ) + await toolbar.closePane('code') + await clickProfile001Point() + await page.keyboard.down('Shift') + await clickProfile002Point() + await page.waitForTimeout(500) + await page.keyboard.up('Shift') + }) + + await test.step('Go through command bar flow', async () => { + await toolbar.extrudeButton.click() + await cmdBar.expectState({ + stage: 'arguments', + currentArgKey: 'sketches', + currentArgValue: '', + headerArguments: { + Sketches: '', + Length: '', + }, + highlightedHeaderArg: 'sketches', + commandName: 'Extrude', + }) + await cmdBar.progressCmdBar() + await cmdBar.expectState({ + stage: 'arguments', + currentArgKey: 'length', + currentArgValue: '5', + headerArguments: { + Sketches: '2 faces', + Length: '', + }, + highlightedHeaderArg: 'length', + commandName: 'Extrude', + }) + await page.keyboard.insertText('1') + await cmdBar.progressCmdBar() + await cmdBar.expectState({ + stage: 'review', + headerArguments: { + Sketches: '2 faces', + Length: '1', + }, + commandName: 'Extrude', + }) + await cmdBar.progressCmdBar() + await scene.settled(cmdBar) + + await toolbar.openPane('code') + await editor.expectEditor.toContain( + `extrude001 = extrude([profile001, profile002], length = 1)`, + { shouldNormalise: true } + ) + await editor.closePane() + }) + + await test.step('Delete extrude via feature tree selection', async () => { + const op = await toolbar.getFeatureTreeOperation('Extrude', 0) + await op.click({ button: 'right' }) + await page.getByTestId('context-menu-delete').click() + await scene.settled(cmdBar) + await toolbar.closePane('feature-tree') + await toolbar.openPane('code') + await editor.expectEditor.not.toContain( + `extrude001 = extrude([profile001, profile002], length = 1)`, + { shouldNormalise: true } + ) + }) + }) + + test('Point-and-click multi-profile sweeps: sweep', async ({ + context, + page, + homePage, + scene, + editor, + toolbar, + cmdBar, + }) => { + await context.addInitScript((initialCode) => { + localStorage.setItem('persistCode', initialCode) + }, multiProfileSweepsCode) + await page.setBodyDimensions({ width: 1000, height: 500 }) + await homePage.goToModelingScene() + await scene.settled(cmdBar) + + await test.step('Select through scene', async () => { + // Unfortunately can't select thru code for multi profile yet + const [clickProfile001Point] = scene.makeMouseHelpers( + profile001Point.x, + profile001Point.y + ) + const [clickProfile002Point] = scene.makeMouseHelpers( + profile002Point.x, + profile002Point.y + ) + await toolbar.closePane('code') + await clickProfile001Point() + await page.keyboard.down('Shift') + await clickProfile002Point() + await page.waitForTimeout(500) + await page.keyboard.up('Shift') + }) + + await test.step('Go through command bar flow', async () => { + await toolbar.sweepButton.click() + await cmdBar.expectState({ + stage: 'arguments', + currentArgKey: 'sketches', + currentArgValue: '', + headerArguments: { + Sketches: '', + Path: '', + Sectional: '', + }, + highlightedHeaderArg: 'sketches', + commandName: 'Sweep', + }) + await cmdBar.progressCmdBar() + await cmdBar.expectState({ + stage: 'arguments', + currentArgKey: 'path', + currentArgValue: '', + headerArguments: { + Sketches: '2 faces', + Path: '', + Sectional: '', + }, + highlightedHeaderArg: 'path', + commandName: 'Sweep', + }) + await toolbar.openPane('code') + await page.getByText('yLine(length = 2)').click() + await cmdBar.progressCmdBar() + await cmdBar.expectState({ + stage: 'review', + headerArguments: { + Sketches: '2 faces', + Path: '1 segment', + Sectional: '', + }, + commandName: 'Sweep', + }) + await cmdBar.progressCmdBar() + await scene.settled(cmdBar) + + await toolbar.openPane('code') + await editor.expectEditor.toContain( + `sweep001 = sweep([profile001, profile002], path = path001)`, + { shouldNormalise: true } + ) + }) + + await test.step('Delete sweep via feature tree selection', async () => { + await editor.closePane() + const op = await toolbar.getFeatureTreeOperation('Sweep', 0) + await op.click({ button: 'right' }) + await page.getByTestId('context-menu-delete').click() + await scene.settled(cmdBar) + await editor.expectEditor.not.toContain( + `sweep001 = sweep([profile001, profile002], path = path001)`, + { shouldNormalise: true } + ) + }) + }) + + test('Point-and-click multi-profile sweeps: revolve', async ({ + context, + page, + homePage, + scene, + editor, + toolbar, + cmdBar, + }) => { + await context.addInitScript((initialCode) => { + localStorage.setItem('persistCode', initialCode) + }, multiProfileSweepsCode) + await page.setBodyDimensions({ width: 1000, height: 500 }) + await homePage.goToModelingScene() + await scene.settled(cmdBar) + + await test.step('Select through scene', async () => { + // Unfortunately can't select thru code for multi profile yet + const [clickProfile001Point] = scene.makeMouseHelpers( + profile001Point.x, + profile001Point.y + ) + const [clickProfile002Point] = scene.makeMouseHelpers( + profile002Point.x, + profile002Point.y + ) + await toolbar.closePane('code') + await clickProfile001Point() + await page.keyboard.down('Shift') + await clickProfile002Point() + await page.waitForTimeout(500) + await page.keyboard.up('Shift') + }) + + await test.step('Go through command bar flow', async () => { + await toolbar.closePane('code') + await toolbar.revolveButton.click() + await cmdBar.expectState({ + stage: 'arguments', + currentArgKey: 'sketches', + currentArgValue: '', + headerArguments: { + Sketches: '', + AxisOrEdge: '', + Angle: '', + }, + highlightedHeaderArg: 'sketches', + commandName: 'Revolve', + }) + await cmdBar.progressCmdBar() + await cmdBar.expectState({ + stage: 'arguments', + currentArgKey: 'axisOrEdge', + currentArgValue: '', + headerArguments: { + Sketches: '2 faces', + AxisOrEdge: '', + Angle: '', + }, + highlightedHeaderArg: 'axisOrEdge', + commandName: 'Revolve', + }) + await cmdBar.selectOption({ name: 'Edge' }).click() + await toolbar.openPane('code') + await page.getByText('yLine(length = 2)').click() + await cmdBar.progressCmdBar() + await cmdBar.expectState({ + stage: 'arguments', + currentArgKey: 'angle', + currentArgValue: '360', + headerArguments: { + Sketches: '2 faces', + AxisOrEdge: 'Edge', + Edge: '1 segment', + Angle: '', + }, + highlightedHeaderArg: 'angle', + commandName: 'Revolve', + }) + await page.keyboard.insertText('180') + await cmdBar.progressCmdBar() + await cmdBar.expectState({ + stage: 'review', + headerArguments: { + Sketches: '2 faces', + AxisOrEdge: 'Edge', + Edge: '1 segment', + Angle: '180', + }, + commandName: 'Revolve', + }) + await cmdBar.progressCmdBar() + await scene.settled(cmdBar) + + await editor.expectEditor.toContain(`yLine(length = 2, tag = $seg01)`, { + shouldNormalise: true, + }) + await editor.expectEditor.toContain( + `revolve001 = revolve([profile001, profile002], angle=180, axis=seg01)`, + { shouldNormalise: true } + ) + }) + + await test.step('Delete revolve via feature tree selection', async () => { + await editor.closePane() + const op = await toolbar.getFeatureTreeOperation('Revolve', 0) + await op.click({ button: 'right' }) + await page.getByTestId('context-menu-delete').click() + await scene.settled(cmdBar) + await editor.expectEditor.not.toContain( + `revolve001 = revolve([profile001, profile002], axis = XY, angle = 180)`, + { shouldNormalise: true } + ) + }) + }) }) diff --git a/e2e/playwright/sketch-tests.spec.ts b/e2e/playwright/sketch-tests.spec.ts index c256b0f56..391887aab 100644 --- a/e2e/playwright/sketch-tests.spec.ts +++ b/e2e/playwright/sketch-tests.spec.ts @@ -962,6 +962,8 @@ profile001 = startProfile(sketch001, at = [${roundOff(scale * 69.6)}, ${roundOff test('exiting a close extrude, has the extrude button enabled ready to go', async ({ page, homePage, + cmdBar, + toolbar, }) => { // this was a regression https://github.com/KittyCAD/modeling-app/issues/2832 await page.addInitScript(async () => { @@ -1002,19 +1004,21 @@ profile001 = startProfile(sketch001, at = [${roundOff(scale * 69.6)}, ${roundOff await page.getByRole('button', { name: 'Exit Sketch' }).click() // expect extrude button to be enabled - await expect( - page.getByRole('button', { name: 'Extrude' }) - ).not.toBeDisabled() + await expect(toolbar.extrudeButton).not.toBeDisabled() // click extrude - await page.getByRole('button', { name: 'Extrude' }).click() + await toolbar.extrudeButton.click() - // sketch selection should already have been made. "Selection: 1 face" only show up when the selection has been made already + // sketch selection should already have been made. "Sketches: 1 face" only show up when the selection has been made already // otherwise the cmdbar would be waiting for a selection. - await expect( - page.getByRole('button', { name: 'selection : 1 segment', exact: false }) - ).toBeVisible({ - timeout: 10_000, + await cmdBar.progressCmdBar() + await cmdBar.expectState({ + stage: 'arguments', + currentArgKey: 'length', + currentArgValue: '5', + headerArguments: { Sketches: '1 segment', Length: '' }, + highlightedHeaderArg: 'length', + commandName: 'Extrude', }) }) test("Existing sketch with bad code delete user's code", async ({ diff --git a/e2e/playwright/various.spec.ts b/e2e/playwright/various.spec.ts index 2b75774bc..d3c819d10 100644 --- a/e2e/playwright/various.spec.ts +++ b/e2e/playwright/various.spec.ts @@ -573,6 +573,7 @@ profile001 = startProfile(sketch002, at = [-12.34, 12.34]) await expect(page.getByTestId('command-bar')).toBeVisible() await page.waitForTimeout(100) + await cmdBar.progressCmdBar() await cmdBar.progressCmdBar() await expect(page.getByText('Confirm Extrude')).toBeVisible() await cmdBar.progressCmdBar() diff --git a/package-lock.json b/package-lock.json index b2cb9144b..20e075f06 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2492,7 +2492,6 @@ }, "node_modules/@clack/prompts/node_modules/is-unicode-supported": { "version": "1.3.0", - "extraneous": true, "inBundle": true, "license": "MIT", "engines": { diff --git a/src/lang/modifyAst.ts b/src/lang/modifyAst.ts index 1e68e4b88..ea25b1a39 100644 --- a/src/lang/modifyAst.ts +++ b/src/lang/modifyAst.ts @@ -31,8 +31,6 @@ import { UNLABELED_ARG, } from '@src/lang/queryAstConstants' import { getNodePathFromSourceRange } from '@src/lang/queryAstNodePathUtils' -import type { Artifact } from '@src/lang/std/artifactGraph' -import { getPathsFromArtifact } from '@src/lang/std/artifactGraph' import { addTagForSketchOnFace, getConstraintInfoKw, @@ -46,7 +44,6 @@ import { import type { SimplifiedArgDetails } from '@src/lang/std/stdTypes' import type { ArrayExpression, - ArtifactGraph, CallExpressionKw, Expr, Literal, @@ -340,122 +337,6 @@ export function mutateObjExpProp( return false } -export function extrudeSketch({ - node, - pathToNode, - distance = createLiteral(4), - extrudeName, - artifact, - artifactGraph, -}: { - node: Node - pathToNode: PathToNode - distance: Expr - extrudeName?: string - artifactGraph: ArtifactGraph - artifact?: Artifact -}): - | { - modifiedAst: Node - pathToNode: PathToNode - pathToExtrudeArg: PathToNode - } - | Error { - const orderedSketchNodePaths = getPathsFromArtifact({ - artifact: artifact, - sketchPathToNode: pathToNode, - artifactGraph, - ast: node, - }) - if (err(orderedSketchNodePaths)) return orderedSketchNodePaths - const _node = structuredClone(node) - const _node1 = getNodeFromPath(_node, pathToNode) - if (err(_node1)) return _node1 - - // determine if sketchExpression is in a pipeExpression or not - const _node2 = getNodeFromPath( - _node, - pathToNode, - 'PipeExpression' - ) - if (err(_node2)) return _node2 - - const _node3 = getNodeFromPath( - _node, - pathToNode, - 'VariableDeclarator' - ) - if (err(_node3)) return _node3 - const { node: variableDeclarator } = _node3 - - const extrudeCall = createCallExpressionStdLibKw( - 'extrude', - createLocalName(variableDeclarator.id.name), - [createLabeledArg('length', distance)] - ) - // index of the 'length' arg above. If you reorder the labeled args above, - // make sure to update this too. - const argIndex = 0 - - // We're not creating a pipe expression, - // but rather a separate constant for the extrusion - const name = - extrudeName ?? findUniqueName(node, KCL_DEFAULT_CONSTANT_PREFIXES.EXTRUDE) - const VariableDeclaration = createVariableDeclaration(name, extrudeCall) - - const lastSketchNodePath = - orderedSketchNodePaths[orderedSketchNodePaths.length - 1] - - const sketchIndexInBody = Number(lastSketchNodePath[1][0]) - _node.body.splice(sketchIndexInBody + 1, 0, VariableDeclaration) - - const pathToExtrudeArg: PathToNode = [ - ['body', ''], - [sketchIndexInBody + 1, 'index'], - ['declaration', 'VariableDeclaration'], - ['init', 'VariableDeclarator'], - ['arguments', 'CallExpressionKw'], - [argIndex, ARG_INDEX_FIELD], - ['arg', LABELED_ARG_FIELD], - ] - return { - modifiedAst: _node, - pathToNode: [...pathToNode.slice(0, -1), [-1, 'index']], - pathToExtrudeArg, - } -} - -export function loftSketches( - node: Node, - declarators: VariableDeclarator[] -): { - modifiedAst: Node - pathToNode: PathToNode -} { - const modifiedAst = structuredClone(node) - const name = findUniqueName(node, KCL_DEFAULT_CONSTANT_PREFIXES.LOFT) - const elements = declarators.map((d) => createLocalName(d.id.name)) - const loft = createCallExpressionStdLibKw( - 'loft', - createArrayExpression(elements), - [] - ) - const declaration = createVariableDeclaration(name, loft) - modifiedAst.body.push(declaration) - const pathToNode: PathToNode = [ - ['body', ''], - [modifiedAst.body.length - 1, 'index'], - ['declaration', 'VariableDeclaration'], - ['init', 'VariableDeclarator'], - ['unlabeled', UNLABELED_ARG], - ] - - return { - modifiedAst, - pathToNode, - } -} - export function addShell({ node, sweepName, @@ -514,63 +395,6 @@ export function addShell({ } } -export function addSweep({ - node, - targetDeclarator, - trajectoryDeclarator, - sectional, - variableName, - insertIndex, -}: { - node: Node - targetDeclarator: VariableDeclarator - trajectoryDeclarator: VariableDeclarator - sectional: boolean - variableName?: string - insertIndex?: number -}): { - modifiedAst: Node - pathToNode: PathToNode -} { - const modifiedAst = structuredClone(node) - const name = - variableName ?? findUniqueName(node, KCL_DEFAULT_CONSTANT_PREFIXES.SWEEP) - const call = createCallExpressionStdLibKw( - 'sweep', - createLocalName(targetDeclarator.id.name), - [ - createLabeledArg('path', createLocalName(trajectoryDeclarator.id.name)), - createLabeledArg('sectional', createLiteral(sectional)), - ] - ) - const variable = createVariableDeclaration(name, call) - const insertAt = - insertIndex !== undefined - ? insertIndex - : modifiedAst.body.length - ? modifiedAst.body.length - : 0 - - modifiedAst.body.length - ? modifiedAst.body.splice(insertAt, 0, variable) - : modifiedAst.body.push(variable) - const argIndex = 0 - const pathToNode: PathToNode = [ - ['body', ''], - [insertAt, 'index'], - ['declaration', 'VariableDeclaration'], - ['init', 'VariableDeclarator'], - ['arguments', 'CallExpressionKw'], - [argIndex, ARG_INDEX_FIELD], - ['arg', LABELED_ARG_FIELD], - ] - - return { - modifiedAst, - pathToNode, - } -} - export function sketchOnExtrudedFace( node: Node, sketchPathToNode: PathToNode, @@ -1345,7 +1169,7 @@ export function createNodeFromExprSnippet( export function insertVariableAndOffsetPathToNode( variable: KclCommandValue, modifiedAst: Node, - pathToNode: PathToNode + pathToNode?: PathToNode ) { if ('variableName' in variable && variable.variableName) { modifiedAst.body.splice( @@ -1353,7 +1177,7 @@ export function insertVariableAndOffsetPathToNode( 0, variable.variableDeclarationAst ) - if (typeof pathToNode[1][0] === 'number') { + if (pathToNode && pathToNode[1] && typeof pathToNode[1][0] === 'number') { pathToNode[1][0]++ } } diff --git a/src/lang/modifyAst/addRevolve.ts b/src/lang/modifyAst/addRevolve.ts deleted file mode 100644 index 4044920af..000000000 --- a/src/lang/modifyAst/addRevolve.ts +++ /dev/null @@ -1,142 +0,0 @@ -import type { Node } from '@rust/kcl-lib/bindings/Node' - -import { - createCallExpressionStdLibKw, - createLabeledArg, - createLocalName, - createVariableDeclaration, - findUniqueName, -} from '@src/lang/create' -import { - getEdgeTagCall, - mutateAstWithTagForSketchSegment, -} from '@src/lang/modifyAst/addEdgeTreatment' -import { getNodeFromPath } from '@src/lang/queryAst' -import { getSafeInsertIndex } from '@src/lang/queryAst/getSafeInsertIndex' -import { ARG_INDEX_FIELD, LABELED_ARG_FIELD } from '@src/lang/queryAstConstants' -import { getNodePathFromSourceRange } from '@src/lang/queryAstNodePathUtils' -import type { - Expr, - PathToNode, - Program, - VariableDeclarator, -} from '@src/lang/wasm' -import { KCL_DEFAULT_CONSTANT_PREFIXES } from '@src/lib/constants' -import type { Selections } from '@src/lib/selections' -import { err } from '@src/lib/trap' - -export function getAxisExpressionAndIndex( - axisOrEdge: 'Axis' | 'Edge', - axis: string | undefined, - edge: Selections | undefined, - ast: Node -) { - let generatedAxis - let axisDeclaration: PathToNode | null = null - let axisIndexIfAxis: number | undefined = undefined - - if (axisOrEdge === 'Edge' && edge) { - const pathToAxisSelection = getNodePathFromSourceRange( - ast, - edge.graphSelections[0]?.codeRef.range - ) - const tagResult = mutateAstWithTagForSketchSegment(ast, pathToAxisSelection) - - // Have the tag whether it is already created or a new one is generated - if (err(tagResult)) return tagResult - const { tag } = tagResult - const axisSelection = edge?.graphSelections[0]?.artifact - if (!axisSelection) return new Error('Generated axis selection is missing.') - generatedAxis = getEdgeTagCall(tag, axisSelection) - if ( - axisSelection.type === 'segment' || - axisSelection.type === 'path' || - axisSelection.type === 'edgeCut' - ) { - axisDeclaration = axisSelection.codeRef.pathToNode - if (!axisDeclaration) - return new Error('Expected to fine axis declaration') - const axisIndexInPathToNode = - axisDeclaration.findIndex((a) => a[0] === 'body') + 1 - const value = axisDeclaration[axisIndexInPathToNode][0] - if (typeof value !== 'number') - return new Error('expected axis index value to be a number') - axisIndexIfAxis = value - } - } else if (axisOrEdge === 'Axis' && axis) { - generatedAxis = createLocalName(axis) - } - - return { - generatedAxis, - axisIndexIfAxis, - } -} - -export function revolveSketch( - ast: Node, - pathToSketchNode: PathToNode, - angle: Expr, - axisOrEdge: 'Axis' | 'Edge', - axis: string | undefined, - edge: Selections | undefined, - variableName?: string, - insertIndex?: number -): - | { - modifiedAst: Node - pathToSketchNode: PathToNode - pathToRevolveArg: PathToNode - } - | Error { - const clonedAst = structuredClone(ast) - const sketchVariableDeclaratorNode = getNodeFromPath( - clonedAst, - pathToSketchNode, - 'VariableDeclarator' - ) - if (err(sketchVariableDeclaratorNode)) return sketchVariableDeclaratorNode - const { node: sketchVariableDeclarator } = sketchVariableDeclaratorNode - - const getAxisResult = getAxisExpressionAndIndex( - axisOrEdge, - axis, - edge, - clonedAst - ) - if (err(getAxisResult)) return getAxisResult - const { generatedAxis } = getAxisResult - if (!generatedAxis) return new Error('Generated axis selection is missing.') - - const revolveCall = createCallExpressionStdLibKw( - 'revolve', - createLocalName(sketchVariableDeclarator.id.name), - [createLabeledArg('angle', angle), createLabeledArg('axis', generatedAxis)] - ) - - // We're not creating a pipe expression, - // but rather a separate constant for the extrusion - const name = - variableName ?? - findUniqueName(clonedAst, KCL_DEFAULT_CONSTANT_PREFIXES.REVOLVE) - const variableDeclaration = createVariableDeclaration(name, revolveCall) - const bodyInsertIndex = - insertIndex ?? getSafeInsertIndex(revolveCall, clonedAst) - clonedAst.body.splice(bodyInsertIndex, 0, variableDeclaration) - const argIndex = 0 - const pathToRevolveArg: PathToNode = [ - ['body', ''], - [bodyInsertIndex, 'index'], - ['declaration', 'VariableDeclaration'], - ['init', 'VariableDeclarator'], - ['arguments', 'CallExpressionKw'], - [argIndex, ARG_INDEX_FIELD], - ['arg', LABELED_ARG_FIELD], - ] - - return { - modifiedAst: clonedAst, - pathToSketchNode: [...pathToSketchNode.slice(0, -1), [-1, 'index']], - pathToRevolveArg, - } -} diff --git a/src/lang/modifyAst/addSweep.ts b/src/lang/modifyAst/addSweep.ts new file mode 100644 index 000000000..9c19ac549 --- /dev/null +++ b/src/lang/modifyAst/addSweep.ts @@ -0,0 +1,406 @@ +import type { Node } from '@rust/kcl-lib/bindings/Node' + +import { + createArrayExpression, + createCallExpressionStdLibKw, + createLabeledArg, + createLiteral, + createLocalName, + createVariableDeclaration, + findUniqueName, +} from '@src/lang/create' +import { insertVariableAndOffsetPathToNode } from '@src/lang/modifyAst' +import { + getEdgeTagCall, + mutateAstWithTagForSketchSegment, +} from '@src/lang/modifyAst/addEdgeTreatment' +import { + getNodeFromPath, + getSketchExprsFromSelection, + valueOrVariable, +} from '@src/lang/queryAst' +import { ARG_INDEX_FIELD, LABELED_ARG_FIELD } from '@src/lang/queryAstConstants' +import { getNodePathFromSourceRange } from '@src/lang/queryAstNodePathUtils' +import type { + CallExpressionKw, + Expr, + PathToNode, + Program, + VariableDeclaration, +} from '@src/lang/wasm' +import type { KclCommandValue } from '@src/lib/commandTypes' +import { KCL_DEFAULT_CONSTANT_PREFIXES } from '@src/lib/constants' +import type { Selections } from '@src/lib/selections' +import { err } from '@src/lib/trap' + +export function addExtrude({ + ast, + sketches, + length, + nodeToEdit, +}: { + ast: Node + sketches: Selections + length: KclCommandValue + nodeToEdit?: PathToNode +}): + | { + modifiedAst: Node + pathToNode: PathToNode + } + | Error { + // 1. Clone the ast so we can edit it + const modifiedAst = structuredClone(ast) + + // 2. Prepare unlabeled and labeled arguments + // Map the sketches selection into a list of kcl expressions to be passed as unlabelled argument + const sketchesExprList = getSketchExprsFromSelection( + sketches, + modifiedAst, + nodeToEdit + ) + if (err(sketchesExprList)) { + return sketchesExprList + } + + const sketchesExpr = createSketchExpression(sketchesExprList) + const call = createCallExpressionStdLibKw('extrude', sketchesExpr, [ + createLabeledArg('length', valueOrVariable(length)), + ]) + + // Insert variables for labeled arguments if provided + if ('variableName' in length && length.variableName) { + insertVariableAndOffsetPathToNode(length, modifiedAst, nodeToEdit) + } + + // 3. If edit, we assign the new function call declaration to the existing node, + // otherwise just push to the end + let pathToNode: PathToNode | undefined + if (nodeToEdit) { + const result = getNodeFromPath( + modifiedAst, + nodeToEdit, + 'CallExpressionKw' + ) + if (err(result)) { + return result + } + + Object.assign(result.node, call) + pathToNode = nodeToEdit + } else { + const name = findUniqueName( + modifiedAst, + KCL_DEFAULT_CONSTANT_PREFIXES.EXTRUDE + ) + const declaration = createVariableDeclaration(name, call) + modifiedAst.body.push(declaration) + pathToNode = createPathToNode(modifiedAst) + } + + return { + modifiedAst, + pathToNode, + } +} + +export function addSweep({ + ast, + sketches, + path, + sectional, + nodeToEdit, +}: { + ast: Node + sketches: Selections + path: Selections + sectional?: boolean + nodeToEdit?: PathToNode +}): + | { + modifiedAst: Node + pathToNode: PathToNode + } + | Error { + // 1. Clone the ast so we can edit it + const modifiedAst = structuredClone(ast) + + // 2. Prepare unlabeled and labeled arguments + // Map the sketches selection into a list of kcl expressions to be passed as unlabelled argument + const sketchesExprList = getSketchExprsFromSelection( + sketches, + modifiedAst, + nodeToEdit + ) + if (err(sketchesExprList)) { + return sketchesExprList + } + + // Extra roundabout to find the trajectory (or path) declaration for the labeled argument + const pathNodePath = getNodePathFromSourceRange( + ast, + path.graphSelections[0].codeRef.range + ) + const pathDeclaration = getNodeFromPath( + ast, + pathNodePath, + 'VariableDeclaration' + ) + if (err(pathDeclaration)) { + return pathDeclaration + } + + // Extra labeled args expressions + const pathExpr = createLocalName(pathDeclaration.node.declaration.id.name) + const sectionalExpr = sectional + ? [createLabeledArg('sectional', createLiteral(sectional))] + : [] + + const sketchesExpr = createSketchExpression(sketchesExprList) + const call = createCallExpressionStdLibKw('sweep', sketchesExpr, [ + createLabeledArg('path', pathExpr), + ...sectionalExpr, + ]) + + // 3. If edit, we assign the new function call declaration to the existing node, + // otherwise just push to the end + let pathToNode: PathToNode | undefined + if (nodeToEdit) { + const result = getNodeFromPath( + modifiedAst, + nodeToEdit, + 'CallExpressionKw' + ) + if (err(result)) { + return result + } + + Object.assign(result.node, call) + pathToNode = nodeToEdit + } else { + const name = findUniqueName( + modifiedAst, + KCL_DEFAULT_CONSTANT_PREFIXES.SWEEP + ) + const declaration = createVariableDeclaration(name, call) + modifiedAst.body.push(declaration) + pathToNode = createPathToNode(modifiedAst) + } + + return { + modifiedAst, + pathToNode, + } +} + +export function addLoft({ + ast, + sketches, +}: { + ast: Node + sketches: Selections +}): + | { + modifiedAst: Node + pathToNode: PathToNode + } + | Error { + // 1. Clone the ast so we can edit it + const modifiedAst = structuredClone(ast) + + // 2. Prepare unlabeled and labeled arguments + // Map the sketches selection into a list of kcl expressions to be passed as unlabelled argument + const sketchesExprList = getSketchExprsFromSelection(sketches, modifiedAst) + if (err(sketchesExprList)) { + return sketchesExprList + } + + const sketchesExpr = createSketchExpression(sketchesExprList) + const call = createCallExpressionStdLibKw('loft', sketchesExpr, []) + + // 3. Just push the declaration to the end + // Note that Loft doesn't support edit flows yet since it's selection only atm + const name = findUniqueName(modifiedAst, KCL_DEFAULT_CONSTANT_PREFIXES.LOFT) + const declaration = createVariableDeclaration(name, call) + modifiedAst.body.push(declaration) + const toFirstKwarg = false + const pathToNode = createPathToNode(modifiedAst, toFirstKwarg) + + return { + modifiedAst, + pathToNode, + } +} + +export function addRevolve({ + ast, + sketches, + angle, + axisOrEdge, + axis, + edge, + nodeToEdit, +}: { + ast: Node + sketches: Selections + angle: KclCommandValue + axisOrEdge: 'Axis' | 'Edge' + axis: string | undefined + edge: Selections | undefined + nodeToEdit?: PathToNode +}): + | { + modifiedAst: Node + pathToNode: PathToNode + } + | Error { + // 1. Clone the ast so we can edit it + const modifiedAst = structuredClone(ast) + + // 2. Prepare unlabeled and labeled arguments + // Map the sketches selection into a list of kcl expressions to be passed as unlabelled argument + const sketchesExprList = getSketchExprsFromSelection( + sketches, + modifiedAst, + nodeToEdit + ) + if (err(sketchesExprList)) { + return sketchesExprList + } + + // Retrieve axis expression depending on mode + const getAxisResult = getAxisExpressionAndIndex( + axisOrEdge, + axis, + edge, + modifiedAst + ) + if (err(getAxisResult) || !getAxisResult.generatedAxis) { + return new Error('Generated axis selection is missing.') + } + + const sketchesExpr = createSketchExpression(sketchesExprList) + const call = createCallExpressionStdLibKw('revolve', sketchesExpr, [ + createLabeledArg('angle', valueOrVariable(angle)), + createLabeledArg('axis', getAxisResult.generatedAxis), + ]) + + // Insert variables for labeled arguments if provided + if ('variableName' in angle && angle.variableName) { + insertVariableAndOffsetPathToNode(angle, modifiedAst, nodeToEdit) + } + + // 3. If edit, we assign the new function call declaration to the existing node, + // otherwise just push to the end + let pathToNode: PathToNode | undefined + if (nodeToEdit) { + const result = getNodeFromPath( + modifiedAst, + nodeToEdit, + 'CallExpressionKw' + ) + if (err(result)) { + return result + } + + Object.assign(result.node, call) + pathToNode = nodeToEdit + } else { + const name = findUniqueName( + modifiedAst, + KCL_DEFAULT_CONSTANT_PREFIXES.REVOLVE + ) + const declaration = createVariableDeclaration(name, call) + modifiedAst.body.push(declaration) + pathToNode = createPathToNode(modifiedAst) + } + + return { + modifiedAst, + pathToNode, + } +} + +// Utilities + +function createSketchExpression(sketches: Expr[]) { + let sketchesExpr: Expr | null = null + if (sketches.every((s) => s.type === 'PipeSubstitution')) { + // Keeping null so we don't even put it the % sign + } else if (sketches.length === 1) { + sketchesExpr = sketches[0] + } else { + sketchesExpr = createArrayExpression(sketches) + } + return sketchesExpr +} + +function createPathToNode( + modifiedAst: Node, + toFirstKwarg = true +): PathToNode { + const argIndex = 0 // first kwarg for all sweeps here + const pathToCall: PathToNode = [ + ['body', ''], + [modifiedAst.body.length - 1, 'index'], + ['declaration', 'VariableDeclaration'], + ['init', 'VariableDeclarator'], + ] + if (toFirstKwarg) { + pathToCall.push( + ['arguments', 'CallExpressionKw'], + [argIndex, ARG_INDEX_FIELD], + ['arg', LABELED_ARG_FIELD] + ) + } + + return pathToCall +} + +export function getAxisExpressionAndIndex( + axisOrEdge: 'Axis' | 'Edge', + axis: string | undefined, + edge: Selections | undefined, + ast: Node +) { + let generatedAxis + let axisDeclaration: PathToNode | null = null + let axisIndexIfAxis: number | undefined = undefined + + if (axisOrEdge === 'Edge' && edge) { + const pathToAxisSelection = getNodePathFromSourceRange( + ast, + edge.graphSelections[0]?.codeRef.range + ) + const tagResult = mutateAstWithTagForSketchSegment(ast, pathToAxisSelection) + + // Have the tag whether it is already created or a new one is generated + if (err(tagResult)) return tagResult + const { tag } = tagResult + const axisSelection = edge?.graphSelections[0]?.artifact + if (!axisSelection) return new Error('Generated axis selection is missing.') + generatedAxis = getEdgeTagCall(tag, axisSelection) + if ( + axisSelection.type === 'segment' || + axisSelection.type === 'path' || + axisSelection.type === 'edgeCut' + ) { + axisDeclaration = axisSelection.codeRef.pathToNode + if (!axisDeclaration) + return new Error('Expected to find axis declaration') + const axisIndexInPathToNode = + axisDeclaration.findIndex((a) => a[0] === 'body') + 1 + const value = axisDeclaration[axisIndexInPathToNode][0] + if (typeof value !== 'number') + return new Error('expected axis index value to be a number') + axisIndexIfAxis = value + } + } else if (axisOrEdge === 'Axis' && axis) { + generatedAxis = createLocalName(axis) + } + + return { + generatedAxis, + axisIndexIfAxis, + } +} diff --git a/src/lang/queryAst.ts b/src/lang/queryAst.ts index e4175767b..48eb4e5e0 100644 --- a/src/lang/queryAst.ts +++ b/src/lang/queryAst.ts @@ -2,12 +2,14 @@ import type { FunctionExpression } from '@rust/kcl-lib/bindings/FunctionExpressi import type { ImportStatement } from '@rust/kcl-lib/bindings/ImportStatement' import type { Node } from '@rust/kcl-lib/bindings/Node' import type { TypeDeclaration } from '@rust/kcl-lib/bindings/TypeDeclaration' - -import { createLocalName } from '@src/lang/create' +import { createLocalName, createPipeSubstitution } from '@src/lang/create' import type { ToolTip } from '@src/lang/langHelpers' import { splitPathAtLastIndex } from '@src/lang/modifyAst' import { getNodePathFromSourceRange } from '@src/lang/queryAstNodePathUtils' -import { codeRefFromRange } from '@src/lang/std/artifactGraph' +import { + codeRefFromRange, + getArtifactOfTypes, +} from '@src/lang/std/artifactGraph' import { getArgForEnd } from '@src/lang/std/sketch' import { getSketchSegmentFromSourceRange } from '@src/lang/std/sketchConstraints' import { @@ -53,6 +55,7 @@ import { getAngle, isArray } from '@src/lib/utils' import { ARG_INDEX_FIELD, LABELED_ARG_FIELD } from '@src/lang/queryAstConstants' import type { KclCommandValue } from '@src/lib/commandTypes' import type { UnaryExpression } from 'typescript' +import type { Operation, OpKclValue } from '@rust/kcl-lib/bindings/Operation' /** * Retrieves a node from a given path within a Program node structure, optionally stopping at a specified node type. @@ -1053,6 +1056,125 @@ export const valueOrVariable = (variable: KclCommandValue) => { : variable.valueAst } +// Go from a selection of sketches to a list of KCL expressions that +// can be used to create KCL sweep call declarations. +export function getSketchExprsFromSelection( + selection: Selections, + ast: Node, + nodeToEdit?: PathToNode +): Error | Expr[] { + const sketches: Expr[] = selection.graphSelections.flatMap((s) => { + const path = getNodePathFromSourceRange(ast, s?.codeRef.range) + const sketchVariable = getNodeFromPath( + ast, + path, + 'VariableDeclarator' + ) + if (err(sketchVariable)) { + return [] + } + + if (sketchVariable.node.id) { + const name = sketchVariable.node?.id.name + if (nodeToEdit) { + const result = getNodeFromPath( + ast, + nodeToEdit, + 'VariableDeclarator' + ) + if ( + !err(result) && + result.node.type === 'VariableDeclarator' && + name === result.node.id.name + ) { + // Pointing to same variable case + return createPipeSubstitution() + } + } + // Pointing to different variable case + return createLocalName(name) + } else { + // No variable case + return createPipeSubstitution() + } + }) + + if (sketches.length === 0) { + return new Error("Couldn't map selections to program references") + } + + return sketches +} + +// Go from the sketches argument in a KCL sweep call declaration +// to a list of graph selections, useful for edit flows. +// Somewhat of an inverse of getSketchExprsFromSelection. +export function getSketchSelectionsFromOperation( + operation: Operation, + artifactGraph: ArtifactGraph +): Error | Selections { + const error = new Error("Couldn't retrieve sketches from operation") + if (operation.type !== 'StdLibCall' && operation.type !== 'KclStdLibCall') { + return error + } + + let sketches: OpKclValue[] = [] + if (operation.unlabeledArg?.value.type === 'Sketch') { + sketches = [operation.unlabeledArg.value] + } else if (operation.unlabeledArg?.value.type === 'Array') { + sketches = operation.unlabeledArg.value.value + } else { + return error + } + + const graphSelections: Selection[] = sketches.flatMap((sketch) => { + // We have to go a little roundabout to get from the original artifact + // to the solid2DId that we need to pass to the Extrude command. + if (sketch.type !== 'Sketch') { + return [] + } + + const pathArtifact = getArtifactOfTypes( + { + key: sketch.value.artifactId, + types: ['path'], + }, + artifactGraph + ) + if ( + err(pathArtifact) || + pathArtifact.type !== 'path' || + !pathArtifact.solid2dId + ) { + return [] + } + + const solid2DArtifact = getArtifactOfTypes( + { + key: pathArtifact.solid2dId, + types: ['solid2d'], + }, + artifactGraph + ) + if (err(solid2DArtifact) || solid2DArtifact.type !== 'solid2d') { + return [] + } + + return { + artifact: solid2DArtifact, + codeRef: pathArtifact.codeRef, + } + }) + if (graphSelections.length === 0) { + return error + } + + return { + graphSelections, + otherSelections: [], + } +} + export function findImportNodeAndAlias( ast: Node, pathToNode: PathToNode diff --git a/src/lib/commandBarConfigs/modelingCommandConfig.ts b/src/lib/commandBarConfigs/modelingCommandConfig.ts index f708ec4cc..55f730e9b 100644 --- a/src/lib/commandBarConfigs/modelingCommandConfig.ts +++ b/src/lib/commandBarConfigs/modelingCommandConfig.ts @@ -64,27 +64,20 @@ export type ModelingCommandSchema = { Extrude: { // Enables editing workflow nodeToEdit?: PathToNode - selection: Selections // & { type: 'face' } would be cool to lock that down - // result: (typeof EXTRUSION_RESULTS)[number] - distance: KclCommandValue + // KCL stdlib arguments + sketches: Selections + length: KclCommandValue } Sweep: { // Enables editing workflow nodeToEdit?: PathToNode - // Arguments - target: Selections - trajectory: Selections - sectional: boolean + // KCL stdlib arguments + sketches: Selections + path: Selections + sectional?: boolean } Loft: { - selection: Selections - } - Shell: { - // Enables editing workflow - nodeToEdit?: PathToNode - // KCL stdlib arguments - selection: Selections - thickness: KclCommandValue + sketches: Selections } Revolve: { // Enables editing workflow @@ -92,11 +85,18 @@ export type ModelingCommandSchema = { // Flow arg axisOrEdge: 'Axis' | 'Edge' // KCL stdlib arguments - selection: Selections + sketches: Selections angle: KclCommandValue axis: string | undefined edge: Selections | undefined } + Shell: { + // Enables editing workflow + nodeToEdit?: PathToNode + // KCL stdlib arguments + selection: Selections + thickness: KclCommandValue + } Fillet: { // Enables editing workflow nodeToEdit?: PathToNode @@ -388,26 +388,14 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig< required: false, hidden: true, }, - selection: { + sketches: { inputType: 'selection', selectionTypes: ['solid2d', 'segment'], - multiple: false, // TODO: multiple selection + multiple: true, required: true, - skip: true, hidden: (context) => Boolean(context.argumentsToSubmit.nodeToEdit), }, - // result: { - // inputType: 'options', - // defaultValue: 'add', - // skip: true, - // required: true, - // options: EXTRUSION_RESULTS.map((r) => ({ - // name: r, - // isCurrent: r === 'add', - // value: r, - // })), - // }, - distance: { + length: { inputType: 'kcl', defaultValue: KCL_DEFAULT_LENGTH, required: true, @@ -426,26 +414,28 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig< skip: true, inputType: 'text', required: false, + hidden: true, }, - target: { + sketches: { inputType: 'selection', - selectionTypes: ['solid2d'], + selectionTypes: ['solid2d', 'segment'], + multiple: true, required: true, - skip: true, - multiple: false, hidden: (context) => Boolean(context.argumentsToSubmit.nodeToEdit), }, - trajectory: { + path: { inputType: 'selection', selectionTypes: ['segment'], required: true, - skip: true, multiple: false, validation: sweepValidator, hidden: (context) => Boolean(context.argumentsToSubmit.nodeToEdit), }, sectional: { inputType: 'options', + skip: true, + defaultValue: false, + hidden: false, required: true, options: [ { name: 'False', value: false }, @@ -458,45 +448,17 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig< Loft: { description: 'Create a 3D body by blending between two or more sketches', icon: 'loft', - needsReview: false, + needsReview: true, args: { - selection: { + sketches: { inputType: 'selection', selectionTypes: ['solid2d'], multiple: true, required: true, - skip: false, validation: loftValidator, }, }, }, - Shell: { - description: 'Hollow out a 3D solid.', - icon: 'shell', - needsReview: true, - args: { - nodeToEdit: { - description: - 'Path to the node in the AST to edit. Never shown to the user.', - skip: true, - inputType: 'text', - required: false, - }, - selection: { - inputType: 'selection', - selectionTypes: ['cap', 'wall'], - multiple: true, - required: true, - validation: shellValidator, - hidden: (context) => Boolean(context.argumentsToSubmit.nodeToEdit), - }, - thickness: { - inputType: 'kcl', - defaultValue: KCL_DEFAULT_LENGTH, - required: true, - }, - }, - }, Revolve: { description: 'Create a 3D body by rotating a sketch region about an axis.', icon: 'revolve', @@ -509,12 +471,11 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig< inputType: 'text', required: false, }, - selection: { + sketches: { inputType: 'selection', selectionTypes: ['solid2d', 'segment'], - multiple: false, // TODO: multiple selection + multiple: true, required: true, - skip: true, hidden: (context) => Boolean(context.argumentsToSubmit.nodeToEdit), }, axisOrEdge: { @@ -557,6 +518,33 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig< }, }, }, + Shell: { + description: 'Hollow out a 3D solid.', + icon: 'shell', + needsReview: true, + args: { + nodeToEdit: { + description: + 'Path to the node in the AST to edit. Never shown to the user.', + skip: true, + inputType: 'text', + required: false, + }, + selection: { + inputType: 'selection', + selectionTypes: ['cap', 'wall'], + multiple: true, + required: true, + validation: shellValidator, + hidden: (context) => Boolean(context.argumentsToSubmit.nodeToEdit), + }, + thickness: { + inputType: 'kcl', + defaultValue: KCL_DEFAULT_LENGTH, + required: true, + }, + }, + }, 'Boolean Subtract': { description: 'Subtract one solid from another.', icon: 'booleanSubtract', diff --git a/src/lib/commandBarConfigs/validators.ts b/src/lib/commandBarConfigs/validators.ts index 244e58a4c..f73175b7f 100644 --- a/src/lib/commandBarConfigs/validators.ts +++ b/src/lib/commandBarConfigs/validators.ts @@ -94,11 +94,12 @@ export const revolveAxisValidator = async ({ data: { [key: string]: Selections } context: CommandBarContext }): Promise => { - if (!isSelections(context.argumentsToSubmit.selection)) { + if (!isSelections(context.argumentsToSubmit.sketches)) { return 'Unable to revolve, selections are missing' } + // Gotcha: this validation only works for the first sketch const artifact = - context.argumentsToSubmit.selection.graphSelections[0].artifact + context.argumentsToSubmit.sketches.graphSelections[0].artifact if (!artifact) { return 'Unable to revolve, sketch not found' @@ -155,16 +156,16 @@ export const loftValidator = async ({ data: { [key: string]: Selections } context: CommandBarContext }): Promise => { - if (!isSelections(data.selection)) { + if (!isSelections(data.sketches)) { return 'Unable to loft, selections are missing' } - const { selection } = data + const { sketches } = data - if (selection.graphSelections.some((s) => s.artifact?.type !== 'solid2d')) { + if (sketches.graphSelections.some((s) => s.artifact?.type !== 'solid2d')) { return 'Unable to loft, some selection are not solid2ds' } - const sectionIds = data.selection.graphSelections.flatMap((s) => + const sectionIds = sketches.graphSelections.flatMap((s) => s.artifact?.type === 'solid2d' ? s.artifact.pathId : [] ) @@ -258,15 +259,15 @@ export const sweepValidator = async ({ data, }: { context: CommandBarContext - data: { trajectory: Selections } + data: { path: Selections } }): Promise => { - if (!isSelections(data.trajectory)) { + if (!isSelections(data.path)) { console.log('Unable to sweep, selections are missing') return 'Unable to sweep, selections are missing' } // Retrieve the parent path from the segment selection directly - const trajectoryArtifact = data.trajectory.graphSelections[0].artifact + const trajectoryArtifact = data.path.graphSelections[0].artifact if (!trajectoryArtifact) { return "Unable to sweep, couldn't find the trajectory artifact" } @@ -276,7 +277,7 @@ export const sweepValidator = async ({ const trajectory = trajectoryArtifact.pathId // Get the former arg in the command bar flow, and retrieve the path from the solid2d directly - const targetArg = context.argumentsToSubmit['target'] as Selections + const targetArg = context.argumentsToSubmit['sketches'] as Selections const targetArtifact = targetArg.graphSelections[0].artifact if (!targetArtifact) { return "Unable to sweep, couldn't find the profile artifact" diff --git a/src/lib/operations.ts b/src/lib/operations.ts index 77a2e1e2f..9646ee9fc 100644 --- a/src/lib/operations.ts +++ b/src/lib/operations.ts @@ -1,7 +1,11 @@ import type { OpKclValue, Operation } from '@rust/kcl-lib/bindings/Operation' import type { CustomIconName } from '@src/components/CustomIcon' -import { getNodeFromPath, findPipesWithImportAlias } from '@src/lang/queryAst' +import { + getNodeFromPath, + findPipesWithImportAlias, + getSketchSelectionsFromOperation, +} from '@src/lang/queryAst' import { getNodePathFromSourceRange } from '@src/lang/queryAstNodePathUtils' import type { Artifact } from '@src/lang/std/artifactGraph' import { @@ -57,81 +61,51 @@ interface StdLibCallInfo { * Gather up the argument values for the Extrude command * to be used in the command bar edit flow. */ -const prepareToEditExtrude: PrepareToEditCallback = - async function prepareToEditExtrude({ operation, artifact }) { - const baseCommand = { - name: 'Extrude', - groupId: 'modeling', - } - if ( - !artifact || - !('pathId' in artifact) || - (operation.type !== 'StdLibCall' && operation.type !== 'KclStdLibCall') - ) { - return baseCommand - } - - // We have to go a little roundabout to get from the original artifact - // to the solid2DId that we need to pass to the Extrude command. - const pathArtifact = getArtifactOfTypes( - { - key: artifact.pathId, - types: ['path'], - }, - kclManager.artifactGraph - ) - if ( - err(pathArtifact) || - pathArtifact.type !== 'path' || - !pathArtifact.solid2dId - ) - return baseCommand - const solid2DArtifact = getArtifactOfTypes( - { - key: pathArtifact.solid2dId, - types: ['solid2d'], - }, - kclManager.artifactGraph - ) - if (err(solid2DArtifact) || solid2DArtifact.type !== 'solid2d') { - return baseCommand - } - - // Convert the length argument from a string to a KCL expression - const distanceResult = await stringToKclExpression( - codeManager.code.slice( - operation.labeledArgs?.['length']?.sourceRange[0], - operation.labeledArgs?.['length']?.sourceRange[1] - ) - ) - if (err(distanceResult) || 'errors' in distanceResult) { - return baseCommand - } - - // Assemble the default argument values for the Extrude command, - // with `nodeToEdit` set, which will let the Extrude actor know - // to edit the node that corresponds to the StdLibCall. - const argDefaultValues: ModelingCommandSchema['Extrude'] = { - selection: { - graphSelections: [ - { - artifact: solid2DArtifact, - codeRef: pathArtifact.codeRef, - }, - ], - otherSelections: [], - }, - distance: distanceResult, - nodeToEdit: getNodePathFromSourceRange( - kclManager.ast, - sourceRangeFromRust(operation.sourceRange) - ), - } - return { - ...baseCommand, - argDefaultValues, - } +const prepareToEditExtrude: PrepareToEditCallback = async ({ operation }) => { + const baseCommand = { + name: 'Extrude', + groupId: 'modeling', } + if (operation.type !== 'StdLibCall' && operation.type !== 'KclStdLibCall') { + return { reason: 'Wrong operation type' } + } + + // 1. Map the unlabeled arguments to solid2d selections + const sketches = getSketchSelectionsFromOperation( + operation, + kclManager.artifactGraph + ) + if (err(sketches)) { + return { reason: "Couldn't retrieve sketches" } + } + + // 2. Convert the length argument from a string to a KCL expression + const length = await stringToKclExpression( + codeManager.code.slice( + operation.labeledArgs?.['length']?.sourceRange[0], + operation.labeledArgs?.['length']?.sourceRange[1] + ) + ) + if (err(length) || 'errors' in length) { + return { reason: "Couldn't retrieve length argument" } + } + + // 3. Assemble the default argument values for the command, + // with `nodeToEdit` set, which will let the actor know + // to edit the node that corresponds to the StdLibCall. + const argDefaultValues: ModelingCommandSchema['Extrude'] = { + sketches, + length, + nodeToEdit: getNodePathFromSourceRange( + kclManager.ast, + sourceRangeFromRust(operation.sourceRange) + ), + } + return { + ...baseCommand, + argDefaultValues, + } +} /** * Gather up the argument values for the Chamfer or Fillet command @@ -446,78 +420,32 @@ const prepareToEditOffsetPlane: PrepareToEditCallback = async ({ } } -const prepareToEditSweep: PrepareToEditCallback = async ({ - artifact, - operation, -}) => { +/** + * Gather up the argument values for the Revolve command + * to be used in the command bar edit flow. + */ +const prepareToEditSweep: PrepareToEditCallback = async ({ operation }) => { const baseCommand = { name: 'Sweep', groupId: 'modeling', } - if ( - (operation.type !== 'StdLibCall' && operation.type !== 'KclStdLibCall') || - !operation.labeledArgs || - !operation.unlabeledArg || - !('sectional' in operation.labeledArgs) || - !operation.labeledArgs.sectional - ) { - return baseCommand - } - if ( - !artifact || - !('pathId' in artifact) || - (operation.type !== 'StdLibCall' && operation.type !== 'KclStdLibCall') - ) { - return baseCommand + if (operation.type !== 'StdLibCall' && operation.type !== 'KclStdLibCall') { + return { reason: 'Wrong operation type' } } - // We have to go a little roundabout to get from the original artifact - // to the solid2DId that we need to pass to the Sweep command, just like Extrude. - const pathArtifact = getArtifactOfTypes( - { - key: artifact.pathId, - types: ['path'], - }, + // 1. Map the unlabeled arguments to solid2d selections + const sketches = getSketchSelectionsFromOperation( + operation, kclManager.artifactGraph ) - - if ( - err(pathArtifact) || - pathArtifact.type !== 'path' || - !pathArtifact.solid2dId - ) { - return baseCommand - } - - const targetArtifact = getArtifactOfTypes( - { - key: pathArtifact.solid2dId, - types: ['solid2d'], - }, - kclManager.artifactGraph - ) - - if (err(targetArtifact) || targetArtifact.type !== 'solid2d') { - return baseCommand - } - - const target = { - graphSelections: [ - { - artifact: targetArtifact, - codeRef: pathArtifact.codeRef, - }, - ], - otherSelections: [], + if (err(sketches)) { + return { reason: "Couldn't retrieve sketches" } } + // 2. Prepare labeled arguments // Same roundabout but twice for 'path' aka trajectory: sketch -> path -> segment - if (!('path' in operation.labeledArgs) || !operation.labeledArgs.path) { - return baseCommand - } - - if (operation.labeledArgs.path.value.type !== 'Sketch') { - return baseCommand + if (operation.labeledArgs.path?.value.type !== 'Sketch') { + return { reason: "Couldn't retrieve trajectory argument" } } const trajectoryPathArtifact = getArtifactOfTypes( @@ -529,7 +457,7 @@ const prepareToEditSweep: PrepareToEditCallback = async ({ ) if (err(trajectoryPathArtifact) || trajectoryPathArtifact.type !== 'path') { - return baseCommand + return { reason: "Couldn't retrieve trajectory path artifact" } } const trajectoryArtifact = getArtifactOfTypes( @@ -541,10 +469,11 @@ const prepareToEditSweep: PrepareToEditCallback = async ({ ) if (err(trajectoryArtifact) || trajectoryArtifact.type !== 'segment') { - return baseCommand + console.log(trajectoryArtifact) + return { reason: "Couldn't retrieve trajectory artifact" } } - const trajectory = { + const path = { graphSelections: [ { artifact: trajectoryArtifact, @@ -554,33 +483,28 @@ const prepareToEditSweep: PrepareToEditCallback = async ({ otherSelections: [], } - // sectional options boolean arg - if ( - !('sectional' in operation.labeledArgs) || - !operation.labeledArgs.sectional - ) { - return baseCommand + // sectional argument from a string to a KCL expression + let sectional: boolean | undefined + if ('sectional' in operation.labeledArgs && operation.labeledArgs.sectional) { + sectional = + codeManager.code.slice( + operation.labeledArgs.sectional.sourceRange[0], + operation.labeledArgs.sectional.sourceRange[1] + ) === 'true' } - const sectional = - codeManager.code.slice( - operation.labeledArgs.sectional.sourceRange[0], - operation.labeledArgs.sectional.sourceRange[1] - ) === 'true' - - // Assemble the default argument values for the Offset Plane command, - // with `nodeToEdit` set, which will let the Offset Plane actor know + // 3. Assemble the default argument values for the command, + // with `nodeToEdit` set, which will let the actor know // to edit the node that corresponds to the StdLibCall. const argDefaultValues: ModelingCommandSchema['Sweep'] = { - target: target, - trajectory, + sketches, + path, sectional, nodeToEdit: getNodePathFromSourceRange( kclManager.ast, sourceRangeFromRust(operation.sourceRange) ), } - return { ...baseCommand, argDefaultValues, @@ -841,6 +765,10 @@ const prepareToEditHelix: PrepareToEditCallback = async ({ operation }) => { } } +/** + * Gather up the argument values for the Revolve command + * to be used in the command bar edit flow. + */ const prepareToEditRevolve: PrepareToEditCallback = async ({ operation, artifact, @@ -851,51 +779,22 @@ const prepareToEditRevolve: PrepareToEditCallback = async ({ } if ( !artifact || - !('pathId' in artifact) || operation.type !== 'KclStdLibCall' || !operation.labeledArgs ) { return { reason: 'Wrong operation type or artifact' } } - // We have to go a little roundabout to get from the original artifact - // to the solid2DId that we need to pass to the command. - const pathArtifact = getArtifactOfTypes( - { - key: artifact.pathId, - types: ['path'], - }, + // 1. Map the unlabeled arguments to solid2d selections + const sketches = getSketchSelectionsFromOperation( + operation, kclManager.artifactGraph ) - if ( - err(pathArtifact) || - pathArtifact.type !== 'path' || - !pathArtifact.solid2dId - ) { - return { reason: "Couldn't find related path artifact" } - } - - const solid2DArtifact = getArtifactOfTypes( - { - key: pathArtifact.solid2dId, - types: ['solid2d'], - }, - kclManager.artifactGraph - ) - if (err(solid2DArtifact) || solid2DArtifact.type !== 'solid2d') { - return { reason: "Couldn't find related solid2d artifact" } - } - - const selection = { - graphSelections: [ - { - artifact: solid2DArtifact, - codeRef: pathArtifact.codeRef, - }, - ], - otherSelections: [], + if (err(sketches)) { + return { reason: "Couldn't retrieve sketches" } } + // 2. Prepare labeled arguments // axis options string arg if (!('axis' in operation.labeledArgs) || !operation.labeledArgs.axis) { return { reason: "Couldn't find axis argument" } @@ -988,14 +887,14 @@ const prepareToEditRevolve: PrepareToEditCallback = async ({ return { reason: 'Error in angle argument retrieval' } } - // Assemble the default argument values for the Offset Plane command, - // with `nodeToEdit` set, which will let the Offset Plane actor know + // 3. Assemble the default argument values for the command, + // with `nodeToEdit` set, which will let the actor know // to edit the node that corresponds to the StdLibCall. const argDefaultValues: ModelingCommandSchema['Revolve'] = { + sketches, axisOrEdge, axis, edge, - selection, angle, nodeToEdit: getNodePathFromSourceRange( kclManager.ast, diff --git a/src/machines/modelingMachine.ts b/src/machines/modelingMachine.ts index e6e2ec21e..a09067d87 100644 --- a/src/machines/modelingMachine.ts +++ b/src/machines/modelingMachine.ts @@ -49,11 +49,8 @@ import { addHelix, addOffsetPlane, addShell, - addSweep, - extrudeSketch, insertNamedConstant, insertVariableAndOffsetPathToNode, - loftSketches, } from '@src/lang/modifyAst' import type { ChamferParameters, @@ -66,9 +63,12 @@ import { mutateAstWithTagForSketchSegment, } from '@src/lang/modifyAst/addEdgeTreatment' import { + addExtrude, + addLoft, + addRevolve, + addSweep, getAxisExpressionAndIndex, - revolveSketch, -} from '@src/lang/modifyAst/addRevolve' +} from '@src/lang/modifyAst/addSweep' import { applyIntersectFromTargetOperatorSelections, applySubtractFromTargetOperatorSelections, @@ -94,7 +94,6 @@ import { updatePathToNodesAfterEdit, valueOrVariable, } from '@src/lang/queryAst' -import { getNodePathFromSourceRange } from '@src/lang/queryAstNodePathUtils' import { getFaceCodeRef, getPathsFromPlaneArtifact, @@ -133,7 +132,7 @@ import { } from '@src/lib/singletons' import type { ToolbarModeName } from '@src/lib/toolbar' import { err, reportRejection, trap } from '@src/lib/trap' -import { isArray, uuidv4 } from '@src/lib/utils' +import { uuidv4 } from '@src/lib/utils' import { deleteNodeInExtrudePipe } from '@src/lang/modifyAst/deleteNodeInExtrudePipe' import type { ImportStatement } from '@rust/kcl-lib/bindings/ImportStatement' @@ -1793,69 +1792,20 @@ export const modelingMachine = setup({ unknown, ModelingCommandSchema['Extrude'] | undefined >(async ({ input }) => { - if (!input) return new Error('No input provided') - const { selection, distance, nodeToEdit } = input - const isEditing = - nodeToEdit !== undefined && typeof nodeToEdit[1][0] === 'number' - let ast = structuredClone(kclManager.ast) - let extrudeName: string | undefined = undefined - - // If this is an edit flow, first we're going to remove the old extrusion - if (isEditing) { - // Extract the plane name from the node to edit - const extrudeNameNode = getNodeFromPath( - ast, - nodeToEdit, - 'VariableDeclaration' - ) - if (err(extrudeNameNode)) { - console.error('Error extracting plane name') - } else { - extrudeName = extrudeNameNode.node.declaration.id.name - } - - // Removing the old extrusion statement - const newBody = [...ast.body] - newBody.splice(nodeToEdit[1][0] as number, 1) - ast.body = newBody - } - - const pathToNode = getNodePathFromSourceRange( + if (!input) return Promise.reject(new Error('No input provided')) + const { nodeToEdit, sketches, length } = input + const { ast } = kclManager + const astResult = addExtrude({ ast, - selection.graphSelections[0]?.codeRef.range - ) - // Add an extrude statement to the AST - const extrudeSketchRes = extrudeSketch({ - node: ast, - pathToNode, - artifact: selection.graphSelections[0].artifact, - artifactGraph: kclManager.artifactGraph, - distance: - 'variableName' in distance - ? distance.variableIdentifierAst - : distance.valueAst, - extrudeName, + sketches, + length, + nodeToEdit, }) - if (err(extrudeSketchRes)) return extrudeSketchRes - const { modifiedAst, pathToExtrudeArg } = extrudeSketchRes - - // Insert the distance variable if the user has provided a variable name - if ( - 'variableName' in distance && - distance.variableName && - typeof pathToExtrudeArg[1][0] === 'number' - ) { - const insertIndex = Math.min( - pathToExtrudeArg[1][0], - distance.insertIndex - ) - const newBody = [...modifiedAst.body] - newBody.splice(insertIndex, 0, distance.variableDeclarationAst) - modifiedAst.body = newBody - // Since we inserted a new variable, we need to update the path to the extrude argument - pathToExtrudeArg[1][0]++ + if (err(astResult)) { + return Promise.reject(new Error("Couldn't add extrude statement")) } + const { modifiedAst, pathToNode } = astResult await updateModelingState( modifiedAst, EXECUTION_TYPE_REAL, @@ -1865,73 +1815,92 @@ export const modelingMachine = setup({ codeManager, }, { - focusPath: [pathToExtrudeArg], + focusPath: [pathToNode], } ) }), + sweepAstMod: fromPromise< + unknown, + ModelingCommandSchema['Sweep'] | undefined + >(async ({ input }) => { + if (!input) return Promise.reject(new Error('No input provided')) + const { nodeToEdit, sketches, path, sectional } = input + const { ast } = kclManager + const astResult = addSweep({ + ast, + sketches, + path, + sectional, + nodeToEdit, + }) + if (err(astResult)) { + return Promise.reject(astResult) + } + + const { modifiedAst, pathToNode } = astResult + await updateModelingState( + modifiedAst, + EXECUTION_TYPE_REAL, + { + kclManager, + editorManager, + codeManager, + }, + { + focusPath: [pathToNode], + } + ) + }), + loftAstMod: fromPromise( + async ({ + input, + }: { + input: ModelingCommandSchema['Loft'] | undefined + }) => { + if (!input) return Promise.reject(new Error('No input provided')) + const { sketches } = input + const { ast } = kclManager + const astResult = addLoft({ ast, sketches }) + if (err(astResult)) { + return Promise.reject(astResult) + } + + const { modifiedAst, pathToNode } = astResult + await updateModelingState( + modifiedAst, + EXECUTION_TYPE_REAL, + { + kclManager, + editorManager, + codeManager, + }, + { + focusPath: [pathToNode], + } + ) + } + ), revolveAstMod: fromPromise< unknown, ModelingCommandSchema['Revolve'] | undefined >(async ({ input }) => { - if (!input) return new Error('No input provided') - const { nodeToEdit, selection, angle, axis, edge, axisOrEdge } = input - let ast = kclManager.ast - let variableName: string | undefined = undefined - let insertIndex: number | undefined = undefined - - // If this is an edit flow, first we're going to remove the old extrusion - if (nodeToEdit && typeof nodeToEdit[1][0] === 'number') { - // Extract the plane name from the node to edit - const nameNode = getNodeFromPath( - ast, - nodeToEdit, - 'VariableDeclaration' - ) - if (err(nameNode)) { - console.error('Error extracting plane name') - } else { - variableName = nameNode.node.declaration.id.name - } - - // Removing the old extrusion statement - const newBody = [...ast.body] - newBody.splice(nodeToEdit[1][0], 1) - ast.body = newBody - insertIndex = nodeToEdit[1][0] - } - - if ( - 'variableName' in angle && - angle.variableName && - angle.insertIndex !== undefined - ) { - const newBody = [...ast.body] - newBody.splice(angle.insertIndex, 0, angle.variableDeclarationAst) - ast.body = newBody - if (insertIndex) { - // if editing need to offset that new var - insertIndex += 1 - } - } - - // This is the selection of the sketch that will be revolved - const pathToNode = getNodePathFromSourceRange( + if (!input) return Promise.reject(new Error('No input provided')) + const { nodeToEdit, sketches, angle, axis, edge, axisOrEdge } = input + const { ast } = kclManager + const astResult = addRevolve({ ast, - selection.graphSelections[0]?.codeRef.range - ) - - const revolveSketchRes = revolveSketch( - ast, - pathToNode, - 'variableName' in angle ? angle.variableIdentifierAst : angle.valueAst, + sketches, + angle, axisOrEdge, axis, edge, - variableName, - insertIndex - ) - if (trap(revolveSketchRes)) return - const { modifiedAst, pathToRevolveArg } = revolveSketchRes + nodeToEdit, + }) + if (err(astResult)) { + return Promise.reject(astResult) + } + + const { modifiedAst, pathToNode } = astResult await updateModelingState( modifiedAst, EXECUTION_TYPE_REAL, @@ -1941,7 +1910,7 @@ export const modelingMachine = setup({ codeManager, }, { - focusPath: [pathToRevolveArg], + focusPath: [pathToNode], } ) }), @@ -2169,141 +2138,6 @@ export const modelingMachine = setup({ ) } ), - sweepAstMod: fromPromise( - async ({ - input, - }: { - input: ModelingCommandSchema['Sweep'] | undefined - }) => { - if (!input) return new Error('No input provided') - // Extract inputs - const ast = kclManager.ast - const { target, trajectory, sectional, nodeToEdit } = input - let variableName: string | undefined = undefined - let insertIndex: number | undefined = undefined - - // If this is an edit flow, first we're going to remove the old one - if (nodeToEdit !== undefined && typeof nodeToEdit[1][0] === 'number') { - // Extract the plane name from the node to edit - const variableNode = getNodeFromPath( - ast, - nodeToEdit, - 'VariableDeclaration' - ) - - if (err(variableNode)) { - console.error('Error extracting name') - } else { - variableName = variableNode.node.declaration.id.name - } - - // Removing the old statement - const newBody = [...ast.body] - newBody.splice(nodeToEdit[1][0], 1) - ast.body = newBody - insertIndex = nodeToEdit[1][0] - } - - // Find the target declaration - const targetNodePath = getNodePathFromSourceRange( - ast, - target.graphSelections[0].codeRef.range - ) - // Gotchas, not sure why - // - it seems like in some cases we get a list on edit, especially the state that e2e hits - // - looking for a VariableDeclaration seems more robust than VariableDeclarator - const targetNode = getNodeFromPath< - VariableDeclaration | VariableDeclaration[] - >(ast, targetNodePath, 'VariableDeclaration') - if (err(targetNode)) { - return new Error("Couldn't parse profile selection") - } - - const targetDeclarator = isArray(targetNode.node) - ? targetNode.node[0].declaration - : targetNode.node.declaration - - // Find the trajectory (or path) declaration - const trajectoryNodePath = getNodePathFromSourceRange( - ast, - trajectory.graphSelections[0].codeRef.range - ) - // Also looking for VariableDeclaration for consistency here - const trajectoryNode = getNodeFromPath( - ast, - trajectoryNodePath, - 'VariableDeclaration' - ) - if (err(trajectoryNode)) { - return new Error("Couldn't parse path selection") - } - - const trajectoryDeclarator = trajectoryNode.node.declaration - - // Perform the sweep - const { modifiedAst, pathToNode } = addSweep({ - node: ast, - targetDeclarator, - trajectoryDeclarator, - sectional, - variableName, - insertIndex, - }) - await updateModelingState( - modifiedAst, - EXECUTION_TYPE_REAL, - { - kclManager, - editorManager, - codeManager, - }, - { - focusPath: [pathToNode], - } - ) - } - ), - loftAstMod: fromPromise( - async ({ - input, - }: { - input: ModelingCommandSchema['Loft'] | undefined - }) => { - if (!input) return new Error('No input provided') - // Extract inputs - const ast = kclManager.ast - const { selection } = input - const declarators = selection.graphSelections.flatMap((s) => { - const path = getNodePathFromSourceRange(ast, s?.codeRef.range) - const nodeFromPath = getNodeFromPath( - ast, - path, - 'VariableDeclarator' - ) - return err(nodeFromPath) ? [] : nodeFromPath.node - }) - - // TODO: add better validation on selection - if (!(declarators && declarators.length > 1)) { - trap('Not enough sketches selected') - } - - // Perform the loft - const loftSketchesRes = loftSketches(ast, declarators) - await updateModelingState( - loftSketchesRes.modifiedAst, - EXECUTION_TYPE_REAL, - { - kclManager, - editorManager, - codeManager, - }, - { - focusPath: [loftSketchesRes.pathToNode], - } - ) - } - ), shellAstMod: fromPromise( async ({ input,