@ -57,23 +57,26 @@ async function doBasicSketch(page: Page, openPanes: string[]) {
 | 
			
		||||
  const startXPx = 600
 | 
			
		||||
  await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
 | 
			
		||||
  if (openPanes.includes('code')) {
 | 
			
		||||
    await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ')
 | 
			
		||||
  |> startProfileAt(${commonPoints.startAt}, %)`)
 | 
			
		||||
    await expect(u.codeLocator).toContainText(
 | 
			
		||||
      `sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${commonPoints.startAt}, sketch001)`
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
  await page.waitForTimeout(500)
 | 
			
		||||
  await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
 | 
			
		||||
  await page.waitForTimeout(500)
 | 
			
		||||
 | 
			
		||||
  if (openPanes.includes('code')) {
 | 
			
		||||
    await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ')
 | 
			
		||||
  |> startProfileAt(${commonPoints.startAt}, %)
 | 
			
		||||
    await expect(u.codeLocator)
 | 
			
		||||
      .toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${commonPoints.startAt}, sketch001)
 | 
			
		||||
  |> xLine(${commonPoints.num1}, %)`)
 | 
			
		||||
  }
 | 
			
		||||
  await page.waitForTimeout(500)
 | 
			
		||||
  await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
 | 
			
		||||
  if (openPanes.includes('code')) {
 | 
			
		||||
    await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ')
 | 
			
		||||
  |> startProfileAt(${commonPoints.startAt}, %)
 | 
			
		||||
    await expect(u.codeLocator)
 | 
			
		||||
      .toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${
 | 
			
		||||
      commonPoints.startAt
 | 
			
		||||
    }, sketch001)
 | 
			
		||||
  |> xLine(${commonPoints.num1}, %)
 | 
			
		||||
  |> yLine(${commonPoints.num1 + 0.01}, %)`)
 | 
			
		||||
  } else {
 | 
			
		||||
@ -82,8 +85,10 @@ async function doBasicSketch(page: Page, openPanes: string[]) {
 | 
			
		||||
  await page.waitForTimeout(200)
 | 
			
		||||
  await page.mouse.click(startXPx, 500 - PUR * 20)
 | 
			
		||||
  if (openPanes.includes('code')) {
 | 
			
		||||
    await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ')
 | 
			
		||||
  |> startProfileAt(${commonPoints.startAt}, %)
 | 
			
		||||
    await expect(u.codeLocator)
 | 
			
		||||
      .toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${
 | 
			
		||||
      commonPoints.startAt
 | 
			
		||||
    }, sketch001)
 | 
			
		||||
  |> xLine(${commonPoints.num1}, %)
 | 
			
		||||
  |> yLine(${commonPoints.num1 + 0.01}, %)
 | 
			
		||||
  |> xLine(${commonPoints.num2 * -1}, %)`)
 | 
			
		||||
@ -140,8 +145,10 @@ async function doBasicSketch(page: Page, openPanes: string[]) {
 | 
			
		||||
 | 
			
		||||
  // Open the code pane.
 | 
			
		||||
  await u.openKclCodePanel()
 | 
			
		||||
  await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ')
 | 
			
		||||
  |> startProfileAt(${commonPoints.startAt}, %)
 | 
			
		||||
  await expect(u.codeLocator)
 | 
			
		||||
    .toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${
 | 
			
		||||
    commonPoints.startAt
 | 
			
		||||
  }, sketch001)
 | 
			
		||||
  |> xLine(${commonPoints.num1}, %, $seg01)
 | 
			
		||||
  |> yLine(${commonPoints.num1 + 0.01}, %)
 | 
			
		||||
  |> xLine(-segLen(seg01), %)`)
 | 
			
		||||
 | 
			
		||||
@ -44,8 +44,7 @@ test.describe('Can create sketches on all planes and their back sides', () => {
 | 
			
		||||
      },
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const code = `sketch001 = startSketchOn('${plane}')
 | 
			
		||||
    |> startProfileAt([0.9, -1.22], %)`
 | 
			
		||||
    const code = `sketch001 = startSketchOn('${plane}')profile001 = startProfileAt([0.9, -1.22], sketch001)`
 | 
			
		||||
 | 
			
		||||
    await u.openDebugPanel()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -11,6 +11,7 @@ import {
 | 
			
		||||
 | 
			
		||||
type mouseParams = {
 | 
			
		||||
  pixelDiff?: number
 | 
			
		||||
  shouldDbClick?: boolean
 | 
			
		||||
}
 | 
			
		||||
type mouseDragToParams = mouseParams & {
 | 
			
		||||
  fromPoint: { x: number; y: number }
 | 
			
		||||
@ -75,11 +76,16 @@ export class SceneFixture {
 | 
			
		||||
        if (clickParams?.pixelDiff) {
 | 
			
		||||
          return doAndWaitForImageDiff(
 | 
			
		||||
            this.page,
 | 
			
		||||
            () => this.page.mouse.click(x, y),
 | 
			
		||||
            () =>
 | 
			
		||||
              clickParams?.shouldDbClick
 | 
			
		||||
                ? this.page.mouse.dblclick(x, y)
 | 
			
		||||
                : this.page.mouse.click(x, y),
 | 
			
		||||
            clickParams.pixelDiff
 | 
			
		||||
          )
 | 
			
		||||
        }
 | 
			
		||||
        return this.page.mouse.click(x, y)
 | 
			
		||||
        return clickParams?.shouldDbClick
 | 
			
		||||
          ? this.page.mouse.dblclick(x, y)
 | 
			
		||||
          : this.page.mouse.click(x, y)
 | 
			
		||||
      },
 | 
			
		||||
      (moveParams?: mouseParams) => {
 | 
			
		||||
        if (moveParams?.pixelDiff) {
 | 
			
		||||
@ -210,7 +216,7 @@ export class SceneFixture {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  expectPixelColor = async (
 | 
			
		||||
    colour: [number, number, number],
 | 
			
		||||
    colour: [number, number, number] | [number, number, number][],
 | 
			
		||||
    coords: { x: number; y: number },
 | 
			
		||||
    diff: number
 | 
			
		||||
  ) => {
 | 
			
		||||
@ -231,22 +237,36 @@ export class SceneFixture {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function isColourArray(
 | 
			
		||||
  colour: [number, number, number] | [number, number, number][]
 | 
			
		||||
): colour is [number, number, number][] {
 | 
			
		||||
  return Array.isArray(colour[0])
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function expectPixelColor(
 | 
			
		||||
  page: Page,
 | 
			
		||||
  colour: [number, number, number],
 | 
			
		||||
  colour: [number, number, number] | [number, number, number][],
 | 
			
		||||
  coords: { x: number; y: number },
 | 
			
		||||
  diff: number
 | 
			
		||||
) {
 | 
			
		||||
  let finalValue = colour
 | 
			
		||||
  await expect
 | 
			
		||||
    .poll(async () => {
 | 
			
		||||
      const pixel = (await getPixelRGBs(page)(coords, 1))[0]
 | 
			
		||||
      if (!pixel) return null
 | 
			
		||||
      finalValue = pixel
 | 
			
		||||
      return pixel.every(
 | 
			
		||||
        (channel, index) => Math.abs(channel - colour[index]) < diff
 | 
			
		||||
      )
 | 
			
		||||
    })
 | 
			
		||||
    .poll(
 | 
			
		||||
      async () => {
 | 
			
		||||
        const pixel = (await getPixelRGBs(page)(coords, 1))[0]
 | 
			
		||||
        if (!pixel) return null
 | 
			
		||||
        finalValue = pixel
 | 
			
		||||
        if (!isColourArray(colour)) {
 | 
			
		||||
          return pixel.every(
 | 
			
		||||
            (channel, index) => Math.abs(channel - colour[index]) < diff
 | 
			
		||||
          )
 | 
			
		||||
        }
 | 
			
		||||
        return colour.some((c) =>
 | 
			
		||||
          c.every((channel, index) => Math.abs(pixel[index] - channel) < diff)
 | 
			
		||||
        )
 | 
			
		||||
      },
 | 
			
		||||
      { timeout: 10_000 }
 | 
			
		||||
    )
 | 
			
		||||
    .toBeTruthy()
 | 
			
		||||
    .catch((cause) => {
 | 
			
		||||
      throw new Error(
 | 
			
		||||
 | 
			
		||||
@ -11,7 +11,10 @@ export class ToolbarFixture {
 | 
			
		||||
  offsetPlaneButton!: Locator
 | 
			
		||||
  startSketchBtn!: Locator
 | 
			
		||||
  lineBtn!: Locator
 | 
			
		||||
  tangentialArcBtn!: Locator
 | 
			
		||||
  circleBtn!: Locator
 | 
			
		||||
  rectangleBtn!: Locator
 | 
			
		||||
  lengthConstraintBtn!: Locator
 | 
			
		||||
  exitSketchBtn!: Locator
 | 
			
		||||
  editSketchBtn!: Locator
 | 
			
		||||
  fileTreeBtn!: Locator
 | 
			
		||||
@ -33,7 +36,10 @@ export class ToolbarFixture {
 | 
			
		||||
    this.offsetPlaneButton = page.getByTestId('plane-offset')
 | 
			
		||||
    this.startSketchBtn = page.getByTestId('sketch')
 | 
			
		||||
    this.lineBtn = page.getByTestId('line')
 | 
			
		||||
    this.tangentialArcBtn = page.getByTestId('tangential-arc')
 | 
			
		||||
    this.circleBtn = page.getByTestId('circle-center')
 | 
			
		||||
    this.rectangleBtn = page.getByTestId('corner-rectangle')
 | 
			
		||||
    this.lengthConstraintBtn = page.getByTestId('constraint-length')
 | 
			
		||||
    this.exitSketchBtn = page.getByTestId('sketch-exit')
 | 
			
		||||
    this.editSketchBtn = page.getByText('Edit Sketch')
 | 
			
		||||
    this.fileTreeBtn = page.locator('[id="files-button-holder"]')
 | 
			
		||||
@ -91,4 +97,13 @@ export class ToolbarFixture {
 | 
			
		||||
      await expect(this.exeIndicator).toBeVisible({ timeout: 15_000 })
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  selectCenterRectangle = async () => {
 | 
			
		||||
    await this.page
 | 
			
		||||
      .getByRole('button', { name: 'caret down Corner rectangle:' })
 | 
			
		||||
      .click()
 | 
			
		||||
    await expect(
 | 
			
		||||
      this.page.getByTestId('dropdown-center-rectangle')
 | 
			
		||||
    ).toBeVisible()
 | 
			
		||||
    await this.page.getByTestId('dropdown-center-rectangle').click()
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -425,7 +425,7 @@ test.describe('Onboarding tests', () => {
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
test.fixme(
 | 
			
		||||
test(
 | 
			
		||||
  'Restarting onboarding on desktop takes one attempt',
 | 
			
		||||
  { tag: '@electron' },
 | 
			
		||||
  async ({ browser: _ }, testInfo) => {
 | 
			
		||||
 | 
			
		||||
@ -135,7 +135,9 @@ test.describe('verify sketch on chamfer works', () => {
 | 
			
		||||
          pixelDiff: 50,
 | 
			
		||||
        })
 | 
			
		||||
        await rectangle2ndClick()
 | 
			
		||||
        await editor.expectEditor.toContain(afterRectangle2ndClickSnippet)
 | 
			
		||||
        await editor.expectEditor.toContain(afterRectangle2ndClickSnippet, {
 | 
			
		||||
          shouldNormalise: true,
 | 
			
		||||
        })
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      await test.step('Clean up so that `_sketchOnAChamfer` util can be called again', async () => {
 | 
			
		||||
@ -177,18 +179,13 @@ test.describe('verify sketch on chamfer works', () => {
 | 
			
		||||
 | 
			
		||||
        afterChamferSelectSnippet:
 | 
			
		||||
          'sketch002 = startSketchOn(extrude001, seg03)',
 | 
			
		||||
        afterRectangle1stClickSnippet: 'startProfileAt([205.96, 254.59], %)',
 | 
			
		||||
        afterRectangle2ndClickSnippet: `angledLine([0, 11.39], %, $rectangleSegmentA002)
 | 
			
		||||
    |> angledLine([
 | 
			
		||||
         segAng(rectangleSegmentA002) - 90,
 | 
			
		||||
         105.26
 | 
			
		||||
       ], %, $rectangleSegmentB001)
 | 
			
		||||
    |> angledLine([
 | 
			
		||||
         segAng(rectangleSegmentA002),
 | 
			
		||||
         -segLen(rectangleSegmentA002)
 | 
			
		||||
       ], %, $rectangleSegmentC001)
 | 
			
		||||
    |> lineTo([profileStartX(%), profileStartY(%)], %)
 | 
			
		||||
    |> close(%)`,
 | 
			
		||||
        afterRectangle1stClickSnippet:
 | 
			
		||||
          'startProfileAt([205.96, 254.59], sketch002)',
 | 
			
		||||
        afterRectangle2ndClickSnippet: `angledLine([0,11.39],%,$rectangleSegmentA002)
 | 
			
		||||
        |>angledLine([segAng(rectangleSegmentA002)-90,105.26],%)
 | 
			
		||||
        |>angledLine([segAng(rectangleSegmentA002),-segLen(rectangleSegmentA002)],%)
 | 
			
		||||
        |>lineTo([profileStartX(%),profileStartY(%)],%)
 | 
			
		||||
        |>close(%)`,
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      await sketchOnAChamfer({
 | 
			
		||||
@ -209,19 +206,15 @@ test.describe('verify sketch on chamfer works', () => {
 | 
			
		||||
 | 
			
		||||
        afterChamferSelectSnippet:
 | 
			
		||||
          'sketch003 = startSketchOn(extrude001, seg04)',
 | 
			
		||||
        afterRectangle1stClickSnippet: 'startProfileAt([-209.64, 255.28], %)',
 | 
			
		||||
        afterRectangle2ndClickSnippet: `angledLine([0, 11.56], %, $rectangleSegmentA003)
 | 
			
		||||
    |> angledLine([
 | 
			
		||||
         segAng(rectangleSegmentA003) - 90,
 | 
			
		||||
         106.84
 | 
			
		||||
       ], %, $rectangleSegmentB002)
 | 
			
		||||
    |> angledLine([
 | 
			
		||||
         segAng(rectangleSegmentA003),
 | 
			
		||||
         -segLen(rectangleSegmentA003)
 | 
			
		||||
       ], %, $rectangleSegmentC002)
 | 
			
		||||
    |> lineTo([profileStartX(%), profileStartY(%)], %)
 | 
			
		||||
    |> close(%)`,
 | 
			
		||||
        afterRectangle1stClickSnippet:
 | 
			
		||||
          'startProfileAt([-209.64, 255.28], sketch003)',
 | 
			
		||||
        afterRectangle2ndClickSnippet: `angledLine([0,11.56],%,$rectangleSegmentA003)
 | 
			
		||||
        |>angledLine([segAng(rectangleSegmentA003)-90,106.84],%)
 | 
			
		||||
        |>angledLine([segAng(rectangleSegmentA003),-segLen(rectangleSegmentA003)],%)
 | 
			
		||||
        |>lineTo([profileStartX(%),profileStartY(%)],%)
 | 
			
		||||
        |>close(%)`,
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      await sketchOnAChamfer({
 | 
			
		||||
        clickCoords: { x: 677, y: 87 },
 | 
			
		||||
        cameraPos: { x: -6200, y: 1500, z: 6200 },
 | 
			
		||||
@ -234,19 +227,14 @@ test.describe('verify sketch on chamfer works', () => {
 | 
			
		||||
         ]
 | 
			
		||||
       }, %)`,
 | 
			
		||||
        afterChamferSelectSnippet:
 | 
			
		||||
          'sketch003 = startSketchOn(extrude001, seg04)',
 | 
			
		||||
        afterRectangle1stClickSnippet: 'startProfileAt([-209.64, 255.28], %)',
 | 
			
		||||
        afterRectangle2ndClickSnippet: `angledLine([0, 11.56], %, $rectangleSegmentA003)
 | 
			
		||||
    |> angledLine([
 | 
			
		||||
         segAng(rectangleSegmentA003) - 90,
 | 
			
		||||
         106.84
 | 
			
		||||
       ], %, $rectangleSegmentB002)
 | 
			
		||||
    |> angledLine([
 | 
			
		||||
         segAng(rectangleSegmentA003),
 | 
			
		||||
         -segLen(rectangleSegmentA003)
 | 
			
		||||
       ], %, $rectangleSegmentC002)
 | 
			
		||||
    |> lineTo([profileStartX(%), profileStartY(%)], %)
 | 
			
		||||
    |> close(%)`,
 | 
			
		||||
          'sketch004 = startSketchOn(extrude001, seg05)',
 | 
			
		||||
        afterRectangle1stClickSnippet:
 | 
			
		||||
          'startProfileAt([82.57, 322.96], sketch004)',
 | 
			
		||||
        afterRectangle2ndClickSnippet: `angledLine([0,11.16],%,$rectangleSegmentA004)
 | 
			
		||||
        |>angledLine([segAng(rectangleSegmentA004)-90,103.07],%)
 | 
			
		||||
        |>angledLine([segAng(rectangleSegmentA004),-segLen(rectangleSegmentA004)],%)
 | 
			
		||||
        |>lineTo([profileStartX(%),profileStartY(%)],%)|
 | 
			
		||||
        >close(%)`,
 | 
			
		||||
      })
 | 
			
		||||
      /// last one
 | 
			
		||||
      await sketchOnAChamfer({
 | 
			
		||||
@ -259,104 +247,97 @@ test.describe('verify sketch on chamfer works', () => {
 | 
			
		||||
       }, %)`,
 | 
			
		||||
        afterChamferSelectSnippet:
 | 
			
		||||
          'sketch005 = startSketchOn(extrude001, seg06)',
 | 
			
		||||
        afterRectangle1stClickSnippet: 'startProfileAt([-23.43, 19.69], %)',
 | 
			
		||||
        afterRectangle2ndClickSnippet: `angledLine([0, 9.1], %, $rectangleSegmentA005)
 | 
			
		||||
 | 
			
		||||
    |> angledLine([
 | 
			
		||||
         segAng(rectangleSegmentA005) - 90,
 | 
			
		||||
         84.07
 | 
			
		||||
       ], %, $rectangleSegmentB004)
 | 
			
		||||
    |> angledLine([
 | 
			
		||||
         segAng(rectangleSegmentA005),
 | 
			
		||||
         -segLen(rectangleSegmentA005)
 | 
			
		||||
       ], %, $rectangleSegmentC004)
 | 
			
		||||
    |> lineTo([profileStartX(%), profileStartY(%)], %)
 | 
			
		||||
    |> close(%)`,
 | 
			
		||||
        afterRectangle1stClickSnippet:
 | 
			
		||||
          'startProfileAt([-23.43, 19.69], sketch005)',
 | 
			
		||||
        afterRectangle2ndClickSnippet: `angledLine([0,9.1],%,$rectangleSegmentA005)
 | 
			
		||||
        |>angledLine([segAng(rectangleSegmentA005)-90,84.07],%)
 | 
			
		||||
        |>angledLine([segAng(rectangleSegmentA005),-segLen(rectangleSegmentA005)],%)
 | 
			
		||||
        |>lineTo([profileStartX(%),profileStartY(%)],%)
 | 
			
		||||
        |>close(%)`,
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      await test.step('verify at the end of the test that final code is what is expected', async () => {
 | 
			
		||||
        await editor.expectEditor.toContain(
 | 
			
		||||
          `sketch001 = startSketchOn('XZ')
 | 
			
		||||
 | 
			
		||||
      |> startProfileAt([75.8, 317.2], %) // [$startCapTag, $EndCapTag]
 | 
			
		||||
      |> angledLine([0, 268.43], %, $rectangleSegmentA001)
 | 
			
		||||
      |> angledLine([
 | 
			
		||||
           segAng(rectangleSegmentA001) - 90,
 | 
			
		||||
           217.26
 | 
			
		||||
         ], %, $seg01)
 | 
			
		||||
      |> angledLine([
 | 
			
		||||
           segAng(rectangleSegmentA001),
 | 
			
		||||
           -segLen(rectangleSegmentA001)
 | 
			
		||||
         ], %, $yo)
 | 
			
		||||
      |> lineTo([profileStartX(%), profileStartY(%)], %, $seg02)
 | 
			
		||||
      |> close(%)
 | 
			
		||||
    extrude001 = extrude(100, sketch001)
 | 
			
		||||
      |> chamfer({
 | 
			
		||||
           length = 30,
 | 
			
		||||
           tags = [getOppositeEdge(seg01)]
 | 
			
		||||
         }, %, $seg03)
 | 
			
		||||
      |> chamfer({ length = 30, tags = [seg01] }, %, $seg04)
 | 
			
		||||
      |> chamfer({
 | 
			
		||||
           length = 30,
 | 
			
		||||
           tags = [getNextAdjacentEdge(seg02)]
 | 
			
		||||
         }, %, $seg05)
 | 
			
		||||
      |> chamfer({
 | 
			
		||||
           length = 30,
 | 
			
		||||
           tags = [getNextAdjacentEdge(yo)]
 | 
			
		||||
         }, %, $seg06)
 | 
			
		||||
    sketch005 = startSketchOn(extrude001, seg06)
 | 
			
		||||
      |> startProfileAt([-23.43, 19.69], %)
 | 
			
		||||
      |> angledLine([0, 9.1], %, $rectangleSegmentA005)
 | 
			
		||||
      |> angledLine([
 | 
			
		||||
           segAng(rectangleSegmentA005) - 90,
 | 
			
		||||
           84.07
 | 
			
		||||
         ], %, $rectangleSegmentB004)
 | 
			
		||||
      |> angledLine([
 | 
			
		||||
           segAng(rectangleSegmentA005),
 | 
			
		||||
           -segLen(rectangleSegmentA005)
 | 
			
		||||
         ], %, $rectangleSegmentC004)
 | 
			
		||||
      |> lineTo([profileStartX(%), profileStartY(%)], %)
 | 
			
		||||
      |> close(%)
 | 
			
		||||
    sketch004 = startSketchOn(extrude001, seg05)
 | 
			
		||||
      |> startProfileAt([82.57, 322.96], %)
 | 
			
		||||
      |> angledLine([0, 11.16], %, $rectangleSegmentA004)
 | 
			
		||||
      |> angledLine([
 | 
			
		||||
           segAng(rectangleSegmentA004) - 90,
 | 
			
		||||
           103.07
 | 
			
		||||
         ], %, $rectangleSegmentB003)
 | 
			
		||||
      |> angledLine([
 | 
			
		||||
           segAng(rectangleSegmentA004),
 | 
			
		||||
           -segLen(rectangleSegmentA004)
 | 
			
		||||
         ], %, $rectangleSegmentC003)
 | 
			
		||||
      |> lineTo([profileStartX(%), profileStartY(%)], %)
 | 
			
		||||
      |> close(%)
 | 
			
		||||
    sketch003 = startSketchOn(extrude001, seg04)
 | 
			
		||||
      |> startProfileAt([-209.64, 255.28], %)
 | 
			
		||||
      |> angledLine([0, 11.56], %, $rectangleSegmentA003)
 | 
			
		||||
      |> angledLine([
 | 
			
		||||
           segAng(rectangleSegmentA003) - 90,
 | 
			
		||||
           106.84
 | 
			
		||||
         ], %, $rectangleSegmentB002)
 | 
			
		||||
      |> angledLine([
 | 
			
		||||
           segAng(rectangleSegmentA003),
 | 
			
		||||
           -segLen(rectangleSegmentA003)
 | 
			
		||||
         ], %, $rectangleSegmentC002)
 | 
			
		||||
      |> lineTo([profileStartX(%), profileStartY(%)], %)
 | 
			
		||||
      |> close(%)
 | 
			
		||||
    sketch002 = startSketchOn(extrude001, seg03)
 | 
			
		||||
      |> startProfileAt([205.96, 254.59], %)
 | 
			
		||||
      |> angledLine([0, 11.39], %, $rectangleSegmentA002)
 | 
			
		||||
      |> angledLine([
 | 
			
		||||
           segAng(rectangleSegmentA002) - 90,
 | 
			
		||||
           105.26
 | 
			
		||||
         ], %, $rectangleSegmentB001)
 | 
			
		||||
      |> angledLine([
 | 
			
		||||
           segAng(rectangleSegmentA002),
 | 
			
		||||
           -segLen(rectangleSegmentA002)
 | 
			
		||||
         ], %, $rectangleSegmentC001)
 | 
			
		||||
      |> lineTo([profileStartX(%), profileStartY(%)], %)
 | 
			
		||||
      |> close(%)
 | 
			
		||||
    `,
 | 
			
		||||
  |> startProfileAt([75.8, 317.2], %) // [$startCapTag, $EndCapTag]
 | 
			
		||||
  |> angledLine([0, 268.43], %, $rectangleSegmentA001)
 | 
			
		||||
  |> angledLine([
 | 
			
		||||
       segAng(rectangleSegmentA001) - 90,
 | 
			
		||||
       217.26
 | 
			
		||||
     ], %, $seg01)
 | 
			
		||||
  |> angledLine([
 | 
			
		||||
       segAng(rectangleSegmentA001),
 | 
			
		||||
       -segLen(rectangleSegmentA001)
 | 
			
		||||
     ], %, $yo)
 | 
			
		||||
  |> lineTo([profileStartX(%), profileStartY(%)], %, $seg02)
 | 
			
		||||
  |> close(%)
 | 
			
		||||
extrude001 = extrude(100, sketch001)
 | 
			
		||||
  |> chamfer({
 | 
			
		||||
       length = 30,
 | 
			
		||||
       tags = [getOppositeEdge(seg01)]
 | 
			
		||||
     }, %, $seg03)
 | 
			
		||||
  |> chamfer({ length = 30, tags = [seg01] }, %, $seg04)
 | 
			
		||||
  |> chamfer({
 | 
			
		||||
       length = 30,
 | 
			
		||||
       tags = [getNextAdjacentEdge(seg02)]
 | 
			
		||||
     }, %, $seg05)
 | 
			
		||||
  |> chamfer({
 | 
			
		||||
       length = 30,
 | 
			
		||||
       tags = [getNextAdjacentEdge(yo)]
 | 
			
		||||
     }, %, $seg06)
 | 
			
		||||
sketch005 = startSketchOn(extrude001, seg06)
 | 
			
		||||
profile004 = startProfileAt([-23.43, 19.69], sketch005)
 | 
			
		||||
  |> angledLine([0, 9.1], %, $rectangleSegmentA005)
 | 
			
		||||
  |> angledLine([
 | 
			
		||||
       segAng(rectangleSegmentA005) - 90,
 | 
			
		||||
       84.07
 | 
			
		||||
     ], %)
 | 
			
		||||
  |> angledLine([
 | 
			
		||||
       segAng(rectangleSegmentA005),
 | 
			
		||||
       -segLen(rectangleSegmentA005)
 | 
			
		||||
     ], %)
 | 
			
		||||
  |> lineTo([profileStartX(%), profileStartY(%)], %)
 | 
			
		||||
  |> close(%)
 | 
			
		||||
sketch004 = startSketchOn(extrude001, seg05)
 | 
			
		||||
profile003 = startProfileAt([82.57, 322.96], sketch004)
 | 
			
		||||
  |> angledLine([0, 11.16], %, $rectangleSegmentA004)
 | 
			
		||||
  |> angledLine([
 | 
			
		||||
       segAng(rectangleSegmentA004) - 90,
 | 
			
		||||
       103.07
 | 
			
		||||
     ], %)
 | 
			
		||||
  |> angledLine([
 | 
			
		||||
       segAng(rectangleSegmentA004),
 | 
			
		||||
       -segLen(rectangleSegmentA004)
 | 
			
		||||
     ], %)
 | 
			
		||||
  |> lineTo([profileStartX(%), profileStartY(%)], %)
 | 
			
		||||
  |> close(%)
 | 
			
		||||
sketch003 = startSketchOn(extrude001, seg04)
 | 
			
		||||
profile002 = startProfileAt([-209.64, 255.28], sketch003)
 | 
			
		||||
  |> angledLine([0, 11.56], %, $rectangleSegmentA003)
 | 
			
		||||
  |> angledLine([
 | 
			
		||||
       segAng(rectangleSegmentA003) - 90,
 | 
			
		||||
       106.84
 | 
			
		||||
     ], %)
 | 
			
		||||
  |> angledLine([
 | 
			
		||||
       segAng(rectangleSegmentA003),
 | 
			
		||||
       -segLen(rectangleSegmentA003)
 | 
			
		||||
     ], %)
 | 
			
		||||
  |> lineTo([profileStartX(%), profileStartY(%)], %)
 | 
			
		||||
  |> close(%)
 | 
			
		||||
sketch002 = startSketchOn(extrude001, seg03)
 | 
			
		||||
profile001 = startProfileAt([205.96, 254.59], sketch002)
 | 
			
		||||
  |> angledLine([0, 11.39], %, $rectangleSegmentA002)
 | 
			
		||||
  |> angledLine([
 | 
			
		||||
       segAng(rectangleSegmentA002) - 90,
 | 
			
		||||
       105.26
 | 
			
		||||
     ], %)
 | 
			
		||||
  |> angledLine([
 | 
			
		||||
       segAng(rectangleSegmentA002),
 | 
			
		||||
       -segLen(rectangleSegmentA002)
 | 
			
		||||
     ], %)
 | 
			
		||||
  |> lineTo([profileStartX(%), profileStartY(%)], %)
 | 
			
		||||
  |> close(%)
 | 
			
		||||
`,
 | 
			
		||||
          { shouldNormalise: true }
 | 
			
		||||
        )
 | 
			
		||||
      })
 | 
			
		||||
@ -392,18 +373,13 @@ test.describe('verify sketch on chamfer works', () => {
 | 
			
		||||
        beforeChamferSnippetEnd: '}, extrude001)',
 | 
			
		||||
        afterChamferSelectSnippet:
 | 
			
		||||
          'sketch002 = startSketchOn(extrude001, seg03)',
 | 
			
		||||
        afterRectangle1stClickSnippet: 'startProfileAt([205.96, 254.59], %)',
 | 
			
		||||
        afterRectangle2ndClickSnippet: `angledLine([0, 11.39], %, $rectangleSegmentA002)
 | 
			
		||||
    |> angledLine([
 | 
			
		||||
         segAng(rectangleSegmentA002) - 90,
 | 
			
		||||
         105.26
 | 
			
		||||
       ], %, $rectangleSegmentB001)
 | 
			
		||||
    |> angledLine([
 | 
			
		||||
         segAng(rectangleSegmentA002),
 | 
			
		||||
         -segLen(rectangleSegmentA002)
 | 
			
		||||
       ], %, $rectangleSegmentC001)
 | 
			
		||||
    |> lineTo([profileStartX(%), profileStartY(%)], %)
 | 
			
		||||
    |> close(%)`,
 | 
			
		||||
        afterRectangle1stClickSnippet:
 | 
			
		||||
          'startProfileAt([205.96, 254.59], sketch002)',
 | 
			
		||||
        afterRectangle2ndClickSnippet: `angledLine([0,11.39],%,$rectangleSegmentA002)
 | 
			
		||||
        |>angledLine([segAng(rectangleSegmentA002)-90,105.26],%)
 | 
			
		||||
        |>angledLine([segAng(rectangleSegmentA002),-segLen(rectangleSegmentA002)],%)
 | 
			
		||||
        |>lineTo([profileStartX(%),profileStartY(%)],%)
 | 
			
		||||
        |>close(%)`,
 | 
			
		||||
      })
 | 
			
		||||
      await editor.expectEditor.toContain(
 | 
			
		||||
        `sketch001 = startSketchOn('XZ')
 | 
			
		||||
@ -433,16 +409,16 @@ chamf = chamfer({
 | 
			
		||||
       ]
 | 
			
		||||
     }, %)
 | 
			
		||||
sketch002 = startSketchOn(extrude001, seg03)
 | 
			
		||||
  |> startProfileAt([205.96, 254.59], %)
 | 
			
		||||
profile001 = startProfileAt([205.96, 254.59], sketch002)
 | 
			
		||||
  |> angledLine([0, 11.39], %, $rectangleSegmentA002)
 | 
			
		||||
  |> angledLine([
 | 
			
		||||
       segAng(rectangleSegmentA002) - 90,
 | 
			
		||||
       105.26
 | 
			
		||||
     ], %, $rectangleSegmentB001)
 | 
			
		||||
     ], %)
 | 
			
		||||
  |> angledLine([
 | 
			
		||||
       segAng(rectangleSegmentA002),
 | 
			
		||||
       -segLen(rectangleSegmentA002)
 | 
			
		||||
     ], %, $rectangleSegmentC001)
 | 
			
		||||
     ], %)
 | 
			
		||||
  |> lineTo([profileStartX(%), profileStartY(%)], %)
 | 
			
		||||
  |> close(%)
 | 
			
		||||
`,
 | 
			
		||||
@ -504,10 +480,10 @@ test(`Verify axis, origin, and horizontal snapping`, async ({
 | 
			
		||||
 | 
			
		||||
  const expectedCodeSnippets = {
 | 
			
		||||
    sketchOnXzPlane: `sketch001 = startSketchOn('XZ')`,
 | 
			
		||||
    pointAtOrigin: `startProfileAt([${originSloppy.kcl[0]}, ${originSloppy.kcl[1]}], %)`,
 | 
			
		||||
    pointAtOrigin: `startProfileAt([${originSloppy.kcl[0]}, ${originSloppy.kcl[1]}], sketch001)`,
 | 
			
		||||
    segmentOnXAxis: `xLine(${xAxisSloppy.kcl[0]}, %)`,
 | 
			
		||||
    afterSegmentDraggedOffYAxis: `startProfileAt([${offYAxis.kcl[0]}, ${offYAxis.kcl[1]}], %)`,
 | 
			
		||||
    afterSegmentDraggedOnYAxis: `startProfileAt([${yAxisSloppy.kcl[0]}, ${yAxisSloppy.kcl[1]}], %)`,
 | 
			
		||||
    afterSegmentDraggedOffYAxis: `startProfileAt([${offYAxis.kcl[0]}, ${offYAxis.kcl[1]}], sketch001)`,
 | 
			
		||||
    afterSegmentDraggedOnYAxis: `startProfileAt([${yAxisSloppy.kcl[0]}, ${yAxisSloppy.kcl[1]}], sketch001)`,
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  await app.initialise()
 | 
			
		||||
 | 
			
		||||
@ -446,8 +446,7 @@ test(
 | 
			
		||||
 | 
			
		||||
    const startXPx = 600
 | 
			
		||||
    await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
 | 
			
		||||
    code += `
 | 
			
		||||
  |> startProfileAt([7.19, -9.7], %)`
 | 
			
		||||
    code += `profile001 = startProfileAt([7.19, -9.7], sketch001)`
 | 
			
		||||
    await expect(page.locator('.cm-content')).toHaveText(code)
 | 
			
		||||
    await page.waitForTimeout(100)
 | 
			
		||||
 | 
			
		||||
@ -469,6 +468,10 @@ test(
 | 
			
		||||
      .getByRole('button', { name: 'arc Tangential Arc', exact: true })
 | 
			
		||||
      .click()
 | 
			
		||||
 | 
			
		||||
    // click to continue profile
 | 
			
		||||
    await page.mouse.move(813, 392, { steps: 10 })
 | 
			
		||||
    await page.waitForTimeout(100)
 | 
			
		||||
 | 
			
		||||
    await page.mouse.move(startXPx + PUR * 30, 500 - PUR * 20, { steps: 10 })
 | 
			
		||||
 | 
			
		||||
    await page.waitForTimeout(1000)
 | 
			
		||||
@ -591,8 +594,7 @@ test(
 | 
			
		||||
      mask: [page.getByTestId('model-state-indicator')],
 | 
			
		||||
    })
 | 
			
		||||
    await expect(page.locator('.cm-content')).toHaveText(
 | 
			
		||||
      `sketch001 = startSketchOn('XZ')
 | 
			
		||||
  |> circle({ center = [14.44, -2.44], radius = 1 }, %)`
 | 
			
		||||
      `sketch001 = startSketchOn('XZ')profile001 = circle({ center = [14.44, -2.44], radius = 1 }, sketch001)`
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
)
 | 
			
		||||
@ -636,8 +638,7 @@ test.describe(
 | 
			
		||||
 | 
			
		||||
      const startXPx = 600
 | 
			
		||||
      await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
 | 
			
		||||
      code += `
 | 
			
		||||
  |> startProfileAt([7.19, -9.7], %)`
 | 
			
		||||
      code += `profile001 = startProfileAt([7.19, -9.7], sketch001)`
 | 
			
		||||
      await expect(u.codeLocator).toHaveText(code)
 | 
			
		||||
      await page.waitForTimeout(100)
 | 
			
		||||
 | 
			
		||||
@ -655,6 +656,10 @@ test.describe(
 | 
			
		||||
        .click()
 | 
			
		||||
      await page.waitForTimeout(100)
 | 
			
		||||
 | 
			
		||||
      // click to continue profile
 | 
			
		||||
      await page.mouse.click(813, 392)
 | 
			
		||||
      await page.waitForTimeout(100)
 | 
			
		||||
 | 
			
		||||
      await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20)
 | 
			
		||||
 | 
			
		||||
      code += `
 | 
			
		||||
@ -741,8 +746,7 @@ test.describe(
 | 
			
		||||
 | 
			
		||||
      const startXPx = 600
 | 
			
		||||
      await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
 | 
			
		||||
      code += `
 | 
			
		||||
  |> startProfileAt([182.59, -246.32], %)`
 | 
			
		||||
      code += `profile001 = startProfileAt([182.59, -246.32], sketch001)`
 | 
			
		||||
      await expect(u.codeLocator).toHaveText(code)
 | 
			
		||||
      await page.waitForTimeout(100)
 | 
			
		||||
 | 
			
		||||
@ -760,6 +764,10 @@ test.describe(
 | 
			
		||||
        .click()
 | 
			
		||||
      await page.waitForTimeout(100)
 | 
			
		||||
 | 
			
		||||
      // click to continue profile
 | 
			
		||||
      await page.mouse.click(813, 392)
 | 
			
		||||
      await page.waitForTimeout(100)
 | 
			
		||||
 | 
			
		||||
      await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20)
 | 
			
		||||
 | 
			
		||||
      code += `
 | 
			
		||||
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 53 KiB  | 
| 
		 Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 50 KiB  | 
| 
		 Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 47 KiB  | 
| 
		 Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB  | 
| 
		 Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB  | 
| 
		 Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 52 KiB  | 
| 
		 Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 48 KiB  | 
| 
		 Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 48 KiB  | 
| 
		 Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 45 KiB  | 
| 
		 Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 45 KiB  | 
| 
		 Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB  | 
| 
		 Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 42 KiB  | 
| 
		 Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB  | 
| 
		 Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB  | 
| 
		 Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB  | 
| 
		 Before Width: | Height: | Size: 139 KiB After Width: | Height: | Size: 139 KiB  | 
@ -1,6 +1,8 @@
 | 
			
		||||
import { test, expect } from '@playwright/test'
 | 
			
		||||
 | 
			
		||||
import { commonPoints, getUtils, setup, tearDown } from './test-utils'
 | 
			
		||||
import { uuidv4 } from 'lib/utils'
 | 
			
		||||
import { EngineCommand } from 'lang/std/artifactGraph'
 | 
			
		||||
 | 
			
		||||
test.beforeEach(async ({ context, page }, testInfo) => {
 | 
			
		||||
  await setup(context, page, testInfo)
 | 
			
		||||
@ -130,17 +132,16 @@ test.describe('Test network and connection issues', () => {
 | 
			
		||||
 | 
			
		||||
    const startXPx = 600
 | 
			
		||||
    await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
 | 
			
		||||
    await expect(page.locator('.cm-content'))
 | 
			
		||||
      .toHaveText(`sketch001 = startSketchOn('XZ')
 | 
			
		||||
    |> startProfileAt(${commonPoints.startAt}, %)`)
 | 
			
		||||
    await expect(page.locator('.cm-content')).toHaveText(
 | 
			
		||||
      `sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${commonPoints.startAt}, sketch001)`
 | 
			
		||||
    )
 | 
			
		||||
    await page.waitForTimeout(100)
 | 
			
		||||
 | 
			
		||||
    await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
 | 
			
		||||
    await page.waitForTimeout(100)
 | 
			
		||||
 | 
			
		||||
    await expect(page.locator('.cm-content'))
 | 
			
		||||
      .toHaveText(`sketch001 = startSketchOn('XZ')
 | 
			
		||||
    |> startProfileAt(${commonPoints.startAt}, %)
 | 
			
		||||
      .toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${commonPoints.startAt}, sketch001)
 | 
			
		||||
    |> xLine(${commonPoints.num1}, %)`)
 | 
			
		||||
 | 
			
		||||
    // Expect the network to be up
 | 
			
		||||
@ -188,7 +189,9 @@ test.describe('Test network and connection issues', () => {
 | 
			
		||||
    await page.mouse.click(100, 100)
 | 
			
		||||
 | 
			
		||||
    // select a line
 | 
			
		||||
    await page.getByText(`startProfileAt(${commonPoints.startAt}, %)`).click()
 | 
			
		||||
    await page
 | 
			
		||||
      .getByText(`startProfileAt(${commonPoints.startAt}, sketch001)`)
 | 
			
		||||
      .click()
 | 
			
		||||
 | 
			
		||||
    // enter sketch again
 | 
			
		||||
    await u.doAndWaitForCmd(
 | 
			
		||||
@ -202,11 +205,36 @@ test.describe('Test network and connection issues', () => {
 | 
			
		||||
 | 
			
		||||
    await page.waitForTimeout(150)
 | 
			
		||||
 | 
			
		||||
    const camCommand: EngineCommand = {
 | 
			
		||||
      type: 'modeling_cmd_req',
 | 
			
		||||
      cmd_id: uuidv4(),
 | 
			
		||||
      cmd: {
 | 
			
		||||
        type: 'default_camera_look_at',
 | 
			
		||||
        center: { x: 109, y: 0, z: -152 },
 | 
			
		||||
        vantage: { x: 115, y: -505, z: -152 },
 | 
			
		||||
        up: { x: 0, y: 0, z: 1 },
 | 
			
		||||
      },
 | 
			
		||||
    }
 | 
			
		||||
    const updateCamCommand: EngineCommand = {
 | 
			
		||||
      type: 'modeling_cmd_req',
 | 
			
		||||
      cmd_id: uuidv4(),
 | 
			
		||||
      cmd: {
 | 
			
		||||
        type: 'default_camera_get_settings',
 | 
			
		||||
      },
 | 
			
		||||
    }
 | 
			
		||||
    await u.sendCustomCmd(camCommand)
 | 
			
		||||
    await page.waitForTimeout(100)
 | 
			
		||||
    await u.sendCustomCmd(updateCamCommand)
 | 
			
		||||
    await page.waitForTimeout(100)
 | 
			
		||||
 | 
			
		||||
    // click to continue profile
 | 
			
		||||
    await page.mouse.click(1007, 400)
 | 
			
		||||
    await page.waitForTimeout(100)
 | 
			
		||||
    // Ensure we can continue sketching
 | 
			
		||||
    await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
 | 
			
		||||
    await expect.poll(u.normalisedEditorCode)
 | 
			
		||||
      .toBe(`sketch001 = startSketchOn('XZ')
 | 
			
		||||
  |> startProfileAt([12.34, -12.34], %)
 | 
			
		||||
profile001 = startProfileAt([12.34, -12.34], sketch001)
 | 
			
		||||
  |> xLine(12.34, %)
 | 
			
		||||
  |> line([-12.34, 12.34], %)
 | 
			
		||||
 | 
			
		||||
@ -216,7 +244,7 @@ test.describe('Test network and connection issues', () => {
 | 
			
		||||
 | 
			
		||||
    await expect.poll(u.normalisedEditorCode)
 | 
			
		||||
      .toBe(`sketch001 = startSketchOn('XZ')
 | 
			
		||||
  |> startProfileAt([12.34, -12.34], %)
 | 
			
		||||
profile001 = startProfileAt([12.34, -12.34], sketch001)
 | 
			
		||||
  |> xLine(12.34, %)
 | 
			
		||||
  |> line([-12.34, 12.34], %)
 | 
			
		||||
  |> xLine(-12.34, %)
 | 
			
		||||
 | 
			
		||||
@ -17,11 +17,11 @@ test.describe('Testing constraints', () => {
 | 
			
		||||
      localStorage.setItem(
 | 
			
		||||
        'persistCode',
 | 
			
		||||
        `sketch001 = startSketchOn('XY')
 | 
			
		||||
    |> startProfileAt([-10, -10], %)
 | 
			
		||||
    |> line([20, 0], %)
 | 
			
		||||
    |> line([0, 20], %)
 | 
			
		||||
    |> xLine(-20, %)
 | 
			
		||||
  `
 | 
			
		||||
  |> startProfileAt([-10, -10], %)
 | 
			
		||||
  |> line([20, 0], %)
 | 
			
		||||
  |> line([0, 20], %)
 | 
			
		||||
  |> xLine(-20, %)
 | 
			
		||||
`
 | 
			
		||||
      )
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -77,30 +77,31 @@ test.describe('Testing selections', () => {
 | 
			
		||||
      const startXPx = 600
 | 
			
		||||
      await u.closeDebugPanel()
 | 
			
		||||
      await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
 | 
			
		||||
      await expect(page.locator('.cm-content'))
 | 
			
		||||
        .toHaveText(`sketch001 = startSketchOn('XZ')
 | 
			
		||||
    |> startProfileAt(${commonPoints.startAt}, %)`)
 | 
			
		||||
      await expect(page.locator('.cm-content')).toHaveText(
 | 
			
		||||
        `sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${commonPoints.startAt}, sketch001)`
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
      await page.waitForTimeout(100)
 | 
			
		||||
      await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
 | 
			
		||||
 | 
			
		||||
      await expect(page.locator('.cm-content'))
 | 
			
		||||
        .toHaveText(`sketch001 = startSketchOn('XZ')
 | 
			
		||||
    |> startProfileAt(${commonPoints.startAt}, %)
 | 
			
		||||
        .toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${commonPoints.startAt}, sketch001)
 | 
			
		||||
    |> xLine(${commonPoints.num1}, %)`)
 | 
			
		||||
 | 
			
		||||
      await page.waitForTimeout(100)
 | 
			
		||||
      await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
 | 
			
		||||
      await expect(page.locator('.cm-content'))
 | 
			
		||||
        .toHaveText(`sketch001 = startSketchOn('XZ')
 | 
			
		||||
    |> startProfileAt(${commonPoints.startAt}, %)
 | 
			
		||||
        .toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${
 | 
			
		||||
        commonPoints.startAt
 | 
			
		||||
      }, sketch001)
 | 
			
		||||
    |> xLine(${commonPoints.num1}, %)
 | 
			
		||||
    |> yLine(${commonPoints.num1 + 0.01}, %)`)
 | 
			
		||||
      await page.waitForTimeout(100)
 | 
			
		||||
      await page.mouse.click(startXPx, 500 - PUR * 20)
 | 
			
		||||
      await expect(page.locator('.cm-content'))
 | 
			
		||||
        .toHaveText(`sketch001 = startSketchOn('XZ')
 | 
			
		||||
    |> startProfileAt(${commonPoints.startAt}, %)
 | 
			
		||||
        .toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${
 | 
			
		||||
        commonPoints.startAt
 | 
			
		||||
      }, sketch001)
 | 
			
		||||
    |> xLine(${commonPoints.num1}, %)
 | 
			
		||||
    |> yLine(${commonPoints.num1 + 0.01}, %)
 | 
			
		||||
    |> xLine(${commonPoints.num2 * -1}, %)`)
 | 
			
		||||
@ -330,6 +331,28 @@ part009 = startSketchOn('XY')
 | 
			
		||||
  |> angledLineToX({ angle = 60, to = pipeLargeDia }, %)
 | 
			
		||||
  |> close(%)
 | 
			
		||||
rev = revolve({ axis = 'y' }, part009)
 | 
			
		||||
sketch006 = startSketchOn('XY')
 | 
			
		||||
profile001 = circle({
 | 
			
		||||
  center = [42.91, -70.42],
 | 
			
		||||
  radius = 17.96
 | 
			
		||||
}, sketch006)
 | 
			
		||||
profile002 = startProfileAt([86.92, -63.81], sketch006)
 | 
			
		||||
  |> angledLine([0, 63.81], %, $rectangleSegmentA001)
 | 
			
		||||
  |> angledLine([
 | 
			
		||||
       segAng(rectangleSegmentA001) - 90,
 | 
			
		||||
       17.05
 | 
			
		||||
     ], %)
 | 
			
		||||
  |> angledLine([
 | 
			
		||||
       segAng(rectangleSegmentA001),
 | 
			
		||||
       -segLen(rectangleSegmentA001)
 | 
			
		||||
     ], %)
 | 
			
		||||
  |> lineTo([profileStartX(%), profileStartY(%)], %)
 | 
			
		||||
  |> close(%)
 | 
			
		||||
profile003 = startProfileAt([40.16, -120.48], sketch006)
 | 
			
		||||
  |> line([26.95, 24.21], %)
 | 
			
		||||
  |> line([20.91, -28.61], %)
 | 
			
		||||
  |> line([32.46, 18.71], %)
 | 
			
		||||
 | 
			
		||||
`
 | 
			
		||||
      )
 | 
			
		||||
    }, KCL_DEFAULT_LENGTH)
 | 
			
		||||
@ -362,9 +385,10 @@ rev = revolve({ axis = 'y' }, part009)
 | 
			
		||||
    })
 | 
			
		||||
    await page.waitForTimeout(100)
 | 
			
		||||
 | 
			
		||||
    const revolve = { x: 646, y: 248 }
 | 
			
		||||
    const revolve = { x: 635, y: 253 }
 | 
			
		||||
    const parentExtrude = { x: 915, y: 133 }
 | 
			
		||||
    const solid2d = { x: 770, y: 167 }
 | 
			
		||||
    const individualProfile = { x: 694, y: 432 }
 | 
			
		||||
 | 
			
		||||
    // DELETE REVOLVE
 | 
			
		||||
    await page.mouse.click(revolve.x, revolve.y)
 | 
			
		||||
@ -430,6 +454,20 @@ rev = revolve({ axis = 'y' }, part009)
 | 
			
		||||
    await u.expectCmdLog('[data-message-type="execution-done"]', 10_000)
 | 
			
		||||
    await page.waitForTimeout(200)
 | 
			
		||||
    await expect(u.codeLocator).not.toContainText(`sketch005 = startSketchOn({`)
 | 
			
		||||
 | 
			
		||||
    // Delete a single profile
 | 
			
		||||
    await page.mouse.click(individualProfile.x, individualProfile.y)
 | 
			
		||||
    await page.waitForTimeout(100)
 | 
			
		||||
    const codeToBeDeletedSnippet =
 | 
			
		||||
      'profile003 = startProfileAt([40.16, -120.48], sketch006)'
 | 
			
		||||
    await expect(page.locator('.cm-activeLine')).toHaveText(
 | 
			
		||||
      '  |> line([20.91, -28.61], %)'
 | 
			
		||||
    )
 | 
			
		||||
    await u.clearCommandLogs()
 | 
			
		||||
    await page.keyboard.press('Backspace')
 | 
			
		||||
    await u.expectCmdLog('[data-message-type="execution-done"]', 10_000)
 | 
			
		||||
    await page.waitForTimeout(200)
 | 
			
		||||
    await expect(u.codeLocator).not.toContainText(codeToBeDeletedSnippet)
 | 
			
		||||
  })
 | 
			
		||||
  test("Deleting solid that the AST mod can't handle results in a toast message", async ({
 | 
			
		||||
    page,
 | 
			
		||||
@ -1258,12 +1296,15 @@ extrude001 = extrude(50, sketch001)
 | 
			
		||||
 | 
			
		||||
    await page.waitForTimeout(600)
 | 
			
		||||
 | 
			
		||||
    const firstClickCoords = { x: 650, y: 200 } as const
 | 
			
		||||
    // Place a point because the line tool will exit if no points are pressed
 | 
			
		||||
    await page.mouse.click(650, 200)
 | 
			
		||||
    await page.mouse.click(firstClickCoords.x, firstClickCoords.y)
 | 
			
		||||
    await page.waitForTimeout(600)
 | 
			
		||||
 | 
			
		||||
    // Code before exiting the tool
 | 
			
		||||
    let previousCodeContent = await page.locator('.cm-content').innerText()
 | 
			
		||||
    let previousCodeContent = (
 | 
			
		||||
      await page.locator('.cm-content').innerText()
 | 
			
		||||
    ).replace(/\s+/g, '')
 | 
			
		||||
 | 
			
		||||
    // deselect the line tool by clicking it
 | 
			
		||||
    await page.getByRole('button', { name: 'line Line', exact: true }).click()
 | 
			
		||||
@ -1275,14 +1316,23 @@ extrude001 = extrude(50, sketch001)
 | 
			
		||||
    await page.mouse.click(750, 200)
 | 
			
		||||
    await page.waitForTimeout(100)
 | 
			
		||||
 | 
			
		||||
    // expect no change
 | 
			
		||||
    await expect(page.locator('.cm-content')).toHaveText(previousCodeContent)
 | 
			
		||||
    await expect
 | 
			
		||||
      .poll(async () => {
 | 
			
		||||
        let str = await page.locator('.cm-content').innerText()
 | 
			
		||||
        str = str.replace(/\s+/g, '')
 | 
			
		||||
        return str
 | 
			
		||||
      })
 | 
			
		||||
      .toBe(previousCodeContent)
 | 
			
		||||
 | 
			
		||||
    // select line tool again
 | 
			
		||||
    await page.getByRole('button', { name: 'line Line', exact: true }).click()
 | 
			
		||||
 | 
			
		||||
    await u.closeDebugPanel()
 | 
			
		||||
 | 
			
		||||
    // Click to continue profile
 | 
			
		||||
    await page.mouse.click(firstClickCoords.x, firstClickCoords.y)
 | 
			
		||||
    await page.waitForTimeout(100)
 | 
			
		||||
 | 
			
		||||
    // line tool should work as expected again
 | 
			
		||||
    await page.mouse.click(700, 200)
 | 
			
		||||
    await expect(page.locator('.cm-content')).not.toHaveText(
 | 
			
		||||
 | 
			
		||||
@ -224,8 +224,13 @@ test('First escape in tool pops you out of tool, second exits sketch mode', asyn
 | 
			
		||||
  // Draw a line
 | 
			
		||||
  await page.mouse.move(700, 200, { steps: 5 })
 | 
			
		||||
  await page.mouse.click(700, 200)
 | 
			
		||||
  await page.mouse.move(800, 250, { steps: 5 })
 | 
			
		||||
  await page.mouse.click(800, 250)
 | 
			
		||||
 | 
			
		||||
  const secondMousePosition = { x: 800, y: 250 }
 | 
			
		||||
 | 
			
		||||
  await page.mouse.move(secondMousePosition.x, secondMousePosition.y, {
 | 
			
		||||
    steps: 5,
 | 
			
		||||
  })
 | 
			
		||||
  await page.mouse.click(secondMousePosition.x, secondMousePosition.y)
 | 
			
		||||
  // Unequip line tool
 | 
			
		||||
  await page.keyboard.press('Escape')
 | 
			
		||||
  // Make sure we didn't pop out of sketch mode.
 | 
			
		||||
@ -234,9 +239,17 @@ test('First escape in tool pops you out of tool, second exits sketch mode', asyn
 | 
			
		||||
  // Equip arc tool
 | 
			
		||||
  await page.keyboard.press('a')
 | 
			
		||||
  await expect(arcButton).toHaveAttribute('aria-pressed', 'true')
 | 
			
		||||
 | 
			
		||||
  // click in the same position again to continue the profile
 | 
			
		||||
  await page.mouse.move(secondMousePosition.x, secondMousePosition.y, {
 | 
			
		||||
    steps: 5,
 | 
			
		||||
  })
 | 
			
		||||
  await page.mouse.click(secondMousePosition.x, secondMousePosition.y)
 | 
			
		||||
 | 
			
		||||
  await page.mouse.move(1000, 100, { steps: 5 })
 | 
			
		||||
  await page.mouse.click(1000, 100)
 | 
			
		||||
  await page.keyboard.press('Escape')
 | 
			
		||||
  await expect(arcButton).toHaveAttribute('aria-pressed', 'false')
 | 
			
		||||
  await page.keyboard.press('l')
 | 
			
		||||
  await expect(lineButton).toHaveAttribute('aria-pressed', 'true')
 | 
			
		||||
 | 
			
		||||
@ -537,9 +550,9 @@ test('Sketch on face', async ({ page }) => {
 | 
			
		||||
 | 
			
		||||
  await expect.poll(u.normalisedEditorCode).toContain(
 | 
			
		||||
    u.normalisedCode(`sketch002 = startSketchOn(extrude001, seg01)
 | 
			
		||||
  |> startProfileAt([-12.94, 6.6], %)
 | 
			
		||||
  |> line([2.45, -0.2], %)
 | 
			
		||||
  |> line([-2.6, -1.25], %)
 | 
			
		||||
profile001 = startProfileAt([-12.88, 6.66], sketch002)
 | 
			
		||||
  |> line([2.71, -0.22], %)
 | 
			
		||||
  |> line([-2.87, -1.38], %)
 | 
			
		||||
  |> lineTo([profileStartX(%), profileStartY(%)], %)
 | 
			
		||||
  |> close(%)`)
 | 
			
		||||
  )
 | 
			
		||||
@ -554,8 +567,7 @@ test('Sketch on face', async ({ page }) => {
 | 
			
		||||
  await page.getByText('startProfileAt([-12').click()
 | 
			
		||||
  await expect(page.getByRole('button', { name: 'Edit Sketch' })).toBeVisible()
 | 
			
		||||
  await page.getByRole('button', { name: 'Edit Sketch' }).click()
 | 
			
		||||
  await page.waitForTimeout(400)
 | 
			
		||||
  await page.waitForTimeout(150)
 | 
			
		||||
  await page.waitForTimeout(500)
 | 
			
		||||
  await page.setViewportSize({ width: 1200, height: 1200 })
 | 
			
		||||
  await u.openAndClearDebugPanel()
 | 
			
		||||
  await u.updateCamPosition([452, -152, 1166])
 | 
			
		||||
 | 
			
		||||
@ -6,7 +6,6 @@ import { useCommandsContext } from 'hooks/useCommandsContext'
 | 
			
		||||
import { useNetworkContext } from 'hooks/useNetworkContext'
 | 
			
		||||
import { NetworkHealthState } from 'hooks/useNetworkStatus'
 | 
			
		||||
import { ActionButton } from 'components/ActionButton'
 | 
			
		||||
import { isSingleCursorInPipe } from 'lang/queryAst'
 | 
			
		||||
import { useKclContext } from 'lang/KclProvider'
 | 
			
		||||
import { ActionButtonDropdown } from 'components/ActionButtonDropdown'
 | 
			
		||||
import { useHotkeys } from 'react-hotkeys-hook'
 | 
			
		||||
@ -22,6 +21,7 @@ import {
 | 
			
		||||
} from 'lib/toolbar'
 | 
			
		||||
import { isDesktop } from 'lib/isDesktop'
 | 
			
		||||
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
 | 
			
		||||
import { isCursorInFunctionDefinition } from 'lang/queryAst'
 | 
			
		||||
 | 
			
		||||
export function Toolbar({
 | 
			
		||||
  className = '',
 | 
			
		||||
@ -38,7 +38,12 @@ export function Toolbar({
 | 
			
		||||
    '!border-transparent hover:!border-chalkboard-20 dark:enabled:hover:!border-primary pressed:!border-primary ui-open:!border-primary'
 | 
			
		||||
 | 
			
		||||
  const sketchPathId = useMemo(() => {
 | 
			
		||||
    if (!isSingleCursorInPipe(context.selectionRanges, kclManager.ast))
 | 
			
		||||
    if (
 | 
			
		||||
      isCursorInFunctionDefinition(
 | 
			
		||||
        kclManager.ast,
 | 
			
		||||
        context.selectionRanges.graphSelections[0]
 | 
			
		||||
      )
 | 
			
		||||
    )
 | 
			
		||||
      return false
 | 
			
		||||
    return isCursorInSketchCommandRange(
 | 
			
		||||
      engineCommandManager.artifactGraph,
 | 
			
		||||
 | 
			
		||||
@ -433,6 +433,8 @@ export async function deleteSegment({
 | 
			
		||||
  if (!sketchDetails) return
 | 
			
		||||
  await sceneEntitiesManager.updateAstAndRejigSketch(
 | 
			
		||||
    pathToNode,
 | 
			
		||||
    sketchDetails.sketchNodePaths,
 | 
			
		||||
    sketchDetails.planeNodePath,
 | 
			
		||||
    modifiedAst,
 | 
			
		||||
    sketchDetails.zAxis,
 | 
			
		||||
    sketchDetails.yAxis,
 | 
			
		||||
 | 
			
		||||
@ -691,19 +691,21 @@ export function createProfileStartHandle({
 | 
			
		||||
  scale = 1,
 | 
			
		||||
  theme,
 | 
			
		||||
  isSelected,
 | 
			
		||||
  size = 12,
 | 
			
		||||
  ...rest
 | 
			
		||||
}: {
 | 
			
		||||
  from: Coords2d
 | 
			
		||||
  scale?: number
 | 
			
		||||
  theme: Themes
 | 
			
		||||
  isSelected?: boolean
 | 
			
		||||
  size?: number
 | 
			
		||||
} & (
 | 
			
		||||
  | { isDraft: true }
 | 
			
		||||
  | { isDraft: false; id: string; pathToNode: PathToNode }
 | 
			
		||||
)) {
 | 
			
		||||
  const group = new Group()
 | 
			
		||||
 | 
			
		||||
  const geometry = new BoxGeometry(12, 12, 12) // in pixels scaled later
 | 
			
		||||
  const geometry = new BoxGeometry(size, size, size) // in pixels scaled later
 | 
			
		||||
  const baseColor = getThemeColorForThreeJs(theme)
 | 
			
		||||
  const color = isSelected ? 0x0000ff : baseColor
 | 
			
		||||
  const body = new MeshBasicMaterial({ color })
 | 
			
		||||
 | 
			
		||||
@ -24,7 +24,7 @@ import { useSetupEngineManager } from 'hooks/useSetupEngineManager'
 | 
			
		||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
 | 
			
		||||
import {
 | 
			
		||||
  isCursorInSketchCommandRange,
 | 
			
		||||
  updatePathToNodeFromMap,
 | 
			
		||||
  updateSketchDetailsNodePaths,
 | 
			
		||||
} from 'lang/util'
 | 
			
		||||
import {
 | 
			
		||||
  kclManager,
 | 
			
		||||
@ -71,14 +71,24 @@ import {
 | 
			
		||||
  replaceValueAtNodePath,
 | 
			
		||||
  sketchOnExtrudedFace,
 | 
			
		||||
  sketchOnOffsetPlane,
 | 
			
		||||
  splitPipedProfile,
 | 
			
		||||
  startSketchOnDefault,
 | 
			
		||||
} from 'lang/modifyAst'
 | 
			
		||||
import { PathToNode, Program, parse, recast, resultIsOk } from 'lang/wasm'
 | 
			
		||||
import {
 | 
			
		||||
  PathToNode,
 | 
			
		||||
  Program,
 | 
			
		||||
  VariableDeclaration,
 | 
			
		||||
  parse,
 | 
			
		||||
  recast,
 | 
			
		||||
  resultIsOk,
 | 
			
		||||
} from 'lang/wasm'
 | 
			
		||||
import {
 | 
			
		||||
  doesSceneHaveExtrudedSketch,
 | 
			
		||||
  doesSceneHaveSweepableSketch,
 | 
			
		||||
  getNodePathFromSourceRange,
 | 
			
		||||
  isSingleCursorInPipe,
 | 
			
		||||
  doesSketchPipeNeedSplitting,
 | 
			
		||||
  getNodeFromPath,
 | 
			
		||||
  isCursorInFunctionDefinition,
 | 
			
		||||
  traverse,
 | 
			
		||||
} from 'lang/queryAst'
 | 
			
		||||
import { exportFromEngine } from 'lib/exportFromEngine'
 | 
			
		||||
import { Models } from '@kittycad/lib/dist/types/src'
 | 
			
		||||
@ -86,7 +96,7 @@ import toast from 'react-hot-toast'
 | 
			
		||||
import { EditorSelection, Transaction } from '@codemirror/state'
 | 
			
		||||
import { useLoaderData, useNavigate, useSearchParams } from 'react-router-dom'
 | 
			
		||||
import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls'
 | 
			
		||||
import { err, reportRejection, trap } from 'lib/trap'
 | 
			
		||||
import { err, reportRejection, trap, reject } from 'lib/trap'
 | 
			
		||||
import { useCommandsContext } from 'hooks/useCommandsContext'
 | 
			
		||||
import { modelingMachineEvent } from 'editor/manager'
 | 
			
		||||
import { hasValidEdgeTreatmentSelection } from 'lang/modifyAst/addEdgeTreatment'
 | 
			
		||||
@ -100,6 +110,10 @@ import { useFileContext } from 'hooks/useFileContext'
 | 
			
		||||
import { uuidv4 } from 'lib/utils'
 | 
			
		||||
import { IndexLoaderData } from 'lib/types'
 | 
			
		||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
 | 
			
		||||
import {
 | 
			
		||||
  getPathsFromArtifact,
 | 
			
		||||
  getPlaneFromArtifact,
 | 
			
		||||
} from 'lang/std/artifactGraph'
 | 
			
		||||
 | 
			
		||||
type MachineContext<T extends AnyStateMachine> = {
 | 
			
		||||
  state: StateFrom<T>
 | 
			
		||||
@ -290,7 +304,7 @@ export const ModelingMachineProvider = ({
 | 
			
		||||
          return {
 | 
			
		||||
            sketchDetails: {
 | 
			
		||||
              ...sketchDetails,
 | 
			
		||||
              sketchPathToNode: event.data,
 | 
			
		||||
              sketchEntryNodePath: event.data,
 | 
			
		||||
            },
 | 
			
		||||
          }
 | 
			
		||||
        }),
 | 
			
		||||
@ -413,9 +427,17 @@ export const ModelingMachineProvider = ({
 | 
			
		||||
                selectionRanges: setSelections.selection,
 | 
			
		||||
                sketchDetails: {
 | 
			
		||||
                  ...sketchDetails,
 | 
			
		||||
                  sketchPathToNode:
 | 
			
		||||
                    setSelections.updatedPathToNode ||
 | 
			
		||||
                    sketchDetails?.sketchPathToNode ||
 | 
			
		||||
                  sketchEntryNodePath:
 | 
			
		||||
                    setSelections.updatedSketchEntryNodePath ||
 | 
			
		||||
                    sketchDetails?.sketchEntryNodePath ||
 | 
			
		||||
                    [],
 | 
			
		||||
                  sketchNodePaths:
 | 
			
		||||
                    setSelections.updatedSketchNodePaths ||
 | 
			
		||||
                    sketchDetails?.sketchNodePaths ||
 | 
			
		||||
                    [],
 | 
			
		||||
                  planeNodePath:
 | 
			
		||||
                    setSelections.updatedPlaneNodePath ||
 | 
			
		||||
                    sketchDetails?.planeNodePath ||
 | 
			
		||||
                    [],
 | 
			
		||||
                },
 | 
			
		||||
              }
 | 
			
		||||
@ -647,7 +669,12 @@ export const ModelingMachineProvider = ({
 | 
			
		||||
        'Selection is on face': ({ context: { selectionRanges }, event }) => {
 | 
			
		||||
          if (event.type !== 'Enter sketch') return false
 | 
			
		||||
          if (event.data?.forceNewSketch) return false
 | 
			
		||||
          if (!isSingleCursorInPipe(selectionRanges, kclManager.ast))
 | 
			
		||||
          if (
 | 
			
		||||
            isCursorInFunctionDefinition(
 | 
			
		||||
              kclManager.ast,
 | 
			
		||||
              selectionRanges.graphSelections[0]
 | 
			
		||||
            )
 | 
			
		||||
          )
 | 
			
		||||
            return false
 | 
			
		||||
          return !!isCursorInSketchCommandRange(
 | 
			
		||||
            engineCommandManager.artifactGraph,
 | 
			
		||||
@ -678,10 +705,32 @@ export const ModelingMachineProvider = ({
 | 
			
		||||
              // this assumes no changes have been made to the sketch besides what we did when entering the sketch
 | 
			
		||||
              // i.e. doesn't account for user's adding code themselves, maybe we need store a flag userEditedSinceSketchMode?
 | 
			
		||||
              const newAst = structuredClone(kclManager.ast)
 | 
			
		||||
              const varDecIndex = sketchDetails.sketchPathToNode[1][0]
 | 
			
		||||
              const varDecIndex = sketchDetails.planeNodePath[1][0]
 | 
			
		||||
 | 
			
		||||
              const varDec = getNodeFromPath<VariableDeclaration>(
 | 
			
		||||
                newAst,
 | 
			
		||||
                sketchDetails.planeNodePath,
 | 
			
		||||
                'VariableDeclaration'
 | 
			
		||||
              )
 | 
			
		||||
              if (err(varDec)) return reject(new Error('No varDec'))
 | 
			
		||||
              const variableName = varDec.node.declaration.id.name
 | 
			
		||||
              let isIdentifierUsed = false
 | 
			
		||||
              traverse(newAst, {
 | 
			
		||||
                enter: (node) => {
 | 
			
		||||
                  if (
 | 
			
		||||
                    node.type === 'Identifier' &&
 | 
			
		||||
                    node.name === variableName
 | 
			
		||||
                  ) {
 | 
			
		||||
                    isIdentifierUsed = true
 | 
			
		||||
                  }
 | 
			
		||||
                },
 | 
			
		||||
              })
 | 
			
		||||
              if (isIdentifierUsed) return
 | 
			
		||||
 | 
			
		||||
              // remove body item at varDecIndex
 | 
			
		||||
              newAst.body = newAst.body.filter((_, i) => i !== varDecIndex)
 | 
			
		||||
              await kclManager.executeAstMock(newAst)
 | 
			
		||||
              await codeManager.updateEditorWithAstAndWriteToFile(newAst)
 | 
			
		||||
            }
 | 
			
		||||
            sceneInfra.setCallbacks({
 | 
			
		||||
              onClick: () => {},
 | 
			
		||||
@ -691,7 +740,7 @@ export const ModelingMachineProvider = ({
 | 
			
		||||
          }
 | 
			
		||||
        ),
 | 
			
		||||
        'animate-to-face': fromPromise(async ({ input }) => {
 | 
			
		||||
          if (!input) return undefined
 | 
			
		||||
          if (!input) return null
 | 
			
		||||
          if (input.type === 'extrudeFace' || input.type === 'offsetPlane') {
 | 
			
		||||
            const sketched =
 | 
			
		||||
              input.type === 'extrudeFace'
 | 
			
		||||
@ -718,7 +767,9 @@ export const ModelingMachineProvider = ({
 | 
			
		||||
            await letEngineAnimateAndSyncCamAfter(engineCommandManager, id)
 | 
			
		||||
            sceneInfra.camControls.syncDirection = 'clientToEngine'
 | 
			
		||||
            return {
 | 
			
		||||
              sketchPathToNode: pathToNewSketchNode,
 | 
			
		||||
              sketchEntryNodePath: [],
 | 
			
		||||
              planeNodePath: pathToNewSketchNode,
 | 
			
		||||
              sketchNodePaths: [],
 | 
			
		||||
              zAxis: input.zAxis,
 | 
			
		||||
              yAxis: input.yAxis,
 | 
			
		||||
              origin: input.position,
 | 
			
		||||
@ -738,7 +789,9 @@ export const ModelingMachineProvider = ({
 | 
			
		||||
          )
 | 
			
		||||
 | 
			
		||||
          return {
 | 
			
		||||
            sketchPathToNode: pathToNode,
 | 
			
		||||
            sketchEntryNodePath: [],
 | 
			
		||||
            planeNodePath: pathToNode,
 | 
			
		||||
            sketchNodePaths: [],
 | 
			
		||||
            zAxis: input.zAxis,
 | 
			
		||||
            yAxis: input.yAxis,
 | 
			
		||||
            origin: [0, 0, 0],
 | 
			
		||||
@ -746,12 +799,14 @@ export const ModelingMachineProvider = ({
 | 
			
		||||
        }),
 | 
			
		||||
        'animate-to-sketch': fromPromise(
 | 
			
		||||
          async ({ input: { selectionRanges } }) => {
 | 
			
		||||
            const sourceRange =
 | 
			
		||||
              selectionRanges.graphSelections[0]?.codeRef?.range
 | 
			
		||||
            const sketchPathToNode = getNodePathFromSourceRange(
 | 
			
		||||
              kclManager.ast,
 | 
			
		||||
              sourceRange
 | 
			
		||||
            const sketchPathToNode =
 | 
			
		||||
              selectionRanges.graphSelections[0]?.codeRef?.pathToNode
 | 
			
		||||
            const plane = getPlaneFromArtifact(
 | 
			
		||||
              selectionRanges.graphSelections[0].artifact,
 | 
			
		||||
              engineCommandManager.artifactGraph
 | 
			
		||||
            )
 | 
			
		||||
            if (err(plane)) return Promise.reject(plane)
 | 
			
		||||
 | 
			
		||||
            const info = await getSketchOrientationDetails(
 | 
			
		||||
              sketchPathToNode || []
 | 
			
		||||
            )
 | 
			
		||||
@ -759,8 +814,17 @@ export const ModelingMachineProvider = ({
 | 
			
		||||
              engineCommandManager,
 | 
			
		||||
              info?.sketchDetails?.faceId || ''
 | 
			
		||||
            )
 | 
			
		||||
            return {
 | 
			
		||||
            const sketchPaths = getPathsFromArtifact({
 | 
			
		||||
              artifact: selectionRanges.graphSelections[0].artifact,
 | 
			
		||||
              sketchPathToNode: sketchPathToNode || [],
 | 
			
		||||
            })
 | 
			
		||||
            if (err(sketchPaths)) return Promise.reject(sketchPaths)
 | 
			
		||||
            if (!plane.codeRef)
 | 
			
		||||
              return Promise.reject(new Error('No plane codeRef'))
 | 
			
		||||
            return {
 | 
			
		||||
              sketchEntryNodePath: sketchPathToNode || [],
 | 
			
		||||
              sketchNodePaths: sketchPaths,
 | 
			
		||||
              planeNodePath: plane.codeRef.pathToNode,
 | 
			
		||||
              zAxis: info.sketchDetails.zAxis || null,
 | 
			
		||||
              yAxis: info.sketchDetails.yAxis || null,
 | 
			
		||||
              origin: info.sketchDetails.origin.map(
 | 
			
		||||
@ -772,7 +836,7 @@ export const ModelingMachineProvider = ({
 | 
			
		||||
 | 
			
		||||
        'Get horizontal info': fromPromise(
 | 
			
		||||
          async ({ input: { selectionRanges, sketchDetails } }) => {
 | 
			
		||||
            const { modifiedAst, pathToNodeMap } =
 | 
			
		||||
            const { modifiedAst, pathToNodeMap, exprInsertIndex } =
 | 
			
		||||
              await applyConstraintHorzVertDistance({
 | 
			
		||||
                constraint: 'setHorzDistance',
 | 
			
		||||
                selectionRanges,
 | 
			
		||||
@ -784,13 +848,23 @@ export const ModelingMachineProvider = ({
 | 
			
		||||
 | 
			
		||||
            if (!sketchDetails)
 | 
			
		||||
              return Promise.reject(new Error('No sketch details'))
 | 
			
		||||
            const updatedPathToNode = updatePathToNodeFromMap(
 | 
			
		||||
              sketchDetails.sketchPathToNode,
 | 
			
		||||
              pathToNodeMap
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            const {
 | 
			
		||||
              updatedSketchEntryNodePath,
 | 
			
		||||
              updatedSketchNodePaths,
 | 
			
		||||
              updatedPlaneNodePath,
 | 
			
		||||
            } = updateSketchDetailsNodePaths({
 | 
			
		||||
              sketchEntryNodePath: sketchDetails.sketchEntryNodePath,
 | 
			
		||||
              sketchNodePaths: sketchDetails.sketchNodePaths,
 | 
			
		||||
              planeNodePath: sketchDetails.planeNodePath,
 | 
			
		||||
              exprInsertIndex,
 | 
			
		||||
            })
 | 
			
		||||
 | 
			
		||||
            const updatedAst =
 | 
			
		||||
              await sceneEntitiesManager.updateAstAndRejigSketch(
 | 
			
		||||
                updatedPathToNode,
 | 
			
		||||
                updatedSketchEntryNodePath,
 | 
			
		||||
                updatedSketchNodePaths,
 | 
			
		||||
                updatedPlaneNodePath,
 | 
			
		||||
                _modifiedAst,
 | 
			
		||||
                sketchDetails.zAxis,
 | 
			
		||||
                sketchDetails.yAxis,
 | 
			
		||||
@ -811,13 +885,15 @@ export const ModelingMachineProvider = ({
 | 
			
		||||
            return {
 | 
			
		||||
              selectionType: 'completeSelection',
 | 
			
		||||
              selection,
 | 
			
		||||
              updatedPathToNode,
 | 
			
		||||
              updatedSketchEntryNodePath,
 | 
			
		||||
              updatedSketchNodePaths,
 | 
			
		||||
              updatedPlaneNodePath,
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        ),
 | 
			
		||||
        'Get vertical info': fromPromise(
 | 
			
		||||
          async ({ input: { selectionRanges, sketchDetails } }) => {
 | 
			
		||||
            const { modifiedAst, pathToNodeMap } =
 | 
			
		||||
            const { modifiedAst, pathToNodeMap, exprInsertIndex } =
 | 
			
		||||
              await applyConstraintHorzVertDistance({
 | 
			
		||||
                constraint: 'setVertDistance',
 | 
			
		||||
                selectionRanges,
 | 
			
		||||
@ -828,13 +904,23 @@ export const ModelingMachineProvider = ({
 | 
			
		||||
            const _modifiedAst = pResult.program
 | 
			
		||||
            if (!sketchDetails)
 | 
			
		||||
              return Promise.reject(new Error('No sketch details'))
 | 
			
		||||
            const updatedPathToNode = updatePathToNodeFromMap(
 | 
			
		||||
              sketchDetails.sketchPathToNode,
 | 
			
		||||
              pathToNodeMap
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            const {
 | 
			
		||||
              updatedSketchEntryNodePath,
 | 
			
		||||
              updatedSketchNodePaths,
 | 
			
		||||
              updatedPlaneNodePath,
 | 
			
		||||
            } = updateSketchDetailsNodePaths({
 | 
			
		||||
              sketchEntryNodePath: sketchDetails.sketchEntryNodePath,
 | 
			
		||||
              sketchNodePaths: sketchDetails.sketchNodePaths,
 | 
			
		||||
              planeNodePath: sketchDetails.planeNodePath,
 | 
			
		||||
              exprInsertIndex,
 | 
			
		||||
            })
 | 
			
		||||
 | 
			
		||||
            const updatedAst =
 | 
			
		||||
              await sceneEntitiesManager.updateAstAndRejigSketch(
 | 
			
		||||
                updatedPathToNode,
 | 
			
		||||
                updatedSketchEntryNodePath,
 | 
			
		||||
                updatedSketchNodePaths,
 | 
			
		||||
                updatedPlaneNodePath,
 | 
			
		||||
                _modifiedAst,
 | 
			
		||||
                sketchDetails.zAxis,
 | 
			
		||||
                sketchDetails.yAxis,
 | 
			
		||||
@ -855,7 +941,9 @@ export const ModelingMachineProvider = ({
 | 
			
		||||
            return {
 | 
			
		||||
              selectionType: 'completeSelection',
 | 
			
		||||
              selection,
 | 
			
		||||
              updatedPathToNode,
 | 
			
		||||
              updatedSketchEntryNodePath,
 | 
			
		||||
              updatedSketchNodePaths,
 | 
			
		||||
              updatedPlaneNodePath,
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        ),
 | 
			
		||||
@ -865,14 +953,15 @@ export const ModelingMachineProvider = ({
 | 
			
		||||
              selectionRanges,
 | 
			
		||||
            })
 | 
			
		||||
            if (err(info)) return Promise.reject(info)
 | 
			
		||||
            const { modifiedAst, pathToNodeMap } = await (info.enabled
 | 
			
		||||
              ? applyConstraintAngleBetween({
 | 
			
		||||
                  selectionRanges,
 | 
			
		||||
                })
 | 
			
		||||
              : applyConstraintAngleLength({
 | 
			
		||||
                  selectionRanges,
 | 
			
		||||
                  angleOrLength: 'setAngle',
 | 
			
		||||
                }))
 | 
			
		||||
            const { modifiedAst, pathToNodeMap, exprInsertIndex } =
 | 
			
		||||
              await (info.enabled
 | 
			
		||||
                ? applyConstraintAngleBetween({
 | 
			
		||||
                    selectionRanges,
 | 
			
		||||
                  })
 | 
			
		||||
                : applyConstraintAngleLength({
 | 
			
		||||
                    selectionRanges,
 | 
			
		||||
                    angleOrLength: 'setAngle',
 | 
			
		||||
                  }))
 | 
			
		||||
            const pResult = parse(recast(modifiedAst))
 | 
			
		||||
            if (trap(pResult) || !resultIsOk(pResult))
 | 
			
		||||
              return Promise.reject(new Error('Unexpected compilation error'))
 | 
			
		||||
@ -881,13 +970,23 @@ export const ModelingMachineProvider = ({
 | 
			
		||||
 | 
			
		||||
            if (!sketchDetails)
 | 
			
		||||
              return Promise.reject(new Error('No sketch details'))
 | 
			
		||||
            const updatedPathToNode = updatePathToNodeFromMap(
 | 
			
		||||
              sketchDetails.sketchPathToNode,
 | 
			
		||||
              pathToNodeMap
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            const {
 | 
			
		||||
              updatedSketchEntryNodePath,
 | 
			
		||||
              updatedSketchNodePaths,
 | 
			
		||||
              updatedPlaneNodePath,
 | 
			
		||||
            } = updateSketchDetailsNodePaths({
 | 
			
		||||
              sketchEntryNodePath: sketchDetails.sketchEntryNodePath,
 | 
			
		||||
              sketchNodePaths: sketchDetails.sketchNodePaths,
 | 
			
		||||
              planeNodePath: sketchDetails.planeNodePath,
 | 
			
		||||
              exprInsertIndex,
 | 
			
		||||
            })
 | 
			
		||||
 | 
			
		||||
            const updatedAst =
 | 
			
		||||
              await sceneEntitiesManager.updateAstAndRejigSketch(
 | 
			
		||||
                updatedPathToNode,
 | 
			
		||||
                updatedSketchEntryNodePath,
 | 
			
		||||
                updatedSketchNodePaths,
 | 
			
		||||
                updatedPlaneNodePath,
 | 
			
		||||
                _modifiedAst,
 | 
			
		||||
                sketchDetails.zAxis,
 | 
			
		||||
                sketchDetails.yAxis,
 | 
			
		||||
@ -908,7 +1007,9 @@ export const ModelingMachineProvider = ({
 | 
			
		||||
            return {
 | 
			
		||||
              selectionType: 'completeSelection',
 | 
			
		||||
              selection,
 | 
			
		||||
              updatedPathToNode,
 | 
			
		||||
              updatedSketchEntryNodePath,
 | 
			
		||||
              updatedSketchNodePaths,
 | 
			
		||||
              updatedPlaneNodePath,
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        ),
 | 
			
		||||
@ -923,20 +1024,30 @@ export const ModelingMachineProvider = ({
 | 
			
		||||
              length: lengthValue,
 | 
			
		||||
            })
 | 
			
		||||
            if (err(constraintResult)) return Promise.reject(constraintResult)
 | 
			
		||||
            const { modifiedAst, pathToNodeMap } = constraintResult
 | 
			
		||||
            const { modifiedAst, pathToNodeMap, exprInsertIndex } =
 | 
			
		||||
              constraintResult
 | 
			
		||||
            const pResult = parse(recast(modifiedAst))
 | 
			
		||||
            if (trap(pResult) || !resultIsOk(pResult))
 | 
			
		||||
              return Promise.reject(new Error('Unexpected compilation error'))
 | 
			
		||||
            const _modifiedAst = pResult.program
 | 
			
		||||
            if (!sketchDetails)
 | 
			
		||||
              return Promise.reject(new Error('No sketch details'))
 | 
			
		||||
            const updatedPathToNode = updatePathToNodeFromMap(
 | 
			
		||||
              sketchDetails.sketchPathToNode,
 | 
			
		||||
              pathToNodeMap
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            const {
 | 
			
		||||
              updatedSketchEntryNodePath,
 | 
			
		||||
              updatedSketchNodePaths,
 | 
			
		||||
              updatedPlaneNodePath,
 | 
			
		||||
            } = updateSketchDetailsNodePaths({
 | 
			
		||||
              sketchEntryNodePath: sketchDetails.sketchEntryNodePath,
 | 
			
		||||
              sketchNodePaths: sketchDetails.sketchNodePaths,
 | 
			
		||||
              planeNodePath: sketchDetails.planeNodePath,
 | 
			
		||||
              exprInsertIndex,
 | 
			
		||||
            })
 | 
			
		||||
            const updatedAst =
 | 
			
		||||
              await sceneEntitiesManager.updateAstAndRejigSketch(
 | 
			
		||||
                updatedPathToNode,
 | 
			
		||||
                updatedSketchEntryNodePath,
 | 
			
		||||
                updatedSketchNodePaths,
 | 
			
		||||
                updatedPlaneNodePath,
 | 
			
		||||
                _modifiedAst,
 | 
			
		||||
                sketchDetails.zAxis,
 | 
			
		||||
                sketchDetails.yAxis,
 | 
			
		||||
@ -957,13 +1068,15 @@ export const ModelingMachineProvider = ({
 | 
			
		||||
            return {
 | 
			
		||||
              selectionType: 'completeSelection',
 | 
			
		||||
              selection,
 | 
			
		||||
              updatedPathToNode,
 | 
			
		||||
              updatedSketchEntryNodePath,
 | 
			
		||||
              updatedSketchNodePaths,
 | 
			
		||||
              updatedPlaneNodePath,
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        ),
 | 
			
		||||
        'Get perpendicular distance info': fromPromise(
 | 
			
		||||
          async ({ input: { selectionRanges, sketchDetails } }) => {
 | 
			
		||||
            const { modifiedAst, pathToNodeMap } =
 | 
			
		||||
            const { modifiedAst, pathToNodeMap, exprInsertIndex } =
 | 
			
		||||
              await applyConstraintIntersect({
 | 
			
		||||
                selectionRanges,
 | 
			
		||||
              })
 | 
			
		||||
@ -973,13 +1086,22 @@ export const ModelingMachineProvider = ({
 | 
			
		||||
            const _modifiedAst = pResult.program
 | 
			
		||||
            if (!sketchDetails)
 | 
			
		||||
              return Promise.reject(new Error('No sketch details'))
 | 
			
		||||
            const updatedPathToNode = updatePathToNodeFromMap(
 | 
			
		||||
              sketchDetails.sketchPathToNode,
 | 
			
		||||
              pathToNodeMap
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            const {
 | 
			
		||||
              updatedSketchEntryNodePath,
 | 
			
		||||
              updatedSketchNodePaths,
 | 
			
		||||
              updatedPlaneNodePath,
 | 
			
		||||
            } = updateSketchDetailsNodePaths({
 | 
			
		||||
              sketchEntryNodePath: sketchDetails.sketchEntryNodePath,
 | 
			
		||||
              sketchNodePaths: sketchDetails.sketchNodePaths,
 | 
			
		||||
              planeNodePath: sketchDetails.planeNodePath,
 | 
			
		||||
              exprInsertIndex,
 | 
			
		||||
            })
 | 
			
		||||
            const updatedAst =
 | 
			
		||||
              await sceneEntitiesManager.updateAstAndRejigSketch(
 | 
			
		||||
                updatedPathToNode,
 | 
			
		||||
                updatedSketchEntryNodePath,
 | 
			
		||||
                updatedSketchNodePaths,
 | 
			
		||||
                updatedPlaneNodePath,
 | 
			
		||||
                _modifiedAst,
 | 
			
		||||
                sketchDetails.zAxis,
 | 
			
		||||
                sketchDetails.yAxis,
 | 
			
		||||
@ -1000,13 +1122,15 @@ export const ModelingMachineProvider = ({
 | 
			
		||||
            return {
 | 
			
		||||
              selectionType: 'completeSelection',
 | 
			
		||||
              selection,
 | 
			
		||||
              updatedPathToNode,
 | 
			
		||||
              updatedSketchEntryNodePath,
 | 
			
		||||
              updatedSketchNodePaths,
 | 
			
		||||
              updatedPlaneNodePath,
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        ),
 | 
			
		||||
        'Get ABS X info': fromPromise(
 | 
			
		||||
          async ({ input: { selectionRanges, sketchDetails } }) => {
 | 
			
		||||
            const { modifiedAst, pathToNodeMap } =
 | 
			
		||||
            const { modifiedAst, pathToNodeMap, exprInsertIndex } =
 | 
			
		||||
              await applyConstraintAbsDistance({
 | 
			
		||||
                constraint: 'xAbs',
 | 
			
		||||
                selectionRanges,
 | 
			
		||||
@ -1017,13 +1141,22 @@ export const ModelingMachineProvider = ({
 | 
			
		||||
            const _modifiedAst = pResult.program
 | 
			
		||||
            if (!sketchDetails)
 | 
			
		||||
              return Promise.reject(new Error('No sketch details'))
 | 
			
		||||
            const updatedPathToNode = updatePathToNodeFromMap(
 | 
			
		||||
              sketchDetails.sketchPathToNode,
 | 
			
		||||
              pathToNodeMap
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            const {
 | 
			
		||||
              updatedSketchEntryNodePath,
 | 
			
		||||
              updatedSketchNodePaths,
 | 
			
		||||
              updatedPlaneNodePath,
 | 
			
		||||
            } = updateSketchDetailsNodePaths({
 | 
			
		||||
              sketchEntryNodePath: sketchDetails.sketchEntryNodePath,
 | 
			
		||||
              sketchNodePaths: sketchDetails.sketchNodePaths,
 | 
			
		||||
              planeNodePath: sketchDetails.planeNodePath,
 | 
			
		||||
              exprInsertIndex,
 | 
			
		||||
            })
 | 
			
		||||
            const updatedAst =
 | 
			
		||||
              await sceneEntitiesManager.updateAstAndRejigSketch(
 | 
			
		||||
                updatedPathToNode,
 | 
			
		||||
                updatedSketchEntryNodePath,
 | 
			
		||||
                updatedSketchNodePaths,
 | 
			
		||||
                updatedPlaneNodePath,
 | 
			
		||||
                _modifiedAst,
 | 
			
		||||
                sketchDetails.zAxis,
 | 
			
		||||
                sketchDetails.yAxis,
 | 
			
		||||
@ -1044,13 +1177,15 @@ export const ModelingMachineProvider = ({
 | 
			
		||||
            return {
 | 
			
		||||
              selectionType: 'completeSelection',
 | 
			
		||||
              selection,
 | 
			
		||||
              updatedPathToNode,
 | 
			
		||||
              updatedSketchEntryNodePath,
 | 
			
		||||
              updatedSketchNodePaths,
 | 
			
		||||
              updatedPlaneNodePath,
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        ),
 | 
			
		||||
        'Get ABS Y info': fromPromise(
 | 
			
		||||
          async ({ input: { selectionRanges, sketchDetails } }) => {
 | 
			
		||||
            const { modifiedAst, pathToNodeMap } =
 | 
			
		||||
            const { modifiedAst, pathToNodeMap, exprInsertIndex } =
 | 
			
		||||
              await applyConstraintAbsDistance({
 | 
			
		||||
                constraint: 'yAbs',
 | 
			
		||||
                selectionRanges,
 | 
			
		||||
@ -1061,13 +1196,22 @@ export const ModelingMachineProvider = ({
 | 
			
		||||
            const _modifiedAst = pResult.program
 | 
			
		||||
            if (!sketchDetails)
 | 
			
		||||
              return Promise.reject(new Error('No sketch details'))
 | 
			
		||||
            const updatedPathToNode = updatePathToNodeFromMap(
 | 
			
		||||
              sketchDetails.sketchPathToNode,
 | 
			
		||||
              pathToNodeMap
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            const {
 | 
			
		||||
              updatedSketchEntryNodePath,
 | 
			
		||||
              updatedSketchNodePaths,
 | 
			
		||||
              updatedPlaneNodePath,
 | 
			
		||||
            } = updateSketchDetailsNodePaths({
 | 
			
		||||
              sketchEntryNodePath: sketchDetails.sketchEntryNodePath,
 | 
			
		||||
              sketchNodePaths: sketchDetails.sketchNodePaths,
 | 
			
		||||
              planeNodePath: sketchDetails.planeNodePath,
 | 
			
		||||
              exprInsertIndex,
 | 
			
		||||
            })
 | 
			
		||||
            const updatedAst =
 | 
			
		||||
              await sceneEntitiesManager.updateAstAndRejigSketch(
 | 
			
		||||
                updatedPathToNode,
 | 
			
		||||
                updatedSketchEntryNodePath,
 | 
			
		||||
                updatedSketchNodePaths,
 | 
			
		||||
                updatedPlaneNodePath,
 | 
			
		||||
                _modifiedAst,
 | 
			
		||||
                sketchDetails.zAxis,
 | 
			
		||||
                sketchDetails.yAxis,
 | 
			
		||||
@ -1088,7 +1232,9 @@ export const ModelingMachineProvider = ({
 | 
			
		||||
            return {
 | 
			
		||||
              selectionType: 'completeSelection',
 | 
			
		||||
              selection,
 | 
			
		||||
              updatedPathToNode,
 | 
			
		||||
              updatedSketchEntryNodePath,
 | 
			
		||||
              updatedSketchNodePaths,
 | 
			
		||||
              updatedPlaneNodePath,
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        ),
 | 
			
		||||
@ -1108,9 +1254,11 @@ export const ModelingMachineProvider = ({
 | 
			
		||||
            let result: {
 | 
			
		||||
              modifiedAst: Node<Program>
 | 
			
		||||
              pathToReplaced: PathToNode | null
 | 
			
		||||
              exprInsertIndex: number
 | 
			
		||||
            } = {
 | 
			
		||||
              modifiedAst: parsed,
 | 
			
		||||
              pathToReplaced: null,
 | 
			
		||||
              exprInsertIndex: -1,
 | 
			
		||||
            }
 | 
			
		||||
            // If the user provided a constant name,
 | 
			
		||||
            // we need to insert the named constant
 | 
			
		||||
@ -1140,6 +1288,7 @@ export const ModelingMachineProvider = ({
 | 
			
		||||
              result = {
 | 
			
		||||
                modifiedAst: parseResultAfterInsertion.program,
 | 
			
		||||
                pathToReplaced: astAfterReplacement.pathToReplaced,
 | 
			
		||||
                exprInsertIndex: astAfterReplacement.exprInsertIndex,
 | 
			
		||||
              }
 | 
			
		||||
            } else if ('valueText' in data.namedValue) {
 | 
			
		||||
              // If they didn't provide a constant name,
 | 
			
		||||
@ -1170,10 +1319,22 @@ export const ModelingMachineProvider = ({
 | 
			
		||||
            parsed = parsed as Node<Program>
 | 
			
		||||
            if (!result.pathToReplaced)
 | 
			
		||||
              return Promise.reject(new Error('No path to replaced node'))
 | 
			
		||||
            const {
 | 
			
		||||
              updatedSketchEntryNodePath,
 | 
			
		||||
              updatedSketchNodePaths,
 | 
			
		||||
              updatedPlaneNodePath,
 | 
			
		||||
            } = updateSketchDetailsNodePaths({
 | 
			
		||||
              sketchEntryNodePath: sketchDetails.sketchEntryNodePath,
 | 
			
		||||
              sketchNodePaths: sketchDetails.sketchNodePaths,
 | 
			
		||||
              planeNodePath: sketchDetails.planeNodePath,
 | 
			
		||||
              exprInsertIndex: result.exprInsertIndex,
 | 
			
		||||
            })
 | 
			
		||||
 | 
			
		||||
            const updatedAst =
 | 
			
		||||
              await sceneEntitiesManager.updateAstAndRejigSketch(
 | 
			
		||||
                result.pathToReplaced || [],
 | 
			
		||||
                updatedSketchEntryNodePath,
 | 
			
		||||
                updatedSketchNodePaths,
 | 
			
		||||
                updatedPlaneNodePath,
 | 
			
		||||
                parsed,
 | 
			
		||||
                sketchDetails.zAxis,
 | 
			
		||||
                sketchDetails.yAxis,
 | 
			
		||||
@ -1194,7 +1355,140 @@ export const ModelingMachineProvider = ({
 | 
			
		||||
            return {
 | 
			
		||||
              selectionType: 'completeSelection',
 | 
			
		||||
              selection,
 | 
			
		||||
              updatedPathToNode: result.pathToReplaced,
 | 
			
		||||
              updatedSketchEntryNodePath,
 | 
			
		||||
              updatedSketchNodePaths,
 | 
			
		||||
              updatedPlaneNodePath,
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        ),
 | 
			
		||||
        'set-up-draft-circle': fromPromise(
 | 
			
		||||
          async ({ input: { sketchDetails, data } }) => {
 | 
			
		||||
            if (!sketchDetails || !data)
 | 
			
		||||
              return reject('No sketch details or data')
 | 
			
		||||
            await sceneEntitiesManager.tearDownSketch({ removeAxis: false })
 | 
			
		||||
 | 
			
		||||
            const result = await sceneEntitiesManager.setupDraftCircle(
 | 
			
		||||
              sketchDetails.sketchEntryNodePath,
 | 
			
		||||
              sketchDetails.sketchNodePaths,
 | 
			
		||||
              sketchDetails.planeNodePath,
 | 
			
		||||
              sketchDetails.zAxis,
 | 
			
		||||
              sketchDetails.yAxis,
 | 
			
		||||
              sketchDetails.origin,
 | 
			
		||||
              data
 | 
			
		||||
            )
 | 
			
		||||
            if (err(result)) return reject(result)
 | 
			
		||||
            await codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
 | 
			
		||||
 | 
			
		||||
            return result
 | 
			
		||||
          }
 | 
			
		||||
        ),
 | 
			
		||||
        'set-up-draft-rectangle': fromPromise(
 | 
			
		||||
          async ({ input: { sketchDetails, data } }) => {
 | 
			
		||||
            if (!sketchDetails || !data)
 | 
			
		||||
              return reject('No sketch details or data')
 | 
			
		||||
            await sceneEntitiesManager.tearDownSketch({ removeAxis: false })
 | 
			
		||||
 | 
			
		||||
            const result = await sceneEntitiesManager.setupDraftRectangle(
 | 
			
		||||
              sketchDetails.sketchEntryNodePath,
 | 
			
		||||
              sketchDetails.sketchNodePaths,
 | 
			
		||||
              sketchDetails.planeNodePath,
 | 
			
		||||
              sketchDetails.zAxis,
 | 
			
		||||
              sketchDetails.yAxis,
 | 
			
		||||
              sketchDetails.origin,
 | 
			
		||||
              data
 | 
			
		||||
            )
 | 
			
		||||
            if (err(result)) return reject(result)
 | 
			
		||||
            await codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
 | 
			
		||||
 | 
			
		||||
            return result
 | 
			
		||||
          }
 | 
			
		||||
        ),
 | 
			
		||||
        'set-up-draft-center-rectangle': fromPromise(
 | 
			
		||||
          async ({ input: { sketchDetails, data } }) => {
 | 
			
		||||
            if (!sketchDetails || !data)
 | 
			
		||||
              return reject('No sketch details or data')
 | 
			
		||||
            await sceneEntitiesManager.tearDownSketch({ removeAxis: false })
 | 
			
		||||
            const result = await sceneEntitiesManager.setupDraftCenterRectangle(
 | 
			
		||||
              sketchDetails.sketchEntryNodePath,
 | 
			
		||||
              sketchDetails.sketchNodePaths,
 | 
			
		||||
              sketchDetails.planeNodePath,
 | 
			
		||||
              sketchDetails.zAxis,
 | 
			
		||||
              sketchDetails.yAxis,
 | 
			
		||||
              sketchDetails.origin,
 | 
			
		||||
              data
 | 
			
		||||
            )
 | 
			
		||||
            if (err(result)) return reject(result)
 | 
			
		||||
            await codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
 | 
			
		||||
 | 
			
		||||
            return result
 | 
			
		||||
          }
 | 
			
		||||
        ),
 | 
			
		||||
        'setup-client-side-sketch-segments': fromPromise(
 | 
			
		||||
          async ({ input: { sketchDetails, selectionRanges } }) => {
 | 
			
		||||
            if (!sketchDetails) return
 | 
			
		||||
            if (!sketchDetails.sketchEntryNodePath.length) return
 | 
			
		||||
            if (Object.keys(sceneEntitiesManager.activeSegments).length > 0) {
 | 
			
		||||
              sceneEntitiesManager.tearDownSketch({ removeAxis: false })
 | 
			
		||||
            }
 | 
			
		||||
            sceneInfra.resetMouseListeners()
 | 
			
		||||
            await sceneEntitiesManager.setupSketch({
 | 
			
		||||
              sketchEntryNodePath: sketchDetails?.sketchEntryNodePath || [],
 | 
			
		||||
              sketchNodePaths: sketchDetails.sketchNodePaths,
 | 
			
		||||
              forward: sketchDetails.zAxis,
 | 
			
		||||
              up: sketchDetails.yAxis,
 | 
			
		||||
              position: sketchDetails.origin,
 | 
			
		||||
              maybeModdedAst: kclManager.ast,
 | 
			
		||||
              selectionRanges,
 | 
			
		||||
            })
 | 
			
		||||
            sceneInfra.resetMouseListeners()
 | 
			
		||||
 | 
			
		||||
            sceneEntitiesManager.setupSketchIdleCallbacks({
 | 
			
		||||
              sketchEntryNodePath: sketchDetails?.sketchEntryNodePath || [],
 | 
			
		||||
              forward: sketchDetails.zAxis,
 | 
			
		||||
              up: sketchDetails.yAxis,
 | 
			
		||||
              position: sketchDetails.origin,
 | 
			
		||||
              sketchNodePaths: sketchDetails.sketchNodePaths,
 | 
			
		||||
              planeNodePath: sketchDetails.planeNodePath,
 | 
			
		||||
            })
 | 
			
		||||
            return undefined
 | 
			
		||||
          }
 | 
			
		||||
        ),
 | 
			
		||||
        'split-sketch-pipe-if-needed': fromPromise(
 | 
			
		||||
          async ({ input: { sketchDetails } }) => {
 | 
			
		||||
            if (!sketchDetails) return reject('No sketch details')
 | 
			
		||||
            const existingSketchInfoNoOp = {
 | 
			
		||||
              updatedEntryNodePath: sketchDetails.sketchEntryNodePath,
 | 
			
		||||
              updatedSketchNodePaths: sketchDetails.sketchNodePaths,
 | 
			
		||||
              updatedPlaneNodePath: sketchDetails.planeNodePath,
 | 
			
		||||
            } as const
 | 
			
		||||
            if (
 | 
			
		||||
              !sketchDetails.sketchNodePaths.length &&
 | 
			
		||||
              sketchDetails.planeNodePath.length
 | 
			
		||||
            ) {
 | 
			
		||||
              // new sketch, no profiles yet
 | 
			
		||||
              return existingSketchInfoNoOp
 | 
			
		||||
            }
 | 
			
		||||
            const doesNeedSplitting = doesSketchPipeNeedSplitting(
 | 
			
		||||
              kclManager.ast,
 | 
			
		||||
              sketchDetails.sketchEntryNodePath
 | 
			
		||||
            )
 | 
			
		||||
            if (err(doesNeedSplitting)) return reject(doesNeedSplitting)
 | 
			
		||||
            if (!doesNeedSplitting) return existingSketchInfoNoOp
 | 
			
		||||
 | 
			
		||||
            const splitResult = splitPipedProfile(
 | 
			
		||||
              kclManager.ast,
 | 
			
		||||
              sketchDetails.sketchEntryNodePath
 | 
			
		||||
            )
 | 
			
		||||
            if (err(splitResult)) return reject(splitResult)
 | 
			
		||||
 | 
			
		||||
            await kclManager.executeAstMock(splitResult.modifiedAst)
 | 
			
		||||
            await codeManager.updateEditorWithAstAndWriteToFile(
 | 
			
		||||
              splitResult.modifiedAst
 | 
			
		||||
            )
 | 
			
		||||
            return {
 | 
			
		||||
              updatedEntryNodePath: splitResult.pathToProfile,
 | 
			
		||||
              updatedSketchNodePaths: [splitResult.pathToProfile],
 | 
			
		||||
              updatedPlaneNodePath: sketchDetails.planeNodePath,
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        ),
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,12 @@ import { SVGProps } from 'react'
 | 
			
		||||
 | 
			
		||||
export const Spinner = (props: SVGProps<SVGSVGElement>) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <svg viewBox="0 0 10 10" className={'w-8 h-8'} {...props}>
 | 
			
		||||
    <svg
 | 
			
		||||
      data-testid="spinner"
 | 
			
		||||
      viewBox="0 0 10 10"
 | 
			
		||||
      className={'w-8 h-8'}
 | 
			
		||||
      {...props}
 | 
			
		||||
    >
 | 
			
		||||
      <circle
 | 
			
		||||
        cx="5"
 | 
			
		||||
        cy="5"
 | 
			
		||||
 | 
			
		||||
@ -136,6 +136,7 @@ export async function applyConstraintIntersect({
 | 
			
		||||
}): Promise<{
 | 
			
		||||
  modifiedAst: Node<Program>
 | 
			
		||||
  pathToNodeMap: PathToNodeMap
 | 
			
		||||
  exprInsertIndex: number
 | 
			
		||||
}> {
 | 
			
		||||
  const info = intersectInfo({
 | 
			
		||||
    selectionRanges,
 | 
			
		||||
@ -174,6 +175,7 @@ export async function applyConstraintIntersect({
 | 
			
		||||
    return {
 | 
			
		||||
      modifiedAst,
 | 
			
		||||
      pathToNodeMap,
 | 
			
		||||
      exprInsertIndex: -1,
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  // transform again but forcing certain values
 | 
			
		||||
@ -192,6 +194,7 @@ export async function applyConstraintIntersect({
 | 
			
		||||
  const { modifiedAst: _modifiedAst, pathToNodeMap: _pathToNodeMap } =
 | 
			
		||||
    transform2
 | 
			
		||||
 | 
			
		||||
  let exprInsertIndex = -1
 | 
			
		||||
  if (variableName) {
 | 
			
		||||
    const newBody = [..._modifiedAst.body]
 | 
			
		||||
    newBody.splice(
 | 
			
		||||
@ -204,9 +207,11 @@ export async function applyConstraintIntersect({
 | 
			
		||||
      const index = pathToNode.findIndex((a) => a[0] === 'body') + 1
 | 
			
		||||
      pathToNode[index][0] = Number(pathToNode[index][0]) + 1
 | 
			
		||||
    })
 | 
			
		||||
    exprInsertIndex = newVariableInsertIndex
 | 
			
		||||
  }
 | 
			
		||||
  return {
 | 
			
		||||
    modifiedAst: _modifiedAst,
 | 
			
		||||
    pathToNodeMap: _pathToNodeMap,
 | 
			
		||||
    exprInsertIndex,
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -28,7 +28,7 @@ export function removeConstrainingValuesInfo({
 | 
			
		||||
  | Error {
 | 
			
		||||
  const _nodes = selectionRanges.graphSelections.map(({ codeRef }) => {
 | 
			
		||||
    const tmp = getNodeFromPath<Expr>(kclManager.ast, codeRef.pathToNode)
 | 
			
		||||
    if (err(tmp)) return tmp
 | 
			
		||||
    if (tmp instanceof Error) return tmp
 | 
			
		||||
    return tmp.node
 | 
			
		||||
  })
 | 
			
		||||
  const _err1 = _nodes.find(err)
 | 
			
		||||
 | 
			
		||||
@ -93,6 +93,7 @@ export async function applyConstraintAbsDistance({
 | 
			
		||||
}): Promise<{
 | 
			
		||||
  modifiedAst: Program
 | 
			
		||||
  pathToNodeMap: PathToNodeMap
 | 
			
		||||
  exprInsertIndex: number
 | 
			
		||||
}> {
 | 
			
		||||
  const info = absDistanceInfo({
 | 
			
		||||
    selectionRanges,
 | 
			
		||||
@ -132,6 +133,7 @@ export async function applyConstraintAbsDistance({
 | 
			
		||||
  if (err(transform2)) return Promise.reject(transform2)
 | 
			
		||||
  const { modifiedAst: _modifiedAst, pathToNodeMap } = transform2
 | 
			
		||||
 | 
			
		||||
  let exprInsertIndex = -1
 | 
			
		||||
  if (variableName) {
 | 
			
		||||
    const newBody = [..._modifiedAst.body]
 | 
			
		||||
    newBody.splice(
 | 
			
		||||
@ -144,8 +146,9 @@ export async function applyConstraintAbsDistance({
 | 
			
		||||
      const index = pathToNode.findIndex((a) => a[0] === 'body') + 1
 | 
			
		||||
      pathToNode[index][0] = Number(pathToNode[index][0]) + 1
 | 
			
		||||
    })
 | 
			
		||||
    exprInsertIndex = newVariableInsertIndex
 | 
			
		||||
  }
 | 
			
		||||
  return { modifiedAst: _modifiedAst, pathToNodeMap }
 | 
			
		||||
  return { modifiedAst: _modifiedAst, pathToNodeMap, exprInsertIndex }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function applyConstraintAxisAlign({
 | 
			
		||||
 | 
			
		||||
@ -86,6 +86,7 @@ export async function applyConstraintAngleBetween({
 | 
			
		||||
}): Promise<{
 | 
			
		||||
  modifiedAst: Program
 | 
			
		||||
  pathToNodeMap: PathToNodeMap
 | 
			
		||||
  exprInsertIndex: number
 | 
			
		||||
}> {
 | 
			
		||||
  const info = angleBetweenInfo({ selectionRanges })
 | 
			
		||||
  if (err(info)) return Promise.reject(info)
 | 
			
		||||
@ -122,6 +123,7 @@ export async function applyConstraintAngleBetween({
 | 
			
		||||
    return {
 | 
			
		||||
      modifiedAst,
 | 
			
		||||
      pathToNodeMap,
 | 
			
		||||
      exprInsertIndex: -1,
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -141,6 +143,7 @@ export async function applyConstraintAngleBetween({
 | 
			
		||||
  const { modifiedAst: _modifiedAst, pathToNodeMap: _pathToNodeMap } =
 | 
			
		||||
    transformed2
 | 
			
		||||
 | 
			
		||||
  let exprInsertIndex = -1
 | 
			
		||||
  if (variableName) {
 | 
			
		||||
    const newBody = [..._modifiedAst.body]
 | 
			
		||||
    newBody.splice(
 | 
			
		||||
@ -153,9 +156,11 @@ export async function applyConstraintAngleBetween({
 | 
			
		||||
      const index = pathToNode.findIndex((a) => a[0] === 'body') + 1
 | 
			
		||||
      pathToNode[index][0] = Number(pathToNode[index][0]) + 1
 | 
			
		||||
    })
 | 
			
		||||
    exprInsertIndex = newVariableInsertIndex
 | 
			
		||||
  }
 | 
			
		||||
  return {
 | 
			
		||||
    modifiedAst: _modifiedAst,
 | 
			
		||||
    pathToNodeMap: _pathToNodeMap,
 | 
			
		||||
    exprInsertIndex,
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -87,15 +87,13 @@ export function horzVertDistanceInfo({
 | 
			
		||||
export async function applyConstraintHorzVertDistance({
 | 
			
		||||
  selectionRanges,
 | 
			
		||||
  constraint,
 | 
			
		||||
  // TODO align will always be false (covered by synconous applyConstraintHorzVertAlign), remove it
 | 
			
		||||
  isAlign = false,
 | 
			
		||||
}: {
 | 
			
		||||
  selectionRanges: Selections
 | 
			
		||||
  constraint: 'setHorzDistance' | 'setVertDistance'
 | 
			
		||||
  isAlign?: false
 | 
			
		||||
}): Promise<{
 | 
			
		||||
  modifiedAst: Program
 | 
			
		||||
  pathToNodeMap: PathToNodeMap
 | 
			
		||||
  exprInsertIndex: number
 | 
			
		||||
}> {
 | 
			
		||||
  const info = horzVertDistanceInfo({
 | 
			
		||||
    selectionRanges: selectionRanges,
 | 
			
		||||
@ -133,13 +131,12 @@ export async function applyConstraintHorzVertDistance({
 | 
			
		||||
    return {
 | 
			
		||||
      modifiedAst,
 | 
			
		||||
      pathToNodeMap,
 | 
			
		||||
      exprInsertIndex: -1,
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    if (!isExprBinaryPart(valueNode))
 | 
			
		||||
      return Promise.reject('Invalid valueNode, is not a BinaryPart')
 | 
			
		||||
    let finalValue = isAlign
 | 
			
		||||
      ? createLiteral(0)
 | 
			
		||||
      : removeDoubleNegatives(valueNode, sign, variableName)
 | 
			
		||||
    let finalValue = removeDoubleNegatives(valueNode, sign, variableName)
 | 
			
		||||
    // transform again but forcing certain values
 | 
			
		||||
    const transformed = transformSecondarySketchLinesTagFirst({
 | 
			
		||||
      ast: kclManager.ast,
 | 
			
		||||
@ -152,6 +149,7 @@ export async function applyConstraintHorzVertDistance({
 | 
			
		||||
 | 
			
		||||
    if (err(transformed)) return Promise.reject(transformed)
 | 
			
		||||
    const { modifiedAst: _modifiedAst, pathToNodeMap } = transformed
 | 
			
		||||
    let exprInsertIndex = -1
 | 
			
		||||
    if (variableName) {
 | 
			
		||||
      const newBody = [..._modifiedAst.body]
 | 
			
		||||
      newBody.splice(
 | 
			
		||||
@ -164,10 +162,12 @@ export async function applyConstraintHorzVertDistance({
 | 
			
		||||
        const index = pathToNode.findIndex((a) => a[0] === 'body') + 1
 | 
			
		||||
        pathToNode[index][0] = Number(pathToNode[index][0]) + 1
 | 
			
		||||
      })
 | 
			
		||||
      exprInsertIndex = newVariableInsertIndex
 | 
			
		||||
    }
 | 
			
		||||
    return {
 | 
			
		||||
      modifiedAst: _modifiedAst,
 | 
			
		||||
      pathToNodeMap,
 | 
			
		||||
      exprInsertIndex,
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -70,10 +70,14 @@ export async function applyConstraintLength({
 | 
			
		||||
}: {
 | 
			
		||||
  length: KclCommandValue
 | 
			
		||||
  selectionRanges: Selections
 | 
			
		||||
}) {
 | 
			
		||||
}): Promise<{
 | 
			
		||||
  modifiedAst: Program
 | 
			
		||||
  pathToNodeMap: PathToNodeMap
 | 
			
		||||
  exprInsertIndex: number
 | 
			
		||||
}> {
 | 
			
		||||
  const ast = kclManager.ast
 | 
			
		||||
  const angleLength = angleLengthInfo({ selectionRanges })
 | 
			
		||||
  if (err(angleLength)) return angleLength
 | 
			
		||||
  if (err(angleLength)) return Promise.reject(angleLength)
 | 
			
		||||
  const { transforms } = angleLength
 | 
			
		||||
 | 
			
		||||
  let distanceExpression: Expr = length.valueAst
 | 
			
		||||
@ -94,7 +98,7 @@ export async function applyConstraintLength({
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!isExprBinaryPart(distanceExpression)) {
 | 
			
		||||
    return new Error('Invalid valueNode, is not a BinaryPart')
 | 
			
		||||
    return Promise.reject('Invalid valueNode, is not a BinaryPart')
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const retval = transformAstSketchLines({
 | 
			
		||||
@ -112,6 +116,12 @@ export async function applyConstraintLength({
 | 
			
		||||
  return {
 | 
			
		||||
    modifiedAst: _modifiedAst,
 | 
			
		||||
    pathToNodeMap,
 | 
			
		||||
    exprInsertIndex:
 | 
			
		||||
      'variableName' in length &&
 | 
			
		||||
      length.variableName &&
 | 
			
		||||
      length.insertIndex !== undefined
 | 
			
		||||
        ? length.insertIndex
 | 
			
		||||
        : -1,
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -124,6 +134,7 @@ export async function applyConstraintAngleLength({
 | 
			
		||||
}): Promise<{
 | 
			
		||||
  modifiedAst: Program
 | 
			
		||||
  pathToNodeMap: PathToNodeMap
 | 
			
		||||
  exprInsertIndex: number
 | 
			
		||||
}> {
 | 
			
		||||
  const angleLength = angleLengthInfo({ selectionRanges, angleOrLength })
 | 
			
		||||
  if (err(angleLength)) return Promise.reject(angleLength)
 | 
			
		||||
@ -208,5 +219,6 @@ export async function applyConstraintAngleLength({
 | 
			
		||||
  return {
 | 
			
		||||
    modifiedAst: _modifiedAst,
 | 
			
		||||
    pathToNodeMap,
 | 
			
		||||
    exprInsertIndex: variableName ? newVariableInsertIndex : -1,
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -359,10 +359,8 @@ export class KclManager {
 | 
			
		||||
    // updateArtifactGraph relies on updated executeState/programMemory
 | 
			
		||||
    await this.engineCommandManager.updateArtifactGraph(this.ast)
 | 
			
		||||
    this._executeCallback()
 | 
			
		||||
    if (!isInterrupted) {
 | 
			
		||||
    if (!isInterrupted)
 | 
			
		||||
      sceneInfra.modelingSend({ type: 'code edit during sketch' })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.engineCommandManager.addCommandLog({
 | 
			
		||||
      type: 'execution-done',
 | 
			
		||||
      data: null,
 | 
			
		||||
@ -404,6 +402,7 @@ export class KclManager {
 | 
			
		||||
 | 
			
		||||
    this._logs = logs
 | 
			
		||||
    this.addDiagnostics(kclErrorsToDiagnostics(errors))
 | 
			
		||||
 | 
			
		||||
    this._execState = execState
 | 
			
		||||
    this._programMemory = execState.memory
 | 
			
		||||
    if (!errors.length) {
 | 
			
		||||
@ -415,7 +414,7 @@ export class KclManager {
 | 
			
		||||
    // problem this solves, but either way we should strive to remove it.
 | 
			
		||||
    Array.from(this.engineCommandManager.artifactGraph).forEach(
 | 
			
		||||
      ([commandId, artifact]) => {
 | 
			
		||||
        if (!('codeRef' in artifact)) return
 | 
			
		||||
        if (!('codeRef' in artifact && artifact.codeRef)) return
 | 
			
		||||
        const _node1 = getNodeFromPath<Node<CallExpression>>(
 | 
			
		||||
          this.ast,
 | 
			
		||||
          artifact.codeRef.pathToNode,
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
import {
 | 
			
		||||
  Program,
 | 
			
		||||
  _executor,
 | 
			
		||||
  executor,
 | 
			
		||||
  ProgramMemory,
 | 
			
		||||
  kclLint,
 | 
			
		||||
  emptyExecState,
 | 
			
		||||
@ -64,10 +64,9 @@ export async function executeAst({
 | 
			
		||||
  try {
 | 
			
		||||
    const execState = await (programMemoryOverride
 | 
			
		||||
      ? enginelessExecutor(ast, programMemoryOverride)
 | 
			
		||||
      : _executor(ast, engineCommandManager))
 | 
			
		||||
      : executor(ast, engineCommandManager))
 | 
			
		||||
 | 
			
		||||
    await engineCommandManager.waitForAllCommands()
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      logs: [],
 | 
			
		||||
      errors: [],
 | 
			
		||||
 | 
			
		||||
@ -16,6 +16,7 @@ import {
 | 
			
		||||
  deleteSegmentFromPipeExpression,
 | 
			
		||||
  removeSingleConstraintInfo,
 | 
			
		||||
  deleteFromSelection,
 | 
			
		||||
  splitPipedProfile,
 | 
			
		||||
} from './modifyAst'
 | 
			
		||||
import { enginelessExecutor } from '../lib/testHelpers'
 | 
			
		||||
import { findUsesOfTagInPipe, getNodePathFromSourceRange } from './queryAst'
 | 
			
		||||
@ -918,3 +919,63 @@ sketch002 = startSketchOn({
 | 
			
		||||
    }
 | 
			
		||||
  )
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
describe('Testing splitPipedProfile', () => {
 | 
			
		||||
  it('should split the pipe expression correctly', () => {
 | 
			
		||||
    const codeBefore = `part001 = startSketchOn('XZ')
 | 
			
		||||
  |> startProfileAt([1, 2], %)
 | 
			
		||||
  |> line([3, 4], %)
 | 
			
		||||
  |> line([5, 6], %)
 | 
			
		||||
  |> close(%)
 | 
			
		||||
extrude001 = extrude(5, part001)
 | 
			
		||||
    `
 | 
			
		||||
 | 
			
		||||
    const expectedCodeAfter = `sketch001 = startSketchOn('XZ')
 | 
			
		||||
part001 = startProfileAt([1, 2], sketch001)
 | 
			
		||||
  |> line([3, 4], %)
 | 
			
		||||
  |> line([5, 6], %)
 | 
			
		||||
  |> close(%)
 | 
			
		||||
extrude001 = extrude(5, part001)
 | 
			
		||||
    `
 | 
			
		||||
 | 
			
		||||
    const ast = assertParse(codeBefore)
 | 
			
		||||
 | 
			
		||||
    const codeOfInterest = `startSketchOn('XZ')`
 | 
			
		||||
    const range: [number, number, boolean] = [
 | 
			
		||||
      codeBefore.indexOf(codeOfInterest),
 | 
			
		||||
      codeBefore.indexOf(codeOfInterest) + codeOfInterest.length,
 | 
			
		||||
      true,
 | 
			
		||||
    ]
 | 
			
		||||
    const pathToPipe = getNodePathFromSourceRange(ast, range)
 | 
			
		||||
 | 
			
		||||
    const result = splitPipedProfile(ast, pathToPipe)
 | 
			
		||||
 | 
			
		||||
    if (err(result)) throw result
 | 
			
		||||
 | 
			
		||||
    const newCode = recast(result.modifiedAst)
 | 
			
		||||
    if (err(newCode)) throw newCode
 | 
			
		||||
    expect(newCode.trim()).toBe(expectedCodeAfter.trim())
 | 
			
		||||
  })
 | 
			
		||||
  it('should return error for already split pipe', () => {
 | 
			
		||||
    const codeBefore = `sketch001 = startSketchOn('XZ')
 | 
			
		||||
part001 = startProfileAt([1, 2], sketch001)
 | 
			
		||||
  |> line([3, 4], %)
 | 
			
		||||
  |> line([5, 6], %)
 | 
			
		||||
  |> close(%)
 | 
			
		||||
extrude001 = extrude(5, part001)
 | 
			
		||||
    `
 | 
			
		||||
 | 
			
		||||
    const ast = assertParse(codeBefore)
 | 
			
		||||
 | 
			
		||||
    const codeOfInterest = `startProfileAt([1, 2], sketch001)`
 | 
			
		||||
    const range: [number, number, boolean] = [
 | 
			
		||||
      codeBefore.indexOf(codeOfInterest),
 | 
			
		||||
      codeBefore.indexOf(codeOfInterest) + codeOfInterest.length,
 | 
			
		||||
      true,
 | 
			
		||||
    ]
 | 
			
		||||
    const pathToPipe = getNodePathFromSourceRange(ast, range)
 | 
			
		||||
 | 
			
		||||
    const result = splitPipedProfile(ast, pathToPipe)
 | 
			
		||||
    expect(result instanceof Error).toBe(true)
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
@ -29,6 +29,8 @@ import {
 | 
			
		||||
  getNodePathFromSourceRange,
 | 
			
		||||
  isNodeSafeToReplace,
 | 
			
		||||
  traverse,
 | 
			
		||||
  getBodyIndex,
 | 
			
		||||
  isCallExprWithName,
 | 
			
		||||
} from './queryAst'
 | 
			
		||||
import { addTagForSketchOnFace, getConstraintInfo } from './std/sketch'
 | 
			
		||||
import {
 | 
			
		||||
@ -46,6 +48,7 @@ import { Models } from '@kittycad/lib'
 | 
			
		||||
import { ExtrudeFacePlane } from 'machines/modelingMachine'
 | 
			
		||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
 | 
			
		||||
import { KclExpressionWithVariable } from 'lib/commandTypes'
 | 
			
		||||
import { Artifact, getPathsFromArtifact } from './std/artifactGraph'
 | 
			
		||||
 | 
			
		||||
export function startSketchOnDefault(
 | 
			
		||||
  node: Node<Program>,
 | 
			
		||||
@ -78,41 +81,54 @@ export function startSketchOnDefault(
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function addStartProfileAt(
 | 
			
		||||
export function insertNewStartProfileAt(
 | 
			
		||||
  node: Node<Program>,
 | 
			
		||||
  pathToNode: PathToNode,
 | 
			
		||||
  at: [number, number]
 | 
			
		||||
): { modifiedAst: Node<Program>; pathToNode: PathToNode } | Error {
 | 
			
		||||
  const _node1 = getNodeFromPath<VariableDeclaration>(
 | 
			
		||||
  sketchEntryNodePath: PathToNode,
 | 
			
		||||
  sketchNodePaths: PathToNode[],
 | 
			
		||||
  planeNodePath: PathToNode,
 | 
			
		||||
  at: [number, number],
 | 
			
		||||
  insertType: 'start' | 'end' = 'end'
 | 
			
		||||
):
 | 
			
		||||
  | {
 | 
			
		||||
      modifiedAst: Node<Program>
 | 
			
		||||
      updatedSketchNodePaths: PathToNode[]
 | 
			
		||||
      updatedEntryNodePath: PathToNode
 | 
			
		||||
    }
 | 
			
		||||
  | Error {
 | 
			
		||||
  const varDec = getNodeFromPath<VariableDeclarator>(
 | 
			
		||||
    node,
 | 
			
		||||
    pathToNode,
 | 
			
		||||
    'VariableDeclaration'
 | 
			
		||||
    planeNodePath,
 | 
			
		||||
    'VariableDeclarator'
 | 
			
		||||
  )
 | 
			
		||||
  if (err(_node1)) return _node1
 | 
			
		||||
  const variableDeclaration = _node1.node
 | 
			
		||||
  if (variableDeclaration.type !== 'VariableDeclaration') {
 | 
			
		||||
    return new Error('variableDeclaration.init.type !== PipeExpression')
 | 
			
		||||
  }
 | 
			
		||||
  const _node = { ...node }
 | 
			
		||||
  const init = variableDeclaration.declaration.init
 | 
			
		||||
  const startProfileAt = createCallExpressionStdLib('startProfileAt', [
 | 
			
		||||
    createArrayExpression([
 | 
			
		||||
      createLiteral(roundOff(at[0])),
 | 
			
		||||
      createLiteral(roundOff(at[1])),
 | 
			
		||||
    ]),
 | 
			
		||||
    createPipeSubstitution(),
 | 
			
		||||
  ])
 | 
			
		||||
  if (init.type === 'PipeExpression') {
 | 
			
		||||
    init.body.splice(1, 0, startProfileAt)
 | 
			
		||||
  } else {
 | 
			
		||||
    variableDeclaration.declaration.init = createPipeExpression([
 | 
			
		||||
      init,
 | 
			
		||||
      startProfileAt,
 | 
			
		||||
  if (err(varDec)) return varDec
 | 
			
		||||
  if (varDec.node.type !== 'VariableDeclarator') return new Error('not a var')
 | 
			
		||||
 | 
			
		||||
  const newExpression = createVariableDeclaration(
 | 
			
		||||
    findUniqueName(node, 'profile'),
 | 
			
		||||
    createCallExpressionStdLib('startProfileAt', [
 | 
			
		||||
      createArrayExpression([
 | 
			
		||||
        createLiteral(roundOff(at[0])),
 | 
			
		||||
        createLiteral(roundOff(at[1])),
 | 
			
		||||
      ]),
 | 
			
		||||
      createIdentifier(varDec.node.id.name),
 | 
			
		||||
    ])
 | 
			
		||||
  }
 | 
			
		||||
  )
 | 
			
		||||
  const insertIndex = getInsertIndex(sketchNodePaths, planeNodePath, insertType)
 | 
			
		||||
 | 
			
		||||
  const _node = structuredClone(node)
 | 
			
		||||
  // TODO the rest of this function will not be robust to work for sketches defined within a function declaration
 | 
			
		||||
  _node.body.splice(insertIndex, 0, newExpression)
 | 
			
		||||
 | 
			
		||||
  const { updatedEntryNodePath, updatedSketchNodePaths } =
 | 
			
		||||
    updateSketchNodePathsWithInsertIndex({
 | 
			
		||||
      insertIndex,
 | 
			
		||||
      insertType,
 | 
			
		||||
      sketchNodePaths,
 | 
			
		||||
    })
 | 
			
		||||
  return {
 | 
			
		||||
    modifiedAst: _node,
 | 
			
		||||
    pathToNode,
 | 
			
		||||
    updatedSketchNodePaths,
 | 
			
		||||
    updatedEntryNodePath,
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -253,7 +269,7 @@ export function mutateObjExpProp(
 | 
			
		||||
export function extrudeSketch(
 | 
			
		||||
  node: Node<Program>,
 | 
			
		||||
  pathToNode: PathToNode,
 | 
			
		||||
  shouldPipe = false,
 | 
			
		||||
  artifact?: Artifact,
 | 
			
		||||
  distance: Expr = createLiteral(4)
 | 
			
		||||
):
 | 
			
		||||
  | {
 | 
			
		||||
@ -262,10 +278,14 @@ export function extrudeSketch(
 | 
			
		||||
      pathToExtrudeArg: PathToNode
 | 
			
		||||
    }
 | 
			
		||||
  | Error {
 | 
			
		||||
  const orderedSketchNodePaths = getPathsFromArtifact({
 | 
			
		||||
    artifact: artifact,
 | 
			
		||||
    sketchPathToNode: pathToNode,
 | 
			
		||||
  })
 | 
			
		||||
  if (err(orderedSketchNodePaths)) return orderedSketchNodePaths
 | 
			
		||||
  const _node = structuredClone(node)
 | 
			
		||||
  const _node1 = getNodeFromPath(_node, pathToNode)
 | 
			
		||||
  if (err(_node1)) return _node1
 | 
			
		||||
  const { node: sketchExpression } = _node1
 | 
			
		||||
 | 
			
		||||
  // determine if sketchExpression is in a pipeExpression or not
 | 
			
		||||
  const _node2 = getNodeFromPath<PipeExpression>(
 | 
			
		||||
@ -274,9 +294,6 @@ export function extrudeSketch(
 | 
			
		||||
    'PipeExpression'
 | 
			
		||||
  )
 | 
			
		||||
  if (err(_node2)) return _node2
 | 
			
		||||
  const { node: pipeExpression } = _node2
 | 
			
		||||
 | 
			
		||||
  const isInPipeExpression = pipeExpression.type === 'PipeExpression'
 | 
			
		||||
 | 
			
		||||
  const _node3 = getNodeFromPath<VariableDeclarator>(
 | 
			
		||||
    _node,
 | 
			
		||||
@ -284,49 +301,23 @@ export function extrudeSketch(
 | 
			
		||||
    'VariableDeclarator'
 | 
			
		||||
  )
 | 
			
		||||
  if (err(_node3)) return _node3
 | 
			
		||||
  const { node: variableDeclarator, shallowPath: pathToDecleration } = _node3
 | 
			
		||||
  const { node: variableDeclarator } = _node3
 | 
			
		||||
 | 
			
		||||
  const extrudeCall = createCallExpressionStdLib('extrude', [
 | 
			
		||||
    distance,
 | 
			
		||||
    shouldPipe
 | 
			
		||||
      ? createPipeSubstitution()
 | 
			
		||||
      : createIdentifier(variableDeclarator.id.name),
 | 
			
		||||
    createIdentifier(variableDeclarator.id.name),
 | 
			
		||||
  ])
 | 
			
		||||
 | 
			
		||||
  if (shouldPipe) {
 | 
			
		||||
    const pipeChain = createPipeExpression(
 | 
			
		||||
      isInPipeExpression
 | 
			
		||||
        ? [...pipeExpression.body, extrudeCall]
 | 
			
		||||
        : [sketchExpression as any, extrudeCall]
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    variableDeclarator.init = pipeChain
 | 
			
		||||
    const pathToExtrudeArg: PathToNode = [
 | 
			
		||||
      ...pathToDecleration,
 | 
			
		||||
      ['init', 'VariableDeclarator'],
 | 
			
		||||
      ['body', ''],
 | 
			
		||||
      [pipeChain.body.length - 1, 'index'],
 | 
			
		||||
      ['arguments', 'CallExpression'],
 | 
			
		||||
      [0, 'index'],
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      modifiedAst: _node,
 | 
			
		||||
      pathToNode,
 | 
			
		||||
      pathToExtrudeArg,
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // We're not creating a pipe expression,
 | 
			
		||||
  // but rather a separate constant for the extrusion
 | 
			
		||||
  const name = findUniqueName(node, KCL_DEFAULT_CONSTANT_PREFIXES.EXTRUDE)
 | 
			
		||||
  const VariableDeclaration = createVariableDeclaration(name, extrudeCall)
 | 
			
		||||
 | 
			
		||||
  const sketchIndexInPathToNode =
 | 
			
		||||
    pathToDecleration.findIndex((a) => a[0] === 'body') + 1
 | 
			
		||||
  const sketchIndexInBody = pathToDecleration[
 | 
			
		||||
    sketchIndexInPathToNode
 | 
			
		||||
  ][0] as number
 | 
			
		||||
  const lastSketchNodePath =
 | 
			
		||||
    orderedSketchNodePaths[orderedSketchNodePaths.length - 1]
 | 
			
		||||
 | 
			
		||||
  console.log('lastSketchNodePath', lastSketchNodePath, orderedSketchNodePaths)
 | 
			
		||||
  const sketchIndexInBody = Number(lastSketchNodePath[1][0])
 | 
			
		||||
  _node.body.splice(sketchIndexInBody + 1, 0, VariableDeclaration)
 | 
			
		||||
 | 
			
		||||
  const pathToExtrudeArg: PathToNode = [
 | 
			
		||||
@ -1295,7 +1286,8 @@ export async function deleteFromSelection(
 | 
			
		||||
    const pipeBody = varDec.node.init.body
 | 
			
		||||
    if (
 | 
			
		||||
      pipeBody[0].type === 'CallExpression' &&
 | 
			
		||||
      pipeBody[0].callee.name === 'startSketchOn'
 | 
			
		||||
      (pipeBody[0].callee.name === 'startSketchOn' ||
 | 
			
		||||
        pipeBody[0].callee.name === 'startProfileAt')
 | 
			
		||||
    ) {
 | 
			
		||||
      // remove varDec
 | 
			
		||||
      const varDecIndex = varDec.shallowPath[1][0] as number
 | 
			
		||||
@ -1310,3 +1302,149 @@ export async function deleteFromSelection(
 | 
			
		||||
const nonCodeMetaEmpty = () => {
 | 
			
		||||
  return { nonCodeNodes: {}, startNodes: [], start: 0, end: 0 }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getInsertIndex(
 | 
			
		||||
  sketchNodePaths: PathToNode[],
 | 
			
		||||
  planeNodePath: PathToNode,
 | 
			
		||||
  insertType: 'start' | 'end'
 | 
			
		||||
) {
 | 
			
		||||
  let minIndex = 0
 | 
			
		||||
  let maxIndex = 0
 | 
			
		||||
  for (const path of sketchNodePaths) {
 | 
			
		||||
    const index = Number(path[1][0])
 | 
			
		||||
    if (index < minIndex) minIndex = index
 | 
			
		||||
    if (index > maxIndex) maxIndex = index
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const insertIndex = !sketchNodePaths.length
 | 
			
		||||
    ? Number(planeNodePath[1][0]) + 1
 | 
			
		||||
    : insertType === 'start'
 | 
			
		||||
    ? minIndex
 | 
			
		||||
    : maxIndex + 1
 | 
			
		||||
  return insertIndex
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function updateSketchNodePathsWithInsertIndex({
 | 
			
		||||
  insertIndex,
 | 
			
		||||
  insertType,
 | 
			
		||||
  sketchNodePaths,
 | 
			
		||||
}: {
 | 
			
		||||
  insertIndex: number
 | 
			
		||||
  insertType: 'start' | 'end'
 | 
			
		||||
  sketchNodePaths: PathToNode[]
 | 
			
		||||
}): {
 | 
			
		||||
  updatedEntryNodePath: PathToNode
 | 
			
		||||
  updatedSketchNodePaths: PathToNode[]
 | 
			
		||||
} {
 | 
			
		||||
  // TODO the rest of this function will not be robust to work for sketches defined within a function declaration
 | 
			
		||||
  const newExpressionPathToNode: PathToNode = [
 | 
			
		||||
    ['body', ''],
 | 
			
		||||
    [insertIndex, 'index'],
 | 
			
		||||
    ['declaration', 'VariableDeclaration'],
 | 
			
		||||
    ['init', 'VariableDeclarator'],
 | 
			
		||||
  ]
 | 
			
		||||
  let updatedSketchNodePaths = structuredClone(sketchNodePaths)
 | 
			
		||||
  if (insertType === 'start') {
 | 
			
		||||
    updatedSketchNodePaths = updatedSketchNodePaths.map((path) => {
 | 
			
		||||
      path[1][0] = Number(path[1][0]) + 1
 | 
			
		||||
      return path
 | 
			
		||||
    })
 | 
			
		||||
    updatedSketchNodePaths.unshift(newExpressionPathToNode)
 | 
			
		||||
  } else {
 | 
			
		||||
    updatedSketchNodePaths.push(newExpressionPathToNode)
 | 
			
		||||
  }
 | 
			
		||||
  return {
 | 
			
		||||
    updatedSketchNodePaths,
 | 
			
		||||
    updatedEntryNodePath: newExpressionPathToNode,
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 
 | 
			
		||||
 * Split the following pipe expression into 
 | 
			
		||||
 * ```ts
 | 
			
		||||
 * part001 = startSketchOn('XZ')
 | 
			
		||||
  |> startProfileAt([1, 2], %)
 | 
			
		||||
  |> line([3, 4], %)
 | 
			
		||||
  |> line([5, 6], %)
 | 
			
		||||
  |> close(%)
 | 
			
		||||
extrude001 = extrude(5, part001)
 | 
			
		||||
```
 | 
			
		||||
into
 | 
			
		||||
```ts
 | 
			
		||||
sketch001 = startSketchOn('XZ')
 | 
			
		||||
part001 = startProfileAt([1, 2], sketch001)
 | 
			
		||||
  |> line([3, 4], %)
 | 
			
		||||
  |> line([5, 6], %)
 | 
			
		||||
  |> close(%)
 | 
			
		||||
extrude001 = extrude(5, part001)
 | 
			
		||||
```
 | 
			
		||||
Notice that the `startSketchOn` is what gets the new variable name, this is so part001 still has the same data as before
 | 
			
		||||
making it safe for later code that uses part001 (the extrude in this example)
 | 
			
		||||
 * 
 | 
			
		||||
 */
 | 
			
		||||
export function splitPipedProfile(
 | 
			
		||||
  ast: Program,
 | 
			
		||||
  pathToPipe: PathToNode
 | 
			
		||||
):
 | 
			
		||||
  | {
 | 
			
		||||
      modifiedAst: Program
 | 
			
		||||
      pathToProfile: PathToNode
 | 
			
		||||
      pathToPlane: PathToNode
 | 
			
		||||
    }
 | 
			
		||||
  | Error {
 | 
			
		||||
  const _ast = structuredClone(ast)
 | 
			
		||||
  const varDec = getNodeFromPath<VariableDeclaration>(
 | 
			
		||||
    _ast,
 | 
			
		||||
    pathToPipe,
 | 
			
		||||
    'VariableDeclaration'
 | 
			
		||||
  )
 | 
			
		||||
  if (err(varDec)) return varDec
 | 
			
		||||
  if (
 | 
			
		||||
    varDec.node.type !== 'VariableDeclaration' ||
 | 
			
		||||
    varDec.node.declaration.init.type !== 'PipeExpression'
 | 
			
		||||
  ) {
 | 
			
		||||
    return new Error('pathToNode does not point to pipe')
 | 
			
		||||
  }
 | 
			
		||||
  const init = varDec.node.declaration.init
 | 
			
		||||
  const firstCall = init.body[0]
 | 
			
		||||
  if (!isCallExprWithName(firstCall, 'startSketchOn'))
 | 
			
		||||
    return new Error('First call is not startSketchOn')
 | 
			
		||||
  const secondCall = init.body[1]
 | 
			
		||||
  if (!isCallExprWithName(secondCall, 'startProfileAt'))
 | 
			
		||||
    return new Error('Second call is not startProfileAt')
 | 
			
		||||
 | 
			
		||||
  const varName = varDec.node.declaration.id.name
 | 
			
		||||
  const newVarName = findUniqueName(_ast, 'sketch')
 | 
			
		||||
  const secondCallArgs = structuredClone(secondCall.arguments)
 | 
			
		||||
  secondCallArgs[1] = createIdentifier(newVarName)
 | 
			
		||||
  const firstCallOfNewPipe = createCallExpression(
 | 
			
		||||
    'startProfileAt',
 | 
			
		||||
    secondCallArgs
 | 
			
		||||
  )
 | 
			
		||||
  const newSketch = createVariableDeclaration(
 | 
			
		||||
    newVarName,
 | 
			
		||||
    varDec.node.declaration.init.body[0]
 | 
			
		||||
  )
 | 
			
		||||
  const newProfile = createVariableDeclaration(
 | 
			
		||||
    varName,
 | 
			
		||||
    varDec.node.declaration.init.body.length <= 2
 | 
			
		||||
      ? firstCallOfNewPipe
 | 
			
		||||
      : createPipeExpression([
 | 
			
		||||
          firstCallOfNewPipe,
 | 
			
		||||
          ...varDec.node.declaration.init.body.slice(2),
 | 
			
		||||
        ])
 | 
			
		||||
  )
 | 
			
		||||
  const index = getBodyIndex(pathToPipe)
 | 
			
		||||
  if (err(index)) return index
 | 
			
		||||
  _ast.body.splice(index, 1, newSketch, newProfile)
 | 
			
		||||
  const pathToPlane = structuredClone(pathToPipe)
 | 
			
		||||
  const pathToProfile = structuredClone(pathToPipe)
 | 
			
		||||
  pathToProfile[1][0] = index + 1
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    modifiedAst: _ast,
 | 
			
		||||
    pathToProfile,
 | 
			
		||||
    pathToPlane,
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -275,7 +275,7 @@ const runModifyAstCloneWithEdgeTreatmentAndTag = async (
 | 
			
		||||
  const selection: Selections = {
 | 
			
		||||
    graphSelections: segmentRanges.map((segmentRange) => {
 | 
			
		||||
      const maybeArtifact = [...artifactGraph].find(([, a]) => {
 | 
			
		||||
        if (!('codeRef' in a)) return false
 | 
			
		||||
        if (!('codeRef' in a && a.codeRef)) return false
 | 
			
		||||
        return isOverlap(a.codeRef.range, segmentRange)
 | 
			
		||||
      })
 | 
			
		||||
      return {
 | 
			
		||||
 | 
			
		||||
@ -5,7 +5,6 @@ import {
 | 
			
		||||
  PathToNode,
 | 
			
		||||
  Expr,
 | 
			
		||||
  CallExpression,
 | 
			
		||||
  PipeExpression,
 | 
			
		||||
  VariableDeclarator,
 | 
			
		||||
} from 'lang/wasm'
 | 
			
		||||
import { Selections } from 'lib/selections'
 | 
			
		||||
@ -15,7 +14,6 @@ import {
 | 
			
		||||
  createCallExpressionStdLib,
 | 
			
		||||
  createObjectExpression,
 | 
			
		||||
  createIdentifier,
 | 
			
		||||
  createPipeExpression,
 | 
			
		||||
  findUniqueName,
 | 
			
		||||
  createVariableDeclaration,
 | 
			
		||||
} from 'lang/modifyAst'
 | 
			
		||||
@ -24,12 +22,13 @@ import {
 | 
			
		||||
  mutateAstWithTagForSketchSegment,
 | 
			
		||||
  getEdgeTagCall,
 | 
			
		||||
} from 'lang/modifyAst/addEdgeTreatment'
 | 
			
		||||
import { Artifact, getPathsFromArtifact } from 'lang/std/artifactGraph'
 | 
			
		||||
export function revolveSketch(
 | 
			
		||||
  ast: Node<Program>,
 | 
			
		||||
  pathToSketchNode: PathToNode,
 | 
			
		||||
  shouldPipe = false,
 | 
			
		||||
  angle: Expr = createLiteral(4),
 | 
			
		||||
  axis: Selections
 | 
			
		||||
  axis: Selections,
 | 
			
		||||
  artifact?: Artifact
 | 
			
		||||
):
 | 
			
		||||
  | {
 | 
			
		||||
      modifiedAst: Node<Program>
 | 
			
		||||
@ -37,6 +36,11 @@ export function revolveSketch(
 | 
			
		||||
      pathToRevolveArg: PathToNode
 | 
			
		||||
    }
 | 
			
		||||
  | Error {
 | 
			
		||||
  const orderedSketchNodePaths = getPathsFromArtifact({
 | 
			
		||||
    artifact: artifact,
 | 
			
		||||
    sketchPathToNode: pathToSketchNode,
 | 
			
		||||
  })
 | 
			
		||||
  if (err(orderedSketchNodePaths)) return orderedSketchNodePaths
 | 
			
		||||
  const clonedAst = structuredClone(ast)
 | 
			
		||||
  const sketchNode = getNodeFromPath(clonedAst, pathToSketchNode)
 | 
			
		||||
  if (err(sketchNode)) return sketchNode
 | 
			
		||||
@ -67,29 +71,13 @@ export function revolveSketch(
 | 
			
		||||
  if (err(tagResult)) return tagResult
 | 
			
		||||
  const { tag } = tagResult
 | 
			
		||||
 | 
			
		||||
  /* Original Code */
 | 
			
		||||
  const { node: sketchExpression } = sketchNode
 | 
			
		||||
 | 
			
		||||
  // determine if sketchExpression is in a pipeExpression or not
 | 
			
		||||
  const sketchPipeExpressionNode = getNodeFromPath<PipeExpression>(
 | 
			
		||||
    clonedAst,
 | 
			
		||||
    pathToSketchNode,
 | 
			
		||||
    'PipeExpression'
 | 
			
		||||
  )
 | 
			
		||||
  if (err(sketchPipeExpressionNode)) return sketchPipeExpressionNode
 | 
			
		||||
  const { node: sketchPipeExpression } = sketchPipeExpressionNode
 | 
			
		||||
  const isInPipeExpression = sketchPipeExpression.type === 'PipeExpression'
 | 
			
		||||
 | 
			
		||||
  const sketchVariableDeclaratorNode = getNodeFromPath<VariableDeclarator>(
 | 
			
		||||
    clonedAst,
 | 
			
		||||
    pathToSketchNode,
 | 
			
		||||
    'VariableDeclarator'
 | 
			
		||||
  )
 | 
			
		||||
  if (err(sketchVariableDeclaratorNode)) return sketchVariableDeclaratorNode
 | 
			
		||||
  const {
 | 
			
		||||
    node: sketchVariableDeclarator,
 | 
			
		||||
    shallowPath: sketchPathToDecleration,
 | 
			
		||||
  } = sketchVariableDeclaratorNode
 | 
			
		||||
  const { node: sketchVariableDeclarator } = sketchVariableDeclaratorNode
 | 
			
		||||
 | 
			
		||||
  const axisSelection = axis?.graphSelections[0]?.artifact
 | 
			
		||||
 | 
			
		||||
@ -103,37 +91,13 @@ export function revolveSketch(
 | 
			
		||||
    createIdentifier(sketchVariableDeclarator.id.name),
 | 
			
		||||
  ])
 | 
			
		||||
 | 
			
		||||
  if (shouldPipe) {
 | 
			
		||||
    const pipeChain = createPipeExpression(
 | 
			
		||||
      isInPipeExpression
 | 
			
		||||
        ? [...sketchPipeExpression.body, revolveCall]
 | 
			
		||||
        : [sketchExpression as any, revolveCall]
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    sketchVariableDeclarator.init = pipeChain
 | 
			
		||||
    const pathToRevolveArg: PathToNode = [
 | 
			
		||||
      ...sketchPathToDecleration,
 | 
			
		||||
      ['init', 'VariableDeclarator'],
 | 
			
		||||
      ['body', ''],
 | 
			
		||||
      [pipeChain.body.length - 1, 'index'],
 | 
			
		||||
      ['arguments', 'CallExpression'],
 | 
			
		||||
      [0, 'index'],
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      modifiedAst: clonedAst,
 | 
			
		||||
      pathToSketchNode,
 | 
			
		||||
      pathToRevolveArg,
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // We're not creating a pipe expression,
 | 
			
		||||
  // but rather a separate constant for the extrusion
 | 
			
		||||
  const name = findUniqueName(clonedAst, KCL_DEFAULT_CONSTANT_PREFIXES.REVOLVE)
 | 
			
		||||
  const VariableDeclaration = createVariableDeclaration(name, revolveCall)
 | 
			
		||||
  const sketchIndexInPathToNode =
 | 
			
		||||
    sketchPathToDecleration.findIndex((a) => a[0] === 'body') + 1
 | 
			
		||||
  const sketchIndexInBody = sketchPathToDecleration[sketchIndexInPathToNode][0]
 | 
			
		||||
  const lastSketchNodePath =
 | 
			
		||||
    orderedSketchNodePaths[orderedSketchNodePaths.length - 1]
 | 
			
		||||
  const sketchIndexInBody = Number(lastSketchNodePath[1][0])
 | 
			
		||||
  if (typeof sketchIndexInBody !== 'number')
 | 
			
		||||
    return new Error('expected sketchIndexInBody to be a number')
 | 
			
		||||
  clonedAst.body.splice(sketchIndexInBody + 1, 0, VariableDeclaration)
 | 
			
		||||
 | 
			
		||||
@ -33,6 +33,7 @@ import { err, Reason } from 'lib/trap'
 | 
			
		||||
import { ImportStatement } from 'wasm-lib/kcl/bindings/ImportStatement'
 | 
			
		||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
 | 
			
		||||
import { ArtifactGraph, codeRefFromRange } from './std/artifactGraph'
 | 
			
		||||
import { FunctionExpression } from 'wasm-lib/kcl/bindings/FunctionExpression'
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Retrieves a node from a given path within a Program node structure, optionally stopping at a specified node type.
 | 
			
		||||
@ -597,7 +598,13 @@ export function findAllPreviousVariables(
 | 
			
		||||
type ReplacerFn = (
 | 
			
		||||
  _ast: Node<Program>,
 | 
			
		||||
  varName: string
 | 
			
		||||
) => { modifiedAst: Node<Program>; pathToReplaced: PathToNode } | Error
 | 
			
		||||
) =>
 | 
			
		||||
  | {
 | 
			
		||||
      modifiedAst: Node<Program>
 | 
			
		||||
      pathToReplaced: PathToNode
 | 
			
		||||
      exprInsertIndex: number
 | 
			
		||||
    }
 | 
			
		||||
  | Error
 | 
			
		||||
 | 
			
		||||
export function isNodeSafeToReplacePath(
 | 
			
		||||
  ast: Program,
 | 
			
		||||
@ -649,7 +656,7 @@ export function isNodeSafeToReplacePath(
 | 
			
		||||
    if (err(_nodeToReplace)) return _nodeToReplace
 | 
			
		||||
    const nodeToReplace = _nodeToReplace.node as any
 | 
			
		||||
    nodeToReplace[last[0]] = identifier
 | 
			
		||||
    return { modifiedAst: _ast, pathToReplaced }
 | 
			
		||||
    return { modifiedAst: _ast, pathToReplaced, exprInsertIndex: index }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const hasPipeSub = isTypeInValue(finVal as Expr, 'PipeSubstitution')
 | 
			
		||||
@ -768,8 +775,15 @@ export function isLinesParallelAndConstrained(
 | 
			
		||||
    if (err(_primarySegment)) return _primarySegment
 | 
			
		||||
    const primarySegment = _primarySegment.segment
 | 
			
		||||
 | 
			
		||||
    const _varDec2 = getNodeFromPath(ast, secondaryPath, 'VariableDeclaration')
 | 
			
		||||
    if (err(_varDec2)) return _varDec2
 | 
			
		||||
    const varDec2 = _varDec2.node
 | 
			
		||||
    const varName2 = (varDec2 as VariableDeclaration)?.declaration.id?.name
 | 
			
		||||
    const sg2 = sketchFromKclValue(programMemory?.get(varName2), varName2)
 | 
			
		||||
    if (err(sg2)) return sg2
 | 
			
		||||
 | 
			
		||||
    const _segment = getSketchSegmentFromSourceRange(
 | 
			
		||||
      sg,
 | 
			
		||||
      sg2,
 | 
			
		||||
      secondaryLine?.codeRef?.range
 | 
			
		||||
    )
 | 
			
		||||
    if (err(_segment)) return _segment
 | 
			
		||||
@ -1102,3 +1116,57 @@ export function getObjExprProperty(
 | 
			
		||||
  if (index === -1) return null
 | 
			
		||||
  return { expr: node.properties[index].value, index }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function isCursorInFunctionDefinition(
 | 
			
		||||
  ast: Node<Program>,
 | 
			
		||||
  selectionRanges: Selection
 | 
			
		||||
): boolean {
 | 
			
		||||
  if (!selectionRanges?.codeRef?.pathToNode) return false
 | 
			
		||||
  const node = getNodeFromPath<FunctionExpression>(
 | 
			
		||||
    ast,
 | 
			
		||||
    selectionRanges.codeRef.pathToNode,
 | 
			
		||||
    'FunctionExpression'
 | 
			
		||||
  )
 | 
			
		||||
  if (err(node)) return false
 | 
			
		||||
  if (node.node.type === 'FunctionExpression') return true
 | 
			
		||||
  return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getBodyIndex(pathToNode: PathToNode): number | Error {
 | 
			
		||||
  const index = Number(pathToNode[1][0])
 | 
			
		||||
  if (Number.isInteger(index)) return index
 | 
			
		||||
  return new Error('Expected number index')
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function isCallExprWithName(
 | 
			
		||||
  expr: Expr | CallExpression,
 | 
			
		||||
  name: string
 | 
			
		||||
): expr is CallExpression {
 | 
			
		||||
  if (expr.type === 'CallExpression' && expr.callee.type === 'Identifier') {
 | 
			
		||||
    return expr.callee.name === name
 | 
			
		||||
  }
 | 
			
		||||
  return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function doesSketchPipeNeedSplitting(
 | 
			
		||||
  ast: Node<Program>,
 | 
			
		||||
  pathToPipe: PathToNode
 | 
			
		||||
): boolean | Error {
 | 
			
		||||
  const varDec = getNodeFromPath<VariableDeclarator>(
 | 
			
		||||
    ast,
 | 
			
		||||
    pathToPipe,
 | 
			
		||||
    'VariableDeclarator'
 | 
			
		||||
  )
 | 
			
		||||
  if (err(varDec)) return varDec
 | 
			
		||||
  if (varDec.node.type !== 'VariableDeclarator') return new Error('Not a var')
 | 
			
		||||
  const pipeExpression = varDec.node.init
 | 
			
		||||
  if (pipeExpression.type !== 'PipeExpression') return false
 | 
			
		||||
  const [firstPipe, secondPipe] = pipeExpression.body
 | 
			
		||||
  if (!firstPipe || !secondPipe) return false
 | 
			
		||||
  if (
 | 
			
		||||
    isCallExprWithName(firstPipe, 'startSketchOn') &&
 | 
			
		||||
    isCallExprWithName(secondPipe, 'startProfileAt')
 | 
			
		||||
  )
 | 
			
		||||
    return true
 | 
			
		||||
  return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -212,6 +212,7 @@ Map {
 | 
			
		||||
    "type": "wall",
 | 
			
		||||
  },
 | 
			
		||||
  "UUID-10" => {
 | 
			
		||||
    "codeRef": undefined,
 | 
			
		||||
    "edgeCutEdgeIds": [],
 | 
			
		||||
    "id": "UUID",
 | 
			
		||||
    "pathIds": [
 | 
			
		||||
 | 
			
		||||
@ -22,6 +22,7 @@ import * as d3 from 'd3-force'
 | 
			
		||||
import path from 'path'
 | 
			
		||||
import pixelmatch from 'pixelmatch'
 | 
			
		||||
import { PNG } from 'pngjs'
 | 
			
		||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
Note this is an integration test, these tests connect to our real dev server and make websocket commands.
 | 
			
		||||
@ -171,7 +172,7 @@ afterAll(() => {
 | 
			
		||||
 | 
			
		||||
describe('testing createArtifactGraph', () => {
 | 
			
		||||
  describe('code with offset planes and a sketch:', () => {
 | 
			
		||||
    let ast: Program
 | 
			
		||||
    let ast: Node<Program>
 | 
			
		||||
    let theMap: ReturnType<typeof createArtifactGraph>
 | 
			
		||||
 | 
			
		||||
    it('setup', () => {
 | 
			
		||||
@ -217,7 +218,7 @@ describe('testing createArtifactGraph', () => {
 | 
			
		||||
    })
 | 
			
		||||
  })
 | 
			
		||||
  describe('code with an extrusion, fillet and sketch of face:', () => {
 | 
			
		||||
    let ast: Program
 | 
			
		||||
    let ast: Node<Program>
 | 
			
		||||
    let theMap: ReturnType<typeof createArtifactGraph>
 | 
			
		||||
    it('setup', () => {
 | 
			
		||||
      // putting this logic in here because describe blocks runs before beforeAll has finished
 | 
			
		||||
@ -312,7 +313,7 @@ describe('testing createArtifactGraph', () => {
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  describe(`code with sketches but no extrusions or other 3D elements`, () => {
 | 
			
		||||
    let ast: Program
 | 
			
		||||
    let ast: Node<Program>
 | 
			
		||||
    let theMap: ReturnType<typeof createArtifactGraph>
 | 
			
		||||
    it(`setup`, () => {
 | 
			
		||||
      // putting this logic in here because describe blocks runs before beforeAll has finished
 | 
			
		||||
@ -377,7 +378,7 @@ describe('testing createArtifactGraph', () => {
 | 
			
		||||
 | 
			
		||||
describe('capture graph of sketchOnFaceOnFace...', () => {
 | 
			
		||||
  describe('code with an extrusion, fillet and sketch of face:', () => {
 | 
			
		||||
    let ast: Program
 | 
			
		||||
    let ast: Node<Program>
 | 
			
		||||
    let theMap: ReturnType<typeof createArtifactGraph>
 | 
			
		||||
    it('setup', async () => {
 | 
			
		||||
      // putting this logic in here because describe blocks runs before beforeAll has finished
 | 
			
		||||
@ -399,7 +400,9 @@ describe('capture graph of sketchOnFaceOnFace...', () => {
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
function getCommands(codeKey: CodeKey): CacheShape[CodeKey] & { ast: Program } {
 | 
			
		||||
function getCommands(
 | 
			
		||||
  codeKey: CodeKey
 | 
			
		||||
): CacheShape[CodeKey] & { ast: Node<Program> } {
 | 
			
		||||
  const ast = assertParse(codeKey)
 | 
			
		||||
  const file = fs.readFileSync(fullPath, 'utf-8')
 | 
			
		||||
  const parsed: CacheShape = JSON.parse(file)
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,19 @@
 | 
			
		||||
import { PathToNode, Program, SourceRange } from 'lang/wasm'
 | 
			
		||||
import {
 | 
			
		||||
  Expr,
 | 
			
		||||
  PathToNode,
 | 
			
		||||
  Program,
 | 
			
		||||
  SourceRange,
 | 
			
		||||
  VariableDeclaration,
 | 
			
		||||
} from 'lang/wasm'
 | 
			
		||||
import { Models } from '@kittycad/lib'
 | 
			
		||||
import { getNodePathFromSourceRange } from 'lang/queryAst'
 | 
			
		||||
import {
 | 
			
		||||
  getNodeFromPath,
 | 
			
		||||
  getNodePathFromSourceRange,
 | 
			
		||||
  traverse,
 | 
			
		||||
} from 'lang/queryAst'
 | 
			
		||||
import { err } from 'lib/trap'
 | 
			
		||||
import { engineCommandManager, kclManager } from 'lib/singletons'
 | 
			
		||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
 | 
			
		||||
 | 
			
		||||
export type ArtifactId = string
 | 
			
		||||
 | 
			
		||||
@ -34,14 +46,14 @@ export interface PathArtifact extends BaseArtifact {
 | 
			
		||||
  codeRef: CodeRef
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface solid2D extends BaseArtifact {
 | 
			
		||||
interface Solid2DArtifact extends BaseArtifact {
 | 
			
		||||
  type: 'solid2D'
 | 
			
		||||
  pathId: ArtifactId
 | 
			
		||||
}
 | 
			
		||||
export interface PathArtifactRich extends BaseArtifact {
 | 
			
		||||
  type: 'path'
 | 
			
		||||
  /** A path must always lie on a plane */
 | 
			
		||||
  plane: PlaneArtifact | WallArtifact
 | 
			
		||||
  plane: PlaneArtifact | WallArtifact | CapArtifact
 | 
			
		||||
  /** A path must always contain 0 or more segments */
 | 
			
		||||
  segments: Array<SegmentArtifact>
 | 
			
		||||
  /** A path may not result in a sweep artifact */
 | 
			
		||||
@ -61,7 +73,7 @@ interface SegmentArtifactRich extends BaseArtifact {
 | 
			
		||||
  type: 'segment'
 | 
			
		||||
  path: PathArtifact
 | 
			
		||||
  surf: WallArtifact
 | 
			
		||||
  edges: Array<SweepEdge>
 | 
			
		||||
  edges: Array<SweepEdgeArtifact>
 | 
			
		||||
  edgeCut?: EdgeCut
 | 
			
		||||
  codeRef: CodeRef
 | 
			
		||||
}
 | 
			
		||||
@ -80,7 +92,7 @@ interface SweepArtifactRich extends BaseArtifact {
 | 
			
		||||
  subType: 'extrusion' | 'revolve'
 | 
			
		||||
  path: PathArtifact
 | 
			
		||||
  surfaces: Array<WallArtifact | CapArtifact>
 | 
			
		||||
  edges: Array<SweepEdge>
 | 
			
		||||
  edges: Array<SweepEdgeArtifact>
 | 
			
		||||
  codeRef: CodeRef
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -90,6 +102,9 @@ interface WallArtifact extends BaseArtifact {
 | 
			
		||||
  edgeCutEdgeIds: Array<ArtifactId>
 | 
			
		||||
  sweepId: ArtifactId
 | 
			
		||||
  pathIds: Array<ArtifactId>
 | 
			
		||||
  // codeRef is for the sketchOnFace plane, not for the wall itself
 | 
			
		||||
  // traverse to the extrude and or segment to get the wall's codeRef
 | 
			
		||||
  codeRef?: CodeRef
 | 
			
		||||
}
 | 
			
		||||
interface CapArtifact extends BaseArtifact {
 | 
			
		||||
  type: 'cap'
 | 
			
		||||
@ -97,9 +112,12 @@ interface CapArtifact extends BaseArtifact {
 | 
			
		||||
  edgeCutEdgeIds: Array<ArtifactId>
 | 
			
		||||
  sweepId: ArtifactId
 | 
			
		||||
  pathIds: Array<ArtifactId>
 | 
			
		||||
  // codeRef is for the sketchOnFace plane, not for the wall itself
 | 
			
		||||
  // traverse to the extrude and or segment to get the wall's codeRef
 | 
			
		||||
  codeRef?: CodeRef
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface SweepEdge extends BaseArtifact {
 | 
			
		||||
interface SweepEdgeArtifact extends BaseArtifact {
 | 
			
		||||
  type: 'sweepEdge'
 | 
			
		||||
  segId: ArtifactId
 | 
			
		||||
  sweepId: ArtifactId
 | 
			
		||||
@ -129,10 +147,10 @@ export type Artifact =
 | 
			
		||||
  | SweepArtifact
 | 
			
		||||
  | WallArtifact
 | 
			
		||||
  | CapArtifact
 | 
			
		||||
  | SweepEdge
 | 
			
		||||
  | SweepEdgeArtifact
 | 
			
		||||
  | EdgeCut
 | 
			
		||||
  | EdgeCutEdge
 | 
			
		||||
  | solid2D
 | 
			
		||||
  | Solid2DArtifact
 | 
			
		||||
 | 
			
		||||
export type ArtifactGraph = Map<ArtifactId, Artifact>
 | 
			
		||||
 | 
			
		||||
@ -159,7 +177,7 @@ export function createArtifactGraph({
 | 
			
		||||
}: {
 | 
			
		||||
  orderedCommands: Array<OrderedCommand>
 | 
			
		||||
  responseMap: ResponseMap
 | 
			
		||||
  ast: Program
 | 
			
		||||
  ast: Node<Program>
 | 
			
		||||
}) {
 | 
			
		||||
  const myMap = new Map<ArtifactId, Artifact>()
 | 
			
		||||
 | 
			
		||||
@ -238,7 +256,7 @@ export function getArtifactsToUpdate({
 | 
			
		||||
  /** Passing in a getter because we don't wan this function to update the map directly */
 | 
			
		||||
  getArtifact: (id: ArtifactId) => Artifact | undefined
 | 
			
		||||
  currentPlaneId: ArtifactId
 | 
			
		||||
  ast: Program
 | 
			
		||||
  ast: Node<Program>
 | 
			
		||||
}): Array<{
 | 
			
		||||
  id: ArtifactId
 | 
			
		||||
  artifact: Artifact
 | 
			
		||||
@ -274,6 +292,13 @@ export function getArtifactsToUpdate({
 | 
			
		||||
      plane?.type === 'plane' ? plane?.codeRef : { range, pathToNode }
 | 
			
		||||
    const existingPlane = getArtifact(currentPlaneId)
 | 
			
		||||
    if (existingPlane?.type === 'wall') {
 | 
			
		||||
      let existingPlaneCodeRef = existingPlane.codeRef
 | 
			
		||||
      if (!existingPlaneCodeRef) {
 | 
			
		||||
        const astWalkCodeRef = getWallOrCapPlaneCodeRef(ast, codeRef.pathToNode)
 | 
			
		||||
        if (!err(astWalkCodeRef)) {
 | 
			
		||||
          existingPlaneCodeRef = astWalkCodeRef
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      return [
 | 
			
		||||
        {
 | 
			
		||||
          id: currentPlaneId,
 | 
			
		||||
@ -284,6 +309,29 @@ export function getArtifactsToUpdate({
 | 
			
		||||
            edgeCutEdgeIds: existingPlane.edgeCutEdgeIds,
 | 
			
		||||
            sweepId: existingPlane.sweepId,
 | 
			
		||||
            pathIds: existingPlane.pathIds,
 | 
			
		||||
            codeRef: existingPlaneCodeRef,
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      ]
 | 
			
		||||
    } else if (existingPlane?.type === 'cap') {
 | 
			
		||||
      let existingPlaneCodeRef = existingPlane.codeRef
 | 
			
		||||
      if (!existingPlaneCodeRef) {
 | 
			
		||||
        const astWalkCodeRef = getWallOrCapPlaneCodeRef(ast, codeRef.pathToNode)
 | 
			
		||||
        if (!err(astWalkCodeRef)) {
 | 
			
		||||
          existingPlaneCodeRef = astWalkCodeRef
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      return [
 | 
			
		||||
        {
 | 
			
		||||
          id: currentPlaneId,
 | 
			
		||||
          artifact: {
 | 
			
		||||
            type: 'cap',
 | 
			
		||||
            subType: existingPlane.subType,
 | 
			
		||||
            id: currentPlaneId,
 | 
			
		||||
            edgeCutEdgeIds: existingPlane.edgeCutEdgeIds,
 | 
			
		||||
            sweepId: existingPlane.sweepId,
 | 
			
		||||
            pathIds: existingPlane.pathIds,
 | 
			
		||||
            codeRef: existingPlaneCodeRef,
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      ]
 | 
			
		||||
@ -328,6 +376,18 @@ export function getArtifactsToUpdate({
 | 
			
		||||
          pathIds: [id],
 | 
			
		||||
        },
 | 
			
		||||
      })
 | 
			
		||||
    } else if (plane?.type === 'cap') {
 | 
			
		||||
      returnArr.push({
 | 
			
		||||
        id: currentPlaneId,
 | 
			
		||||
        artifact: {
 | 
			
		||||
          type: 'cap',
 | 
			
		||||
          id: currentPlaneId,
 | 
			
		||||
          subType: plane.subType,
 | 
			
		||||
          edgeCutEdgeIds: plane.edgeCutEdgeIds,
 | 
			
		||||
          sweepId: plane.sweepId,
 | 
			
		||||
          pathIds: [id],
 | 
			
		||||
        },
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
    return returnArr
 | 
			
		||||
  } else if (cmd.type === 'extend_path' || cmd.type === 'close_path') {
 | 
			
		||||
@ -733,7 +793,7 @@ export function getCapCodeRef(
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getSolid2dCodeRef(
 | 
			
		||||
  solid2D: solid2D,
 | 
			
		||||
  solid2D: Solid2DArtifact,
 | 
			
		||||
  artifactGraph: ArtifactGraph
 | 
			
		||||
): CodeRef | Error {
 | 
			
		||||
  const path = getArtifactOfTypes(
 | 
			
		||||
@ -757,7 +817,7 @@ export function getWallCodeRef(
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getSweepEdgeCodeRef(
 | 
			
		||||
  edge: SweepEdge,
 | 
			
		||||
  edge: SweepEdgeArtifact,
 | 
			
		||||
  artifactGraph: ArtifactGraph
 | 
			
		||||
): CodeRef | Error {
 | 
			
		||||
  const seg = getArtifactOfTypes(
 | 
			
		||||
@ -871,3 +931,281 @@ export function codeRefFromRange(range: SourceRange, ast: Program): CodeRef {
 | 
			
		||||
    pathToNode: getNodePathFromSourceRange(ast, range),
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getPlaneFromPath(
 | 
			
		||||
  path: PathArtifact,
 | 
			
		||||
  graph: ArtifactGraph
 | 
			
		||||
): PlaneArtifact | WallArtifact | CapArtifact | Error {
 | 
			
		||||
  const plane = getArtifactOfTypes(
 | 
			
		||||
    { key: path.planeId, types: ['plane', 'wall', 'cap'] },
 | 
			
		||||
    graph
 | 
			
		||||
  )
 | 
			
		||||
  if (err(plane)) return plane
 | 
			
		||||
  return plane
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getPlaneFromSegment(
 | 
			
		||||
  segment: SegmentArtifact,
 | 
			
		||||
  graph: ArtifactGraph
 | 
			
		||||
): PlaneArtifact | WallArtifact | CapArtifact | Error {
 | 
			
		||||
  const path = getArtifactOfTypes(
 | 
			
		||||
    { key: segment.pathId, types: ['path'] },
 | 
			
		||||
    graph
 | 
			
		||||
  )
 | 
			
		||||
  if (err(path)) return path
 | 
			
		||||
  return getPlaneFromPath(path, graph)
 | 
			
		||||
}
 | 
			
		||||
function getPlaneFromSolid2D(
 | 
			
		||||
  solid2D: Solid2DArtifact,
 | 
			
		||||
  graph: ArtifactGraph
 | 
			
		||||
): PlaneArtifact | WallArtifact | CapArtifact | Error {
 | 
			
		||||
  const path = getArtifactOfTypes(
 | 
			
		||||
    { key: solid2D.pathId, types: ['path'] },
 | 
			
		||||
    graph
 | 
			
		||||
  )
 | 
			
		||||
  if (err(path)) return path
 | 
			
		||||
  return getPlaneFromPath(path, graph)
 | 
			
		||||
}
 | 
			
		||||
function getPlaneFromCap(
 | 
			
		||||
  cap: CapArtifact,
 | 
			
		||||
  graph: ArtifactGraph
 | 
			
		||||
): PlaneArtifact | WallArtifact | CapArtifact | Error {
 | 
			
		||||
  const sweep = getArtifactOfTypes(
 | 
			
		||||
    { key: cap.sweepId, types: ['sweep'] },
 | 
			
		||||
    graph
 | 
			
		||||
  )
 | 
			
		||||
  if (err(sweep)) return sweep
 | 
			
		||||
  const path = getArtifactOfTypes({ key: sweep.pathId, types: ['path'] }, graph)
 | 
			
		||||
  if (err(path)) return path
 | 
			
		||||
  return getPlaneFromPath(path, graph)
 | 
			
		||||
}
 | 
			
		||||
function getPlaneFromWall(
 | 
			
		||||
  wall: WallArtifact,
 | 
			
		||||
  graph: ArtifactGraph
 | 
			
		||||
): PlaneArtifact | WallArtifact | CapArtifact | Error {
 | 
			
		||||
  const sweep = getArtifactOfTypes(
 | 
			
		||||
    { key: wall.sweepId, types: ['sweep'] },
 | 
			
		||||
    graph
 | 
			
		||||
  )
 | 
			
		||||
  if (err(sweep)) return sweep
 | 
			
		||||
  const path = getArtifactOfTypes({ key: sweep.pathId, types: ['path'] }, graph)
 | 
			
		||||
  if (err(path)) return path
 | 
			
		||||
  return getPlaneFromPath(path, graph)
 | 
			
		||||
}
 | 
			
		||||
function getPlaneFromSweepEdge(edge: SweepEdgeArtifact, graph: ArtifactGraph) {
 | 
			
		||||
  const sweep = getArtifactOfTypes(
 | 
			
		||||
    { key: edge.sweepId, types: ['sweep'] },
 | 
			
		||||
    graph
 | 
			
		||||
  )
 | 
			
		||||
  if (err(sweep)) return sweep
 | 
			
		||||
  const path = getArtifactOfTypes({ key: sweep.pathId, types: ['path'] }, graph)
 | 
			
		||||
  if (err(path)) return path
 | 
			
		||||
  return getPlaneFromPath(path, graph)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getPlaneFromArtifact(
 | 
			
		||||
  artifact: Artifact | undefined,
 | 
			
		||||
  graph: ArtifactGraph
 | 
			
		||||
): PlaneArtifact | WallArtifact | CapArtifact | Error {
 | 
			
		||||
  if (!artifact) return new Error(`Artifact is undefined`)
 | 
			
		||||
  if (artifact.type === 'plane') return artifact
 | 
			
		||||
  if (artifact.type === 'path') return getPlaneFromPath(artifact, graph)
 | 
			
		||||
  if (artifact.type === 'segment') return getPlaneFromSegment(artifact, graph)
 | 
			
		||||
  if (artifact.type === 'solid2D') return getPlaneFromSolid2D(artifact, graph)
 | 
			
		||||
  if (artifact.type === 'cap') return getPlaneFromCap(artifact, graph)
 | 
			
		||||
  if (artifact.type === 'wall') return getPlaneFromWall(artifact, graph)
 | 
			
		||||
  if (artifact.type === 'sweepEdge')
 | 
			
		||||
    return getPlaneFromSweepEdge(artifact, graph)
 | 
			
		||||
  return new Error(`Artifact type ${artifact.type} does not have a plane`)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const isExprSafe = (index: number): boolean => {
 | 
			
		||||
  const expr = kclManager.ast.body?.[index]
 | 
			
		||||
  if (!expr) {
 | 
			
		||||
    return false
 | 
			
		||||
  }
 | 
			
		||||
  if (expr.type === 'ImportStatement' || expr.type === 'ReturnStatement') {
 | 
			
		||||
    return false
 | 
			
		||||
  }
 | 
			
		||||
  if (expr.type === 'VariableDeclaration') {
 | 
			
		||||
    const init = expr.declaration?.init
 | 
			
		||||
    if (!init) return false
 | 
			
		||||
    if (init.type === 'CallExpression') {
 | 
			
		||||
      return false
 | 
			
		||||
    }
 | 
			
		||||
    if (init.type === 'BinaryExpression' && isNodeSafe(init)) {
 | 
			
		||||
      return true
 | 
			
		||||
    }
 | 
			
		||||
    if (init.type === 'Literal' || init.type === 'MemberExpression') {
 | 
			
		||||
      return true
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const onlyConsecutivePaths = (
 | 
			
		||||
  orderedNodePaths: PathToNode[],
 | 
			
		||||
  originalPath: PathToNode
 | 
			
		||||
): PathToNode[] => {
 | 
			
		||||
  const originalIndex = Number(
 | 
			
		||||
    orderedNodePaths.find(
 | 
			
		||||
      (path) => path[1][0] === originalPath[1][0]
 | 
			
		||||
    )?.[1]?.[0] || 0
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  const minIndex = Number(orderedNodePaths[0][1][0])
 | 
			
		||||
  const maxIndex = Number(orderedNodePaths[orderedNodePaths.length - 1][1][0])
 | 
			
		||||
  const pathIndexMap: any = {}
 | 
			
		||||
  orderedNodePaths.forEach((path) => {
 | 
			
		||||
    const bodyIndex = Number(path[1][0])
 | 
			
		||||
    pathIndexMap[bodyIndex] = path
 | 
			
		||||
  })
 | 
			
		||||
  const safePaths: PathToNode[] = []
 | 
			
		||||
 | 
			
		||||
  // traverse expressions in either direction from the profile selected
 | 
			
		||||
  // when the user entered sketch mode
 | 
			
		||||
  for (let i = originalIndex; i <= maxIndex; i++) {
 | 
			
		||||
    if (pathIndexMap[i]) {
 | 
			
		||||
      safePaths.push(pathIndexMap[i])
 | 
			
		||||
    } else if (!isExprSafe(i)) {
 | 
			
		||||
      break
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  for (let i = originalIndex - 1; i >= minIndex; i--) {
 | 
			
		||||
    if (pathIndexMap[i]) {
 | 
			
		||||
      safePaths.unshift(pathIndexMap[i])
 | 
			
		||||
    } else if (!isExprSafe(i)) {
 | 
			
		||||
      break
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return safePaths
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getPathsFromPlaneArtifact(planeArtifact: PlaneArtifact) {
 | 
			
		||||
  const nodePaths: PathToNode[] = []
 | 
			
		||||
  for (const pathId of planeArtifact.pathIds) {
 | 
			
		||||
    const path = engineCommandManager.artifactGraph.get(pathId)
 | 
			
		||||
    if (!path) continue
 | 
			
		||||
    if ('codeRef' in path && path.codeRef) {
 | 
			
		||||
      // TODO should figure out why upstream the path is bad
 | 
			
		||||
      const isNodePathBad = path.codeRef.pathToNode.length < 2
 | 
			
		||||
      nodePaths.push(
 | 
			
		||||
        isNodePathBad
 | 
			
		||||
          ? getNodePathFromSourceRange(kclManager.ast, path.codeRef.range)
 | 
			
		||||
          : path.codeRef.pathToNode
 | 
			
		||||
      )
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return onlyConsecutivePaths(nodePaths, nodePaths[0])
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getPathsFromArtifact({
 | 
			
		||||
  sketchPathToNode,
 | 
			
		||||
  artifact,
 | 
			
		||||
}: {
 | 
			
		||||
  sketchPathToNode: PathToNode
 | 
			
		||||
  artifact?: Artifact
 | 
			
		||||
}): PathToNode[] | Error {
 | 
			
		||||
  const plane = getPlaneFromArtifact(
 | 
			
		||||
    artifact,
 | 
			
		||||
    engineCommandManager.artifactGraph
 | 
			
		||||
  )
 | 
			
		||||
  if (err(plane)) return plane
 | 
			
		||||
  const paths = getArtifactsOfTypes(
 | 
			
		||||
    { keys: plane.pathIds, types: ['path'] },
 | 
			
		||||
    engineCommandManager.artifactGraph
 | 
			
		||||
  )
 | 
			
		||||
  let nodePaths = [...paths.values()]
 | 
			
		||||
    .map((path) => path.codeRef.pathToNode)
 | 
			
		||||
    .sort((a, b) => Number(a[1][0]) - Number(b[1][0]))
 | 
			
		||||
  return onlyConsecutivePaths(nodePaths, sketchPathToNode)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function isNodeSafe(node: Expr): boolean {
 | 
			
		||||
  if (node.type === 'Literal' || node.type === 'MemberExpression') {
 | 
			
		||||
    return true
 | 
			
		||||
  }
 | 
			
		||||
  if (node.type === 'BinaryExpression') {
 | 
			
		||||
    return isNodeSafe(node.left) && isNodeSafe(node.right)
 | 
			
		||||
  }
 | 
			
		||||
  return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** {@deprecated} this information should come from the ArtifactGraph not digging around in the AST */
 | 
			
		||||
function getWallOrCapPlaneCodeRef(
 | 
			
		||||
  ast: Node<Program>,
 | 
			
		||||
  pathToNode: PathToNode
 | 
			
		||||
): CodeRef | Error {
 | 
			
		||||
  const varDec = getNodeFromPath<VariableDeclaration>(
 | 
			
		||||
    ast,
 | 
			
		||||
    pathToNode,
 | 
			
		||||
    'VariableDeclaration'
 | 
			
		||||
  )
 | 
			
		||||
  if (err(varDec)) return varDec
 | 
			
		||||
  if (varDec.node.type !== 'VariableDeclaration')
 | 
			
		||||
    return new Error('Expected VariableDeclaration')
 | 
			
		||||
  const init = varDec.node.declaration.init
 | 
			
		||||
  let varName = ''
 | 
			
		||||
  if (
 | 
			
		||||
    init.type === 'CallExpression' &&
 | 
			
		||||
    init.callee.type === 'Identifier' &&
 | 
			
		||||
    (init.callee.name === 'circle' || init.callee.name === 'startProfileAt')
 | 
			
		||||
  ) {
 | 
			
		||||
    const secondArg = init.arguments[1]
 | 
			
		||||
    if (secondArg.type === 'Identifier') {
 | 
			
		||||
      varName = secondArg.name
 | 
			
		||||
    }
 | 
			
		||||
  } else if (init.type === 'PipeExpression') {
 | 
			
		||||
    const firstExpr = init.body[0]
 | 
			
		||||
    if (
 | 
			
		||||
      firstExpr.type === 'CallExpression' &&
 | 
			
		||||
      firstExpr.callee.type === 'Identifier' &&
 | 
			
		||||
      firstExpr.callee.name === 'startProfileAt'
 | 
			
		||||
    ) {
 | 
			
		||||
      const secondArg = firstExpr.arguments[1]
 | 
			
		||||
      if (secondArg.type === 'Identifier') {
 | 
			
		||||
        varName = secondArg.name
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  if (varName === '') return new Error('Could not find variable name')
 | 
			
		||||
 | 
			
		||||
  let currentVariableName = ''
 | 
			
		||||
  const planeCodeRef: Array<{
 | 
			
		||||
    path: PathToNode
 | 
			
		||||
    sketchName: string
 | 
			
		||||
    range: SourceRange
 | 
			
		||||
  }> = []
 | 
			
		||||
  traverse(ast, {
 | 
			
		||||
    leave: (node) => {
 | 
			
		||||
      if (node.type === 'VariableDeclaration') {
 | 
			
		||||
        currentVariableName = ''
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    enter: (node, path) => {
 | 
			
		||||
      if (node.type === 'VariableDeclaration') {
 | 
			
		||||
        currentVariableName = node.declaration.id.name
 | 
			
		||||
      }
 | 
			
		||||
      if (
 | 
			
		||||
        // match `${varName} = startSketchOn(...)`
 | 
			
		||||
        node.type === 'CallExpression' &&
 | 
			
		||||
        node.callee.name === 'startSketchOn' &&
 | 
			
		||||
        node.arguments[0].type === 'Identifier' &&
 | 
			
		||||
        currentVariableName === varName
 | 
			
		||||
      ) {
 | 
			
		||||
        planeCodeRef.push({
 | 
			
		||||
          path,
 | 
			
		||||
          sketchName: currentVariableName,
 | 
			
		||||
          range: [node.start, node.end, true],
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
  })
 | 
			
		||||
  if (!planeCodeRef.length)
 | 
			
		||||
    return new Error('No paths found depending on extrude')
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    pathToNode: planeCodeRef[0].path,
 | 
			
		||||
    range: planeCodeRef[0].range,
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 568 KiB After Width: | Height: | Size: 560 KiB  | 
@ -37,6 +37,7 @@ import { KclManager } from 'lang/KclSingleton'
 | 
			
		||||
import { reportRejection } from 'lib/trap'
 | 
			
		||||
import { markOnce } from 'lib/performance'
 | 
			
		||||
import { MachineManager } from 'components/MachineManagerProvider'
 | 
			
		||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
 | 
			
		||||
 | 
			
		||||
// TODO(paultag): This ought to be tweakable.
 | 
			
		||||
const pingIntervalMs = 5_000
 | 
			
		||||
@ -2115,7 +2116,7 @@ export class EngineCommandManager extends EventTarget {
 | 
			
		||||
      Object.values(this.pendingCommands).map((a) => a.promise)
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
  updateArtifactGraph(ast: Program) {
 | 
			
		||||
  updateArtifactGraph(ast: Node<Program>) {
 | 
			
		||||
    this.artifactGraph = createArtifactGraph({
 | 
			
		||||
      orderedCommands: this.orderedCommands,
 | 
			
		||||
      responseMap: this.responseMap,
 | 
			
		||||
@ -2213,7 +2214,11 @@ export class EngineCommandManager extends EventTarget {
 | 
			
		||||
    commandTypeToTarget: string
 | 
			
		||||
  ): string | undefined {
 | 
			
		||||
    for (const [artifactId, artifact] of this.artifactGraph) {
 | 
			
		||||
      if ('codeRef' in artifact && isOverlap(range, artifact.codeRef.range)) {
 | 
			
		||||
      if (
 | 
			
		||||
        'codeRef' in artifact &&
 | 
			
		||||
        artifact.codeRef &&
 | 
			
		||||
        isOverlap(range, artifact.codeRef.range)
 | 
			
		||||
      ) {
 | 
			
		||||
        if (commandTypeToTarget === artifact.type) return artifactId
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -297,14 +297,20 @@ export const lineTo: SketchLineHelper = {
 | 
			
		||||
  add: ({ node, pathToNode, segmentInput, replaceExistingCallback }) => {
 | 
			
		||||
    if (segmentInput.type !== 'straight-segment') return STRAIGHT_SEGMENT_ERR
 | 
			
		||||
    const to = segmentInput.to
 | 
			
		||||
    const _node = { ...node }
 | 
			
		||||
    const _node = structuredClone(node)
 | 
			
		||||
    const nodeMeta = getNodeFromPath<PipeExpression>(
 | 
			
		||||
      _node,
 | 
			
		||||
      pathToNode,
 | 
			
		||||
      'PipeExpression'
 | 
			
		||||
    )
 | 
			
		||||
    if (err(nodeMeta)) return nodeMeta
 | 
			
		||||
    const { node: pipe } = nodeMeta
 | 
			
		||||
    const varDec = getNodeFromPath<VariableDeclaration>(
 | 
			
		||||
      _node,
 | 
			
		||||
      pathToNode,
 | 
			
		||||
      'VariableDeclaration'
 | 
			
		||||
    )
 | 
			
		||||
    if (err(varDec)) return varDec
 | 
			
		||||
    const dec = varDec.node.declaration
 | 
			
		||||
 | 
			
		||||
    const newVals: [Expr, Expr] = [
 | 
			
		||||
      createLiteral(roundOff(to[0], 2)),
 | 
			
		||||
@ -333,14 +339,20 @@ export const lineTo: SketchLineHelper = {
 | 
			
		||||
      ])
 | 
			
		||||
      if (err(result)) return result
 | 
			
		||||
      const { callExp, valueUsedInTransform } = result
 | 
			
		||||
      pipe.body[callIndex] = callExp
 | 
			
		||||
      if (dec.init.type === 'PipeExpression') {
 | 
			
		||||
        dec.init.body[callIndex] = callExp
 | 
			
		||||
      } else {
 | 
			
		||||
        dec.init = callExp
 | 
			
		||||
      }
 | 
			
		||||
      return {
 | 
			
		||||
        modifiedAst: _node,
 | 
			
		||||
        pathToNode,
 | 
			
		||||
        valueUsedInTransform: valueUsedInTransform,
 | 
			
		||||
      }
 | 
			
		||||
    } else if (dec.init.type === 'PipeExpression') {
 | 
			
		||||
      dec.init.body = [...dec.init.body, newLine]
 | 
			
		||||
    } else {
 | 
			
		||||
      pipe.body = [...pipe.body, newLine]
 | 
			
		||||
      dec.init = createPipeExpression([dec.init, newLine])
 | 
			
		||||
    }
 | 
			
		||||
    return {
 | 
			
		||||
      modifiedAst: _node,
 | 
			
		||||
@ -663,11 +675,11 @@ export const xLine: SketchLineHelper = {
 | 
			
		||||
  add: ({ node, pathToNode, segmentInput, replaceExistingCallback }) => {
 | 
			
		||||
    if (segmentInput.type !== 'straight-segment') return STRAIGHT_SEGMENT_ERR
 | 
			
		||||
    const { from, to } = segmentInput
 | 
			
		||||
    const _node = { ...node }
 | 
			
		||||
    const _node = structuredClone(node)
 | 
			
		||||
    const getNode = getNodeFromPathCurry(_node, pathToNode)
 | 
			
		||||
    const _node1 = getNode<PipeExpression>('PipeExpression')
 | 
			
		||||
    if (err(_node1)) return _node1
 | 
			
		||||
    const { node: pipe } = _node1
 | 
			
		||||
    const varDec = getNode<VariableDeclaration>('VariableDeclaration')
 | 
			
		||||
    if (err(varDec)) return varDec
 | 
			
		||||
    const dec = varDec.node.declaration
 | 
			
		||||
 | 
			
		||||
    const newVal = createLiteral(roundOff(to[0] - from[0], 2))
 | 
			
		||||
 | 
			
		||||
@ -682,7 +694,11 @@ export const xLine: SketchLineHelper = {
 | 
			
		||||
      ])
 | 
			
		||||
      if (err(result)) return result
 | 
			
		||||
      const { callExp, valueUsedInTransform } = result
 | 
			
		||||
      pipe.body[callIndex] = callExp
 | 
			
		||||
      if (dec.init.type === 'PipeExpression') {
 | 
			
		||||
        dec.init.body[callIndex] = callExp
 | 
			
		||||
      } else {
 | 
			
		||||
        dec.init = callExp
 | 
			
		||||
      }
 | 
			
		||||
      return {
 | 
			
		||||
        modifiedAst: _node,
 | 
			
		||||
        pathToNode,
 | 
			
		||||
@ -694,7 +710,11 @@ export const xLine: SketchLineHelper = {
 | 
			
		||||
      newVal,
 | 
			
		||||
      createPipeSubstitution(),
 | 
			
		||||
    ])
 | 
			
		||||
    pipe.body = [...pipe.body, newLine]
 | 
			
		||||
    if (dec.init.type === 'PipeExpression') {
 | 
			
		||||
      dec.init.body = [...dec.init.body, newLine]
 | 
			
		||||
    } else {
 | 
			
		||||
      dec.init = createPipeExpression([dec.init, newLine])
 | 
			
		||||
    }
 | 
			
		||||
    return { modifiedAst: _node, pathToNode }
 | 
			
		||||
  },
 | 
			
		||||
  updateArgs: ({ node, pathToNode, input }) => {
 | 
			
		||||
@ -731,11 +751,11 @@ export const yLine: SketchLineHelper = {
 | 
			
		||||
  add: ({ node, pathToNode, segmentInput, replaceExistingCallback }) => {
 | 
			
		||||
    if (segmentInput.type !== 'straight-segment') return STRAIGHT_SEGMENT_ERR
 | 
			
		||||
    const { from, to } = segmentInput
 | 
			
		||||
    const _node = { ...node }
 | 
			
		||||
    const _node = structuredClone(node)
 | 
			
		||||
    const getNode = getNodeFromPathCurry(_node, pathToNode)
 | 
			
		||||
    const _node1 = getNode<PipeExpression>('PipeExpression')
 | 
			
		||||
    if (err(_node1)) return _node1
 | 
			
		||||
    const { node: pipe } = _node1
 | 
			
		||||
    const varDec = getNode<VariableDeclaration>('VariableDeclaration')
 | 
			
		||||
    if (err(varDec)) return varDec
 | 
			
		||||
    const dec = varDec.node.declaration
 | 
			
		||||
    const newVal = createLiteral(roundOff(to[1] - from[1], 2))
 | 
			
		||||
    if (replaceExistingCallback) {
 | 
			
		||||
      const { index: callIndex } = splitPathAtPipeExpression(pathToNode)
 | 
			
		||||
@ -748,7 +768,11 @@ export const yLine: SketchLineHelper = {
 | 
			
		||||
      ])
 | 
			
		||||
      if (err(result)) return result
 | 
			
		||||
      const { callExp, valueUsedInTransform } = result
 | 
			
		||||
      pipe.body[callIndex] = callExp
 | 
			
		||||
      if (dec.init.type === 'PipeExpression') {
 | 
			
		||||
        dec.init.body[callIndex] = callExp
 | 
			
		||||
      } else {
 | 
			
		||||
        dec.init = callExp
 | 
			
		||||
      }
 | 
			
		||||
      return {
 | 
			
		||||
        modifiedAst: _node,
 | 
			
		||||
        pathToNode,
 | 
			
		||||
@ -760,7 +784,11 @@ export const yLine: SketchLineHelper = {
 | 
			
		||||
      newVal,
 | 
			
		||||
      createPipeSubstitution(),
 | 
			
		||||
    ])
 | 
			
		||||
    pipe.body = [...pipe.body, newLine]
 | 
			
		||||
    if (dec.init.type === 'PipeExpression') {
 | 
			
		||||
      dec.init.body = [...dec.init.body, newLine]
 | 
			
		||||
    } else {
 | 
			
		||||
      dec.init = createPipeExpression([dec.init, newLine])
 | 
			
		||||
    }
 | 
			
		||||
    return { modifiedAst: _node, pathToNode }
 | 
			
		||||
  },
 | 
			
		||||
  updateArgs: ({ node, pathToNode, input }) => {
 | 
			
		||||
@ -2145,8 +2173,6 @@ function addTagToChamfer(
 | 
			
		||||
  if (err(variableDec)) return variableDec
 | 
			
		||||
  const isPipeExpression = pipeExpr.node.type === 'PipeExpression'
 | 
			
		||||
 | 
			
		||||
  console.log('pipeExpr', pipeExpr, variableDec)
 | 
			
		||||
  // const callExpr = isPipeExpression ? pipeExpr.node.body[pipeIndex] : variableDec.node.init
 | 
			
		||||
  const callExpr = isPipeExpression
 | 
			
		||||
    ? pipeExpr.node.body[pipeIndex]
 | 
			
		||||
    : variableDec.node.init
 | 
			
		||||
@ -2227,7 +2253,6 @@ function addTagToChamfer(
 | 
			
		||||
  if (isPipeExpression) {
 | 
			
		||||
    pipeExpr.node.body.splice(pipeIndex, 0, newExpressionToInsert)
 | 
			
		||||
  } else {
 | 
			
		||||
    console.log('yo', createPipeExpression([newExpressionToInsert, callExpr]))
 | 
			
		||||
    callExpr.arguments[1] = createPipeSubstitution()
 | 
			
		||||
    variableDec.node.init = createPipeExpression([
 | 
			
		||||
      newExpressionToInsert,
 | 
			
		||||
 | 
			
		||||
@ -9,20 +9,47 @@ import {
 | 
			
		||||
import { ArtifactGraph, filterArtifacts } from 'lang/std/artifactGraph'
 | 
			
		||||
import { isOverlap } from 'lib/utils'
 | 
			
		||||
 | 
			
		||||
export function updatePathToNodeFromMap(
 | 
			
		||||
  oldPath: PathToNode,
 | 
			
		||||
  pathToNodeMap: { [key: number]: PathToNode }
 | 
			
		||||
/**
 | 
			
		||||
 * Updates pathToNode body indices to account for the insertion of an expression
 | 
			
		||||
 * PathToNode expression is after the insertion index, that the body index is incremented
 | 
			
		||||
 * Negative insertion index means no insertion
 | 
			
		||||
 */
 | 
			
		||||
export function updatePathToNodePostExprInjection(
 | 
			
		||||
  pathToNode: PathToNode,
 | 
			
		||||
  exprInsertIndex: number
 | 
			
		||||
): PathToNode {
 | 
			
		||||
  const updatedPathToNode = structuredClone(oldPath)
 | 
			
		||||
  let max = 0
 | 
			
		||||
  Object.values(pathToNodeMap).forEach((path) => {
 | 
			
		||||
    const index = Number(path[1][0])
 | 
			
		||||
    if (index > max) {
 | 
			
		||||
      max = index
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
  updatedPathToNode[1][0] = max
 | 
			
		||||
  return updatedPathToNode
 | 
			
		||||
  if (exprInsertIndex < 0) return pathToNode
 | 
			
		||||
  const bodyIndex = Number(pathToNode[1][0])
 | 
			
		||||
  if (bodyIndex < exprInsertIndex) return pathToNode
 | 
			
		||||
  const clone = structuredClone(pathToNode)
 | 
			
		||||
  clone[1][0] = bodyIndex + 1
 | 
			
		||||
  return clone
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function updateSketchDetailsNodePaths({
 | 
			
		||||
  sketchEntryNodePath,
 | 
			
		||||
  sketchNodePaths,
 | 
			
		||||
  planeNodePath,
 | 
			
		||||
  exprInsertIndex,
 | 
			
		||||
}: {
 | 
			
		||||
  sketchEntryNodePath: PathToNode
 | 
			
		||||
  sketchNodePaths: Array<PathToNode>
 | 
			
		||||
  planeNodePath: PathToNode
 | 
			
		||||
  exprInsertIndex: number
 | 
			
		||||
}) {
 | 
			
		||||
  return {
 | 
			
		||||
    updatedSketchEntryNodePath: updatePathToNodePostExprInjection(
 | 
			
		||||
      sketchEntryNodePath,
 | 
			
		||||
      exprInsertIndex
 | 
			
		||||
    ),
 | 
			
		||||
    updatedSketchNodePaths: sketchNodePaths.map((path) =>
 | 
			
		||||
      updatePathToNodePostExprInjection(path, exprInsertIndex)
 | 
			
		||||
    ),
 | 
			
		||||
    updatedPlaneNodePath: updatePathToNodePostExprInjection(
 | 
			
		||||
      planeNodePath,
 | 
			
		||||
      exprInsertIndex
 | 
			
		||||
    ),
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function isCursorInSketchCommandRange(
 | 
			
		||||
@ -31,7 +58,7 @@ export function isCursorInSketchCommandRange(
 | 
			
		||||
): string | false {
 | 
			
		||||
  const overlappingEntries = filterArtifacts(
 | 
			
		||||
    {
 | 
			
		||||
      types: ['segment', 'path'],
 | 
			
		||||
      types: ['segment', 'path', 'plane'],
 | 
			
		||||
      predicate: (artifact) => {
 | 
			
		||||
        return selectionRanges.graphSelections.some(
 | 
			
		||||
          (selection) =>
 | 
			
		||||
 | 
			
		||||
@ -501,26 +501,6 @@ export const executor = async (
 | 
			
		||||
  if (programMemoryOverride !== null && err(programMemoryOverride))
 | 
			
		||||
    return Promise.reject(programMemoryOverride)
 | 
			
		||||
 | 
			
		||||
  // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
  engineCommandManager.startNewSession()
 | 
			
		||||
  const _programMemory = await _executor(
 | 
			
		||||
    node,
 | 
			
		||||
    engineCommandManager,
 | 
			
		||||
    programMemoryOverride
 | 
			
		||||
  )
 | 
			
		||||
  await engineCommandManager.waitForAllCommands()
 | 
			
		||||
 | 
			
		||||
  return _programMemory
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const _executor = async (
 | 
			
		||||
  node: Node<Program>,
 | 
			
		||||
  engineCommandManager: EngineCommandManager,
 | 
			
		||||
  programMemoryOverride: ProgramMemory | Error | null = null
 | 
			
		||||
): Promise<ExecState> => {
 | 
			
		||||
  if (programMemoryOverride !== null && err(programMemoryOverride))
 | 
			
		||||
    return Promise.reject(programMemoryOverride)
 | 
			
		||||
 | 
			
		||||
  try {
 | 
			
		||||
    let jsAppSettings = default_app_settings()
 | 
			
		||||
    if (!TEST) {
 | 
			
		||||
 | 
			
		||||
@ -29,7 +29,7 @@ import {
 | 
			
		||||
 */
 | 
			
		||||
export const getRectangleCallExpressions = (
 | 
			
		||||
  rectangleOrigin: [number, number],
 | 
			
		||||
  tags: [string, string, string]
 | 
			
		||||
  tag: string
 | 
			
		||||
) => [
 | 
			
		||||
  createCallExpressionStdLib('angledLine', [
 | 
			
		||||
    createArrayExpression([
 | 
			
		||||
@ -37,30 +37,28 @@ export const getRectangleCallExpressions = (
 | 
			
		||||
      createLiteral(0), // This will be the width of the rectangle
 | 
			
		||||
    ]),
 | 
			
		||||
    createPipeSubstitution(),
 | 
			
		||||
    createTagDeclarator(tags[0]),
 | 
			
		||||
    createTagDeclarator(tag),
 | 
			
		||||
  ]),
 | 
			
		||||
  createCallExpressionStdLib('angledLine', [
 | 
			
		||||
    createArrayExpression([
 | 
			
		||||
      createBinaryExpression([
 | 
			
		||||
        createCallExpressionStdLib('segAng', [createIdentifier(tags[0])]),
 | 
			
		||||
        createCallExpressionStdLib('segAng', [createIdentifier(tag)]),
 | 
			
		||||
        '+',
 | 
			
		||||
        createLiteral(90),
 | 
			
		||||
      ]), // 90 offset from the previous line
 | 
			
		||||
      createLiteral(0), // This will be the height of the rectangle
 | 
			
		||||
    ]),
 | 
			
		||||
    createPipeSubstitution(),
 | 
			
		||||
    createTagDeclarator(tags[1]),
 | 
			
		||||
  ]),
 | 
			
		||||
  createCallExpressionStdLib('angledLine', [
 | 
			
		||||
    createArrayExpression([
 | 
			
		||||
      createCallExpressionStdLib('segAng', [createIdentifier(tags[0])]), // same angle as the first line
 | 
			
		||||
      createCallExpressionStdLib('segAng', [createIdentifier(tag)]), // same angle as the first line
 | 
			
		||||
      createUnaryExpression(
 | 
			
		||||
        createCallExpressionStdLib('segLen', [createIdentifier(tags[0])]),
 | 
			
		||||
        createCallExpressionStdLib('segLen', [createIdentifier(tag)]),
 | 
			
		||||
        '-'
 | 
			
		||||
      ), // negative height
 | 
			
		||||
    ]),
 | 
			
		||||
    createPipeSubstitution(),
 | 
			
		||||
    createTagDeclarator(tags[2]),
 | 
			
		||||
  ]),
 | 
			
		||||
  createCallExpressionStdLib('lineTo', [
 | 
			
		||||
    createArrayExpression([
 | 
			
		||||
@ -85,12 +83,12 @@ export function updateRectangleSketch(
 | 
			
		||||
  y: number,
 | 
			
		||||
  tag: string
 | 
			
		||||
) {
 | 
			
		||||
  ;((pipeExpression.body[2] as CallExpression)
 | 
			
		||||
  ;((pipeExpression.body[1] as CallExpression)
 | 
			
		||||
    .arguments[0] as ArrayExpression) = createArrayExpression([
 | 
			
		||||
    createLiteral(x >= 0 ? 0 : 180),
 | 
			
		||||
    createLiteral(Math.abs(x)),
 | 
			
		||||
  ])
 | 
			
		||||
  ;((pipeExpression.body[3] as CallExpression)
 | 
			
		||||
  ;((pipeExpression.body[2] as CallExpression)
 | 
			
		||||
    .arguments[0] as ArrayExpression) = createArrayExpression([
 | 
			
		||||
    createBinaryExpression([
 | 
			
		||||
      createCallExpressionStdLib('segAng', [createIdentifier(tag)]),
 | 
			
		||||
@ -120,7 +118,7 @@ export function updateCenterRectangleSketch(
 | 
			
		||||
  let startY = originY - Math.abs(deltaY)
 | 
			
		||||
 | 
			
		||||
  // pipeExpression.body[1] is startProfileAt
 | 
			
		||||
  let callExpression = pipeExpression.body[1]
 | 
			
		||||
  let callExpression = pipeExpression.body[0]
 | 
			
		||||
  if (isCallExpression(callExpression)) {
 | 
			
		||||
    const arrayExpression = callExpression.arguments[0]
 | 
			
		||||
    if (isArrayExpression(arrayExpression)) {
 | 
			
		||||
@ -134,7 +132,7 @@ export function updateCenterRectangleSketch(
 | 
			
		||||
  const twoX = deltaX * 2
 | 
			
		||||
  const twoY = deltaY * 2
 | 
			
		||||
 | 
			
		||||
  callExpression = pipeExpression.body[2]
 | 
			
		||||
  callExpression = pipeExpression.body[1]
 | 
			
		||||
  if (isCallExpression(callExpression)) {
 | 
			
		||||
    const arrayExpression = callExpression.arguments[0]
 | 
			
		||||
    if (isArrayExpression(arrayExpression)) {
 | 
			
		||||
@ -148,7 +146,7 @@ export function updateCenterRectangleSketch(
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  callExpression = pipeExpression.body[3]
 | 
			
		||||
  callExpression = pipeExpression.body[2]
 | 
			
		||||
  if (isCallExpression(callExpression)) {
 | 
			
		||||
    const arrayExpression = callExpression.arguments[0]
 | 
			
		||||
    if (isArrayExpression(arrayExpression)) {
 | 
			
		||||
 | 
			
		||||
@ -280,18 +280,19 @@ export function getEventForSegmentSelection(
 | 
			
		||||
  }
 | 
			
		||||
  if (!id || !group) return null
 | 
			
		||||
  const artifact = engineCommandManager.artifactGraph.get(id)
 | 
			
		||||
  const codeRefs = getCodeRefsByArtifactId(
 | 
			
		||||
    id,
 | 
			
		||||
    engineCommandManager.artifactGraph
 | 
			
		||||
  )
 | 
			
		||||
  if (!artifact || !codeRefs) return null
 | 
			
		||||
  if (!artifact) return null
 | 
			
		||||
  const node = getNodeFromPath<Expr>(kclManager.ast, group.userData.pathToNode)
 | 
			
		||||
  if (err(node)) return null
 | 
			
		||||
  return {
 | 
			
		||||
    type: 'Set selection',
 | 
			
		||||
    data: {
 | 
			
		||||
      selectionType: 'singleCodeCursor',
 | 
			
		||||
      selection: {
 | 
			
		||||
        artifact,
 | 
			
		||||
        codeRef: codeRefs[0],
 | 
			
		||||
        codeRef: {
 | 
			
		||||
          pathToNode: group?.userData?.pathToNode,
 | 
			
		||||
          range: [node.node.start, node.node.end, true],
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  }
 | 
			
		||||
@ -675,8 +676,7 @@ export function getSelectionTypeDisplayText(
 | 
			
		||||
  const selectionsByType = getSelectionCountByType(selection)
 | 
			
		||||
  if (selectionsByType === 'none') return null
 | 
			
		||||
 | 
			
		||||
  return selectionsByType
 | 
			
		||||
    .entries()
 | 
			
		||||
  return [...selectionsByType.entries()]
 | 
			
		||||
    .map(
 | 
			
		||||
      // Hack for showing "face" instead of "extrude-wall" in command bar text
 | 
			
		||||
      ([type, count]) =>
 | 
			
		||||
@ -685,7 +685,6 @@ export function getSelectionTypeDisplayText(
 | 
			
		||||
          .replace('solid2D', 'face')
 | 
			
		||||
          .replace('segment', 'face')}${count > 1 ? 's' : ''}`
 | 
			
		||||
    )
 | 
			
		||||
    .toArray()
 | 
			
		||||
    .join(', ')
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -695,7 +694,7 @@ export function canSubmitSelectionArg(
 | 
			
		||||
) {
 | 
			
		||||
  return (
 | 
			
		||||
    selectionsByType !== 'none' &&
 | 
			
		||||
    selectionsByType.entries().every(([type, count]) => {
 | 
			
		||||
    [...selectionsByType.entries()].every(([type, count]) => {
 | 
			
		||||
      const foundIndex = argument.selectionTypes.findIndex((s) => s === type)
 | 
			
		||||
      return (
 | 
			
		||||
        foundIndex !== -1 &&
 | 
			
		||||
@ -718,7 +717,7 @@ export function codeToIdSelections(
 | 
			
		||||
      // TODO #868: loops over all artifacts will become inefficient at a large scale
 | 
			
		||||
      const overlappingEntries = Array.from(engineCommandManager.artifactGraph)
 | 
			
		||||
        .map(([id, artifact]) => {
 | 
			
		||||
          if (!('codeRef' in artifact)) return null
 | 
			
		||||
          if (!('codeRef' in artifact && artifact.codeRef)) return null
 | 
			
		||||
          return isOverlap(artifact.codeRef.range, selection.range)
 | 
			
		||||
            ? {
 | 
			
		||||
                artifact,
 | 
			
		||||
@ -961,7 +960,6 @@ export function updateSelections(
 | 
			
		||||
            JSON.stringify(pathToNode)
 | 
			
		||||
          ) {
 | 
			
		||||
            artifact = a
 | 
			
		||||
            console.log('found artifact', a)
 | 
			
		||||
            break
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -1,20 +1,16 @@
 | 
			
		||||
import {
 | 
			
		||||
  Program,
 | 
			
		||||
  ProgramMemory,
 | 
			
		||||
  _executor,
 | 
			
		||||
  executor,
 | 
			
		||||
  SourceRange,
 | 
			
		||||
  ExecState,
 | 
			
		||||
} from '../lang/wasm'
 | 
			
		||||
import {
 | 
			
		||||
  EngineCommandManager,
 | 
			
		||||
  EngineCommandManagerEvents,
 | 
			
		||||
} from 'lang/std/engineConnection'
 | 
			
		||||
import { EngineCommandManager } from 'lang/std/engineConnection'
 | 
			
		||||
import { EngineCommand } from 'lang/std/artifactGraph'
 | 
			
		||||
import { Models } from '@kittycad/lib'
 | 
			
		||||
import { v4 as uuidv4 } from 'uuid'
 | 
			
		||||
import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
 | 
			
		||||
import { err, reportRejection } from 'lib/trap'
 | 
			
		||||
import { toSync } from './utils'
 | 
			
		||||
import { err } from 'lib/trap'
 | 
			
		||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
 | 
			
		||||
 | 
			
		||||
type WebSocketResponse = Models['WebSocketResponse_type']
 | 
			
		||||
@ -94,36 +90,7 @@ export async function enginelessExecutor(
 | 
			
		||||
  }) as any as EngineCommandManager
 | 
			
		||||
  // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
  mockEngineCommandManager.startNewSession()
 | 
			
		||||
  const execState = await _executor(ast, mockEngineCommandManager, pmo)
 | 
			
		||||
  const execState = await executor(ast, mockEngineCommandManager, pmo)
 | 
			
		||||
  await mockEngineCommandManager.waitForAllCommands()
 | 
			
		||||
  return execState
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function executor(
 | 
			
		||||
  ast: Node<Program>,
 | 
			
		||||
  pmo: ProgramMemory = ProgramMemory.empty()
 | 
			
		||||
): Promise<ExecState> {
 | 
			
		||||
  const engineCommandManager = new EngineCommandManager()
 | 
			
		||||
  engineCommandManager.start({
 | 
			
		||||
    setIsStreamReady: () => {},
 | 
			
		||||
    setMediaStream: () => {},
 | 
			
		||||
    width: 0,
 | 
			
		||||
    height: 0,
 | 
			
		||||
    makeDefaultPlanes: () => {
 | 
			
		||||
      return new Promise((resolve) => resolve(defaultPlanes))
 | 
			
		||||
    },
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  return new Promise((resolve) => {
 | 
			
		||||
    engineCommandManager.addEventListener(
 | 
			
		||||
      EngineCommandManagerEvents.SceneReady,
 | 
			
		||||
      toSync(async () => {
 | 
			
		||||
        // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
        engineCommandManager.startNewSession()
 | 
			
		||||
        const execState = await _executor(ast, engineCommandManager, pmo)
 | 
			
		||||
        await engineCommandManager.waitForAllCommands()
 | 
			
		||||
        resolve(execState)
 | 
			
		||||
      }, reportRejection)
 | 
			
		||||
    )
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -2,8 +2,6 @@ import { CustomIconName } from 'components/CustomIcon'
 | 
			
		||||
import { DEV } from 'env'
 | 
			
		||||
import { commandBarMachine } from 'machines/commandBarMachine'
 | 
			
		||||
import {
 | 
			
		||||
  canRectangleOrCircleTool,
 | 
			
		||||
  isClosedSketch,
 | 
			
		||||
  isEditingExistingSketch,
 | 
			
		||||
  modelingMachine,
 | 
			
		||||
  pipeHasCircle,
 | 
			
		||||
@ -73,7 +71,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
 | 
			
		||||
        status: 'available',
 | 
			
		||||
        disabled: (state) => !state.matches('idle'),
 | 
			
		||||
        title: ({ sketchPathId }) =>
 | 
			
		||||
          `${sketchPathId ? 'Edit' : 'Start'} Sketch`,
 | 
			
		||||
          sketchPathId ? 'Edit Sketch' : 'Start Sketch',
 | 
			
		||||
        showTitle: true,
 | 
			
		||||
        hotkey: 'S',
 | 
			
		||||
        description: 'Start drawing a 2D sketch',
 | 
			
		||||
@ -315,22 +313,14 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
 | 
			
		||||
      {
 | 
			
		||||
        id: 'line',
 | 
			
		||||
        onClick: ({ modelingState, modelingSend }) => {
 | 
			
		||||
          if (modelingState.matches({ Sketch: { 'Line tool': 'No Points' } })) {
 | 
			
		||||
            // Exit the sketch state if there are no points and they press ESC
 | 
			
		||||
            modelingSend({
 | 
			
		||||
              type: 'Cancel',
 | 
			
		||||
            })
 | 
			
		||||
          } else {
 | 
			
		||||
            // Exit the tool if there are points and they press ESC
 | 
			
		||||
            modelingSend({
 | 
			
		||||
              type: 'change tool',
 | 
			
		||||
              data: {
 | 
			
		||||
                tool: !modelingState.matches({ Sketch: 'Line tool' })
 | 
			
		||||
                  ? 'line'
 | 
			
		||||
                  : 'none',
 | 
			
		||||
              },
 | 
			
		||||
            })
 | 
			
		||||
          }
 | 
			
		||||
          modelingSend({
 | 
			
		||||
            type: 'change tool',
 | 
			
		||||
            data: {
 | 
			
		||||
              tool: !modelingState.matches({ Sketch: 'Line tool' })
 | 
			
		||||
                ? 'line'
 | 
			
		||||
                : 'none',
 | 
			
		||||
            },
 | 
			
		||||
          })
 | 
			
		||||
        },
 | 
			
		||||
        icon: 'line',
 | 
			
		||||
        status: 'available',
 | 
			
		||||
@ -341,8 +331,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
 | 
			
		||||
          }) ||
 | 
			
		||||
          state.matches({
 | 
			
		||||
            Sketch: { 'Circle tool': 'Awaiting Radius' },
 | 
			
		||||
          }) ||
 | 
			
		||||
          isClosedSketch(state.context),
 | 
			
		||||
          }),
 | 
			
		||||
        title: 'Line',
 | 
			
		||||
        hotkey: (state) =>
 | 
			
		||||
          state.matches({ Sketch: 'Line tool' }) ? ['Esc', 'L'] : 'L',
 | 
			
		||||
@ -422,10 +411,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
 | 
			
		||||
          icon: 'circle',
 | 
			
		||||
          status: 'available',
 | 
			
		||||
          title: 'Center circle',
 | 
			
		||||
          disabled: (state) =>
 | 
			
		||||
            state.matches('Sketch no face') ||
 | 
			
		||||
            (!canRectangleOrCircleTool(state.context) &&
 | 
			
		||||
              !state.matches({ Sketch: 'Circle tool' })),
 | 
			
		||||
          disabled: (state) => state.matches('Sketch no face'),
 | 
			
		||||
          isActive: (state) => state.matches({ Sketch: 'Circle tool' }),
 | 
			
		||||
          hotkey: (state) =>
 | 
			
		||||
            state.matches({ Sketch: 'Circle tool' }) ? ['Esc', 'C'] : 'C',
 | 
			
		||||
@ -464,10 +450,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
 | 
			
		||||
            }),
 | 
			
		||||
          icon: 'rectangle',
 | 
			
		||||
          status: 'available',
 | 
			
		||||
          disabled: (state) =>
 | 
			
		||||
            state.matches('Sketch no face') ||
 | 
			
		||||
            (!canRectangleOrCircleTool(state.context) &&
 | 
			
		||||
              !state.matches({ Sketch: 'Rectangle tool' })),
 | 
			
		||||
          disabled: (state) => state.matches('Sketch no face'),
 | 
			
		||||
          title: 'Corner rectangle',
 | 
			
		||||
          hotkey: (state) =>
 | 
			
		||||
            state.matches({ Sketch: 'Rectangle tool' }) ? ['Esc', 'R'] : 'R',
 | 
			
		||||
@ -490,10 +473,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
 | 
			
		||||
            }),
 | 
			
		||||
          icon: 'arc',
 | 
			
		||||
          status: 'available',
 | 
			
		||||
          disabled: (state) =>
 | 
			
		||||
            state.matches('Sketch no face') ||
 | 
			
		||||
            (!canRectangleOrCircleTool(state.context) &&
 | 
			
		||||
              !state.matches({ Sketch: 'Center Rectangle tool' })),
 | 
			
		||||
          disabled: (state) => state.matches('Sketch no face'),
 | 
			
		||||
          title: 'Center rectangle',
 | 
			
		||||
          hotkey: (state) =>
 | 
			
		||||
            state.matches({ Sketch: 'Center Rectangle tool' })
 | 
			
		||||
 | 
			
		||||
@ -97,3 +97,7 @@ export function trap<T>(
 | 
			
		||||
    })
 | 
			
		||||
  return true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function reject(errOrString: Error | string): Promise<never> {
 | 
			
		||||
  return Promise.reject(errOrString)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||