Multi-profile sweeps and more robust edit flows in point-and-click (#6437)
This commit is contained in:
		@ -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)'
 | 
			
		||||
    )
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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(
 | 
			
		||||
 | 
			
		||||
@ -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',
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
@ -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 }
 | 
			
		||||
      )
 | 
			
		||||
    })
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
@ -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 ({
 | 
			
		||||
 | 
			
		||||
@ -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()
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										1
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							@ -2492,7 +2492,6 @@
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@clack/prompts/node_modules/is-unicode-supported": {
 | 
			
		||||
      "version": "1.3.0",
 | 
			
		||||
      "extraneous": true,
 | 
			
		||||
      "inBundle": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "engines": {
 | 
			
		||||
 | 
			
		||||
@ -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<Program>
 | 
			
		||||
  pathToNode: PathToNode
 | 
			
		||||
  distance: Expr
 | 
			
		||||
  extrudeName?: string
 | 
			
		||||
  artifactGraph: ArtifactGraph
 | 
			
		||||
  artifact?: Artifact
 | 
			
		||||
}):
 | 
			
		||||
  | {
 | 
			
		||||
      modifiedAst: Node<Program>
 | 
			
		||||
      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<PipeExpression>(
 | 
			
		||||
    _node,
 | 
			
		||||
    pathToNode,
 | 
			
		||||
    'PipeExpression'
 | 
			
		||||
  )
 | 
			
		||||
  if (err(_node2)) return _node2
 | 
			
		||||
 | 
			
		||||
  const _node3 = getNodeFromPath<VariableDeclarator>(
 | 
			
		||||
    _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<Program>,
 | 
			
		||||
  declarators: VariableDeclarator[]
 | 
			
		||||
): {
 | 
			
		||||
  modifiedAst: Node<Program>
 | 
			
		||||
  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<Program>
 | 
			
		||||
  targetDeclarator: VariableDeclarator
 | 
			
		||||
  trajectoryDeclarator: VariableDeclarator
 | 
			
		||||
  sectional: boolean
 | 
			
		||||
  variableName?: string
 | 
			
		||||
  insertIndex?: number
 | 
			
		||||
}): {
 | 
			
		||||
  modifiedAst: Node<Program>
 | 
			
		||||
  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<Program>,
 | 
			
		||||
  sketchPathToNode: PathToNode,
 | 
			
		||||
@ -1345,7 +1169,7 @@ export function createNodeFromExprSnippet(
 | 
			
		||||
export function insertVariableAndOffsetPathToNode(
 | 
			
		||||
  variable: KclCommandValue,
 | 
			
		||||
  modifiedAst: Node<Program>,
 | 
			
		||||
  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]++
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -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<Program>
 | 
			
		||||
) {
 | 
			
		||||
  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<Program>,
 | 
			
		||||
  pathToSketchNode: PathToNode,
 | 
			
		||||
  angle: Expr,
 | 
			
		||||
  axisOrEdge: 'Axis' | 'Edge',
 | 
			
		||||
  axis: string | undefined,
 | 
			
		||||
  edge: Selections | undefined,
 | 
			
		||||
  variableName?: string,
 | 
			
		||||
  insertIndex?: number
 | 
			
		||||
):
 | 
			
		||||
  | {
 | 
			
		||||
      modifiedAst: Node<Program>
 | 
			
		||||
      pathToSketchNode: PathToNode
 | 
			
		||||
      pathToRevolveArg: PathToNode
 | 
			
		||||
    }
 | 
			
		||||
  | Error {
 | 
			
		||||
  const clonedAst = structuredClone(ast)
 | 
			
		||||
  const sketchVariableDeclaratorNode = getNodeFromPath<VariableDeclarator>(
 | 
			
		||||
    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,
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										406
									
								
								src/lang/modifyAst/addSweep.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										406
									
								
								src/lang/modifyAst/addSweep.ts
									
									
									
									
									
										Normal file
									
								
							@ -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<Program>
 | 
			
		||||
  sketches: Selections
 | 
			
		||||
  length: KclCommandValue
 | 
			
		||||
  nodeToEdit?: PathToNode
 | 
			
		||||
}):
 | 
			
		||||
  | {
 | 
			
		||||
      modifiedAst: Node<Program>
 | 
			
		||||
      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<CallExpressionKw>(
 | 
			
		||||
      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<Program>
 | 
			
		||||
  sketches: Selections
 | 
			
		||||
  path: Selections
 | 
			
		||||
  sectional?: boolean
 | 
			
		||||
  nodeToEdit?: PathToNode
 | 
			
		||||
}):
 | 
			
		||||
  | {
 | 
			
		||||
      modifiedAst: Node<Program>
 | 
			
		||||
      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<VariableDeclaration>(
 | 
			
		||||
    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<CallExpressionKw>(
 | 
			
		||||
      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<Program>
 | 
			
		||||
  sketches: Selections
 | 
			
		||||
}):
 | 
			
		||||
  | {
 | 
			
		||||
      modifiedAst: Node<Program>
 | 
			
		||||
      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<Program>
 | 
			
		||||
  sketches: Selections
 | 
			
		||||
  angle: KclCommandValue
 | 
			
		||||
  axisOrEdge: 'Axis' | 'Edge'
 | 
			
		||||
  axis: string | undefined
 | 
			
		||||
  edge: Selections | undefined
 | 
			
		||||
  nodeToEdit?: PathToNode
 | 
			
		||||
}):
 | 
			
		||||
  | {
 | 
			
		||||
      modifiedAst: Node<Program>
 | 
			
		||||
      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<CallExpressionKw>(
 | 
			
		||||
      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<Program>,
 | 
			
		||||
  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<Program>
 | 
			
		||||
) {
 | 
			
		||||
  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,
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -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<Program>,
 | 
			
		||||
  nodeToEdit?: PathToNode
 | 
			
		||||
): Error | Expr[] {
 | 
			
		||||
  const sketches: Expr[] = selection.graphSelections.flatMap((s) => {
 | 
			
		||||
    const path = getNodePathFromSourceRange(ast, s?.codeRef.range)
 | 
			
		||||
    const sketchVariable = getNodeFromPath<VariableDeclarator>(
 | 
			
		||||
      ast,
 | 
			
		||||
      path,
 | 
			
		||||
      'VariableDeclarator'
 | 
			
		||||
    )
 | 
			
		||||
    if (err(sketchVariable)) {
 | 
			
		||||
      return []
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (sketchVariable.node.id) {
 | 
			
		||||
      const name = sketchVariable.node?.id.name
 | 
			
		||||
      if (nodeToEdit) {
 | 
			
		||||
        const result = getNodeFromPath<VariableDeclarator>(
 | 
			
		||||
          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<Program>,
 | 
			
		||||
  pathToNode: PathToNode
 | 
			
		||||
 | 
			
		||||
@ -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',
 | 
			
		||||
 | 
			
		||||
@ -94,11 +94,12 @@ export const revolveAxisValidator = async ({
 | 
			
		||||
  data: { [key: string]: Selections }
 | 
			
		||||
  context: CommandBarContext
 | 
			
		||||
}): Promise<boolean | string> => {
 | 
			
		||||
  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<boolean | string> => {
 | 
			
		||||
  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<boolean | string> => {
 | 
			
		||||
  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"
 | 
			
		||||
 | 
			
		||||
@ -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,
 | 
			
		||||
 | 
			
		||||
@ -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<VariableDeclaration>(
 | 
			
		||||
          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<VariableDeclaration>(
 | 
			
		||||
          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<VariableDeclaration>(
 | 
			
		||||
            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<VariableDeclaration>(
 | 
			
		||||
          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<VariableDeclarator>(
 | 
			
		||||
            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,
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user