diff --git a/e2e/playwright/basic-sketch.spec.ts b/e2e/playwright/basic-sketch.spec.ts index cd3fad58f8..4e024cc3ef 100644 --- a/e2e/playwright/basic-sketch.spec.ts +++ b/e2e/playwright/basic-sketch.spec.ts @@ -57,26 +57,23 @@ 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).toContainText( - `sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${commonPoints.startAt}, sketch001)` - ) + await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ') + |> startProfileAt(${commonPoints.startAt}, %)`) } 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')profile001 = startProfileAt(${commonPoints.startAt}, sketch001) + await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ') + |> startProfileAt(${commonPoints.startAt}, %) |> 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')profile001 = startProfileAt(${ - commonPoints.startAt - }, sketch001) + await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ') + |> startProfileAt(${commonPoints.startAt}, %) |> xLine(${commonPoints.num1}, %) |> yLine(${commonPoints.num1 + 0.01}, %)`) } else { @@ -85,10 +82,8 @@ 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')profile001 = startProfileAt(${ - commonPoints.startAt - }, sketch001) + await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ') + |> startProfileAt(${commonPoints.startAt}, %) |> xLine(${commonPoints.num1}, %) |> yLine(${commonPoints.num1 + 0.01}, %) |> xLine(${commonPoints.num2 * -1}, %)`) @@ -145,10 +140,8 @@ async function doBasicSketch(page: Page, openPanes: string[]) { // Open the code pane. await u.openKclCodePanel() - await expect(u.codeLocator) - .toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${ - commonPoints.startAt - }, sketch001) + await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ') + |> startProfileAt(${commonPoints.startAt}, %) |> xLine(${commonPoints.num1}, %, $seg01) |> yLine(${commonPoints.num1 + 0.01}, %) |> xLine(-segLen(seg01), %)`) diff --git a/e2e/playwright/can-create-sketches-on-all-planes-and-their-back-sides.spec.ts b/e2e/playwright/can-create-sketches-on-all-planes-and-their-back-sides.spec.ts index 4eb77be192..fab0140154 100644 --- a/e2e/playwright/can-create-sketches-on-all-planes-and-their-back-sides.spec.ts +++ b/e2e/playwright/can-create-sketches-on-all-planes-and-their-back-sides.spec.ts @@ -44,7 +44,8 @@ test.describe('Can create sketches on all planes and their back sides', () => { }, } - const code = `sketch001 = startSketchOn('${plane}')profile001 = startProfileAt([0.9, -1.22], sketch001)` + const code = `sketch001 = startSketchOn('${plane}') + |> startProfileAt([0.9, -1.22], %)` await u.openDebugPanel() diff --git a/e2e/playwright/fixtures/sceneFixture.ts b/e2e/playwright/fixtures/sceneFixture.ts index 59426cb486..fff1d36e35 100644 --- a/e2e/playwright/fixtures/sceneFixture.ts +++ b/e2e/playwright/fixtures/sceneFixture.ts @@ -11,7 +11,6 @@ import { type mouseParams = { pixelDiff?: number - shouldDbClick?: boolean } type mouseDragToParams = mouseParams & { fromPoint: { x: number; y: number } @@ -76,16 +75,11 @@ export class SceneFixture { if (clickParams?.pixelDiff) { return doAndWaitForImageDiff( this.page, - () => - clickParams?.shouldDbClick - ? this.page.mouse.dblclick(x, y) - : this.page.mouse.click(x, y), + () => this.page.mouse.click(x, y), clickParams.pixelDiff ) } - return clickParams?.shouldDbClick - ? this.page.mouse.dblclick(x, y) - : this.page.mouse.click(x, y) + return this.page.mouse.click(x, y) }, (moveParams?: mouseParams) => { if (moveParams?.pixelDiff) { @@ -216,7 +210,7 @@ export class SceneFixture { } expectPixelColor = async ( - colour: [number, number, number] | [number, number, number][], + colour: [number, number, number], coords: { x: number; y: number }, diff: number ) => { @@ -237,36 +231,22 @@ 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] | [number, number, number][], + colour: [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 - 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 } - ) + .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 + ) + }) .toBeTruthy() .catch((cause) => { throw new Error( diff --git a/e2e/playwright/fixtures/toolbarFixture.ts b/e2e/playwright/fixtures/toolbarFixture.ts index 484f26e92c..43cf577e14 100644 --- a/e2e/playwright/fixtures/toolbarFixture.ts +++ b/e2e/playwright/fixtures/toolbarFixture.ts @@ -11,10 +11,7 @@ export class ToolbarFixture { offsetPlaneButton!: Locator startSketchBtn!: Locator lineBtn!: Locator - tangentialArcBtn!: Locator - circleBtn!: Locator rectangleBtn!: Locator - lengthConstraintBtn!: Locator exitSketchBtn!: Locator editSketchBtn!: Locator fileTreeBtn!: Locator @@ -36,10 +33,7 @@ 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"]') @@ -97,13 +91,4 @@ 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() - } } diff --git a/e2e/playwright/onboarding-tests.spec.ts b/e2e/playwright/onboarding-tests.spec.ts index c722f120ad..9c1652be6f 100644 --- a/e2e/playwright/onboarding-tests.spec.ts +++ b/e2e/playwright/onboarding-tests.spec.ts @@ -425,7 +425,7 @@ test.describe('Onboarding tests', () => { }) }) -test( +test.fixme( 'Restarting onboarding on desktop takes one attempt', { tag: '@electron' }, async ({ browser: _ }, testInfo) => { diff --git a/e2e/playwright/point-click.spec.ts b/e2e/playwright/point-click.spec.ts index 7ee02695d4..6073ad6b6c 100644 --- a/e2e/playwright/point-click.spec.ts +++ b/e2e/playwright/point-click.spec.ts @@ -135,9 +135,7 @@ test.describe('verify sketch on chamfer works', () => { pixelDiff: 50, }) await rectangle2ndClick() - await editor.expectEditor.toContain(afterRectangle2ndClickSnippet, { - shouldNormalise: true, - }) + await editor.expectEditor.toContain(afterRectangle2ndClickSnippet) }) await test.step('Clean up so that `_sketchOnAChamfer` util can be called again', async () => { @@ -179,13 +177,18 @@ test.describe('verify sketch on chamfer works', () => { afterChamferSelectSnippet: 'sketch002 = startSketchOn(extrude001, seg03)', - 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(%)`, + 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(%)`, }) await sketchOnAChamfer({ @@ -206,15 +209,19 @@ test.describe('verify sketch on chamfer works', () => { afterChamferSelectSnippet: 'sketch003 = startSketchOn(extrude001, seg04)', - 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(%)`, + 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(%)`, }) - await sketchOnAChamfer({ clickCoords: { x: 677, y: 87 }, cameraPos: { x: -6200, y: 1500, z: 6200 }, @@ -227,14 +234,19 @@ test.describe('verify sketch on chamfer works', () => { ] }, %)`, afterChamferSelectSnippet: - '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(%)`, + '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(%)`, }) /// last one await sketchOnAChamfer({ @@ -247,97 +259,104 @@ test.describe('verify sketch on chamfer works', () => { }, %)`, afterChamferSelectSnippet: 'sketch005 = startSketchOn(extrude001, seg06)', - 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(%)`, + 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(%)`, }) 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) -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(%) -`, + + |> 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(%) + `, { shouldNormalise: true } ) }) @@ -373,13 +392,18 @@ profile001 = startProfileAt([205.96, 254.59], sketch002) beforeChamferSnippetEnd: '}, extrude001)', afterChamferSelectSnippet: 'sketch002 = startSketchOn(extrude001, seg03)', - 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(%)`, + 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(%)`, }) await editor.expectEditor.toContain( `sketch001 = startSketchOn('XZ') @@ -409,16 +433,16 @@ chamf = chamfer({ ] }, %) sketch002 = startSketchOn(extrude001, seg03) -profile001 = startProfileAt([205.96, 254.59], sketch002) + |> 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(%) `, @@ -480,10 +504,10 @@ test(`Verify axis, origin, and horizontal snapping`, async ({ const expectedCodeSnippets = { sketchOnXzPlane: `sketch001 = startSketchOn('XZ')`, - pointAtOrigin: `startProfileAt([${originSloppy.kcl[0]}, ${originSloppy.kcl[1]}], sketch001)`, + pointAtOrigin: `startProfileAt([${originSloppy.kcl[0]}, ${originSloppy.kcl[1]}], %)`, segmentOnXAxis: `xLine(${xAxisSloppy.kcl[0]}, %)`, - afterSegmentDraggedOffYAxis: `startProfileAt([${offYAxis.kcl[0]}, ${offYAxis.kcl[1]}], sketch001)`, - afterSegmentDraggedOnYAxis: `startProfileAt([${yAxisSloppy.kcl[0]}, ${yAxisSloppy.kcl[1]}], sketch001)`, + afterSegmentDraggedOffYAxis: `startProfileAt([${offYAxis.kcl[0]}, ${offYAxis.kcl[1]}], %)`, + afterSegmentDraggedOnYAxis: `startProfileAt([${yAxisSloppy.kcl[0]}, ${yAxisSloppy.kcl[1]}], %)`, } await app.initialise() diff --git a/e2e/playwright/sketch-tests.spec.ts b/e2e/playwright/sketch-tests.spec.ts index 6c16fbb78a..e96ad60a50 100644 --- a/e2e/playwright/sketch-tests.spec.ts +++ b/e2e/playwright/sketch-tests.spec.ts @@ -7,7 +7,6 @@ import { PERSIST_MODELING_CONTEXT, setup, tearDown, - TEST_COLORS, } from './test-utils' import { uuidv4, roundOff } from 'lib/utils' @@ -115,9 +114,9 @@ test.describe('Sketch tests', () => { localStorage.setItem( 'persistCode', `sketch001 = startSketchOn('XZ') - |> startProfileAt([2.61, -4.01], %) - |> xLine(8.73, %) - |> tangentialArcTo([8.33, -1.31], %)` + |> startProfileAt([4.61, -14.01], %) + |> xLine(12.73, %) + |> tangentialArcTo([24.95, -5.38], %)` ) }) @@ -127,7 +126,7 @@ test.describe('Sketch tests', () => { await expect(async () => { await page.mouse.click(700, 200) - await page.getByText('tangentialArcTo([8.33, -1.31], %)').click() + await page.getByText('tangentialArcTo([24.95, -5.38], %)').click() await expect( page.getByRole('button', { name: 'Edit Sketch' }) ).toBeEnabled({ timeout: 1000 }) @@ -136,7 +135,7 @@ test.describe('Sketch tests', () => { await page.waitForTimeout(600) // wait for animation - await page.getByText('tangentialArcTo([8.33, -1.31], %)').click() + await page.getByText('tangentialArcTo([24.95, -5.38], %)').click() await page.keyboard.press('End') await page.keyboard.down('Shift') await page.keyboard.press('ArrowUp') @@ -150,21 +149,17 @@ test.describe('Sketch tests', () => { await page.getByRole('button', { name: 'line Line', exact: true }).click() await page.waitForTimeout(100) - // click start profileAt handle to continue profile - await page.mouse.click(702, 407) - await page.waitForTimeout(100) await expect(async () => { - // click to add segment await page.mouse.click(700, 200) await expect.poll(u.normalisedEditorCode, { timeout: 1000 }) - .toBe(`sketch002 = startSketchOn('XZ') -sketch001 = startProfileAt([12.34, -12.34], sketch002) + .toBe(`sketch001 = startSketchOn('XZ') + |> startProfileAt([12.34, -12.34], %) |> yLine(12.34, %) `) - }).toPass({ timeout: 5_000, intervals: [1_000] }) + }).toPass({ timeout: 40_000, intervals: [1_000] }) }) test('Can exit selection of face', async ({ page }) => { // Load the app with the code panes @@ -674,7 +669,7 @@ sketch001 = startProfileAt([12.34, -12.34], sketch002) await page.waitForTimeout(500) // TODO detect animation ending, or disable animation await click00r(0, 0) - codeStr += `profile001 = startProfileAt(${toSU([0, 0])}, sketch001)` + codeStr += ` |> startProfileAt(${toSU([0, 0])}, %)` await expect(u.codeLocator).toHaveText(codeStr) await click00r(50, 0) @@ -710,7 +705,7 @@ sketch001 = startProfileAt([12.34, -12.34], sketch002) await u.closeDebugPanel() await click00r(30, 0) - codeStr += `profile002 = startProfileAt([2.03, 0], sketch002)` + codeStr += ` |> startProfileAt([2.03, 0], %)` await expect(u.codeLocator).toHaveText(codeStr) // TODO: I couldn't use `toSU` here because of some rounding error causing @@ -747,9 +742,7 @@ sketch001 = startProfileAt([12.34, -12.34], sketch002) await u.openDebugPanel() const code = `sketch001 = startSketchOn('-XZ') -profile001 = startProfileAt([${roundOff(scale * 69.6)}, ${roundOff( - scale * 34.8 - )}], sketch001) + |> startProfileAt([${roundOff(scale * 69.6)}, ${roundOff(scale * 34.8)}], %) |> xLine(${roundOff(scale * 139.19)}, %) |> yLine(-${roundOff(scale * 139.2)}, %) |> lineTo([profileStartX(%), profileStartY(%)], %) @@ -815,17 +808,11 @@ profile001 = startProfileAt([${roundOff(scale * 69.6)}, ${roundOff( await expect(page.locator('.cm-content')).not.toHaveText(prevContent) prevContent = await page.locator('.cm-content').innerText() - await expect - .poll(async () => { - const text = await page.locator('.cm-content').innerText() - return text.replace(/\s/g, '') - }) - .toBe(code.replace(/\s/g, '')) - - // Assert the tool stays equipped after a profile is closed (ready for the next one) + await expect(page.locator('.cm-content')).toHaveText(code) + // Assert the tool was unequipped await expect( page.getByRole('button', { name: 'line Line', exact: true }) - ).toHaveAttribute('aria-pressed', 'true') + ).not.toHaveAttribute('aria-pressed', 'true') // exit sketch await u.openAndClearDebugPanel() @@ -1143,17 +1130,11 @@ sketch002 = startSketchOn(extrude001, 'END') await page.mouse.click(XYPlanePoint.x, XYPlanePoint.y) await page.waitForTimeout(200) await page.mouse.click(XYPlanePoint.x + 50, XYPlanePoint.y + 50) - await expect - .poll(async () => { - const text = await u.codeLocator.innerText() - return text.replace(/\s/g, '') - }) - .toBe( - `sketch001 = startSketchOn('XZ') -profile001 = startProfileAt([11.8, 9.09], sketch001) - |> line([3.39, -3.39], %) -`.replace(/\s/g, '') - ) + await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ') + |> startProfileAt([11.8, 9.09], %) + |> line([3.39, -3.39], %) +`) + await page.addInitScript(async () => { localStorage.setItem( 'persistCode', @@ -1351,7 +1332,7 @@ test2.describe('Sketch mode should be toleratant to syntax errors', () => { const [objClick] = scene.makeMouseHelpers(600, 250) const arrowHeadLocation = { x: 604, y: 129 } as const - const arrowHeadWhite = TEST_COLORS.WHITE + const arrowHeadWhite: [number, number, number] = [255, 255, 255] const backgroundGray: [number, number, number] = [28, 28, 28] const verifyArrowHeadColor = async (c: [number, number, number]) => scene.expectPixelColor(c, arrowHeadLocation, 15) @@ -1438,890 +1419,3 @@ test2.describe(`Sketching with offset planes`, () => { } ) }) - -test2.describe('multi-profile sketching', () => { - test2( - 'Can add multiple profiles to a sketch (all tool types)', - async ({ app, scene, toolbar, editor }) => { - await app.initialise(``) - - const [selectXZPlane] = scene.makeMouseHelpers(650, 150) - - const [startProfile1] = scene.makeMouseHelpers(568, 70) - const [endLineStartTanArc] = scene.makeMouseHelpers(701, 78) - const [endArcStartLine] = scene.makeMouseHelpers(745, 189) - - const [startProfile2] = scene.makeMouseHelpers(782, 80) - const [profile2Point2] = scene.makeMouseHelpers(921, 90) - const [profile2Point3] = scene.makeMouseHelpers(953, 178) - - const [circle1Center] = scene.makeMouseHelpers(842, 147) - const [circle1Radius] = scene.makeMouseHelpers(870, 171) - - const [circle2Center] = scene.makeMouseHelpers(850, 222) - const [circle2Radius] = scene.makeMouseHelpers(843, 230) - - const [crnRect1point1] = scene.makeMouseHelpers(583, 205) - const [crnRect1point2] = scene.makeMouseHelpers(618, 320) - - const [crnRect2point1] = scene.makeMouseHelpers(663, 215) - const [crnRect2point2] = scene.makeMouseHelpers(744, 276) - - const [cntrRect1point1] = scene.makeMouseHelpers(624, 387) - const [cntrRect1point2] = scene.makeMouseHelpers(676, 355) - - const [cntrRect2point1] = scene.makeMouseHelpers(785, 332) - const [cntrRect2point2] = scene.makeMouseHelpers(808, 286) - - await toolbar.startSketchPlaneSelection() - await selectXZPlane() - // timeout wait for engine animation is unavoidable - await app.page.waitForTimeout(600) - await editor.expectEditor.toContain(`sketch001 = startSketchOn('XZ')`) - await test.step('Create a close profile stopping mid profile to equip the tangential arc, and than back to the line tool', async () => { - await startProfile1() - await editor.expectEditor.toContain( - `profile001 = startProfileAt([4.61, 12.21], sketch001)` - ) - - await endLineStartTanArc() - await editor.expectEditor.toContain(`|> line([9.02, -0.55], %)`) - await toolbar.tangentialArcBtn.click() - await app.page.waitForTimeout(100) - await endLineStartTanArc() - - await endArcStartLine() - await editor.expectEditor.toContain( - `|> tangentialArcTo([16.61, 4.14], %)` - ) - await toolbar.lineBtn.click() - await app.page.waitForTimeout(100) - await endArcStartLine() - - await app.page.mouse.click(572, 110) - await editor.expectEditor.toContain(`|> line([-11.73, 5.35], %)`) - await startProfile1() - await editor.expectEditor - .toContain(`|> lineTo([profileStartX(%), profileStartY(%)], %) - |> close(%)`) - await app.page.waitForTimeout(100) - }) - - await test.step('Without unequipping from the last step, make another profile, and one that is not closed', async () => { - await startProfile2() - await editor.expectEditor.toContain( - `profile002 = startProfileAt([19.12, 11.53], sketch001)` - ) - await profile2Point2() - await editor.expectEditor.toContain(`|> line([9.43, -0.68], %)`) - await profile2Point3() - await editor.expectEditor.toContain(`|> line([2.17, -5.97], %)`) - }) - - await test.step('create two circles in a row without unequip', async () => { - await toolbar.circleBtn.click() - - await circle1Center() - await app.page.waitForTimeout(100) - await circle1Radius() - await editor.expectEditor.toContain( - `profile003 = circle({ center = [23.19, 6.98], radius = 2.5 }, sketch001)` - ) - - await test.step('hover in empty space to wait for overlays to get out of the way', async () => { - await app.page.mouse.move(951, 223) - await app.page.waitForTimeout(1000) - }) - - await circle2Center() - await app.page.waitForTimeout(100) - await circle2Radius() - await editor.expectEditor.toContain( - `profile004 = circle({ center = [23.74, 1.9], radius = 0.72 }, sketch001)` - ) - }) - await test.step('create two corner rectangles in a row without unequip', async () => { - await toolbar.rectangleBtn.click() - - await crnRect1point1() - await editor.expectEditor.toContain( - `profile005 = startProfileAt([5.63, 3.05], sketch001)` - ) - await crnRect1point2() - await editor.expectEditor - .toContain(`|> angledLine([0, 2.37], %, $rectangleSegmentA001) - |> angledLine([segAng(rectangleSegmentA001) - 90, 7.8], %) - |> angledLine([ - segAng(rectangleSegmentA001), - -segLen(rectangleSegmentA001) - ], %) - |> lineTo([profileStartX(%), profileStartY(%)], %) - |> close(%)`) - await app.page.waitForTimeout(100) - - await crnRect2point1() - await editor.expectEditor.toContain( - `profile006 = startProfileAt([11.05, 2.37], sketch001)` - ) - await crnRect2point2() - await editor.expectEditor - .toContain(`|> angledLine([0, 5.49], %, $rectangleSegmentA002) - |> angledLine([ - segAng(rectangleSegmentA002) - 90, - 4.14 - ], %) - |> angledLine([ - segAng(rectangleSegmentA002), - -segLen(rectangleSegmentA002) - ], %) - |> lineTo([profileStartX(%), profileStartY(%)], %) - |> close(%)`) - }) - - await test.step('create two center rectangles in a row without unequip', async () => { - await toolbar.selectCenterRectangle() - - await cntrRect1point1() - await editor.expectEditor.toContain( - `profile007 = startProfileAt([8.41, -9.29], sketch001)` - ) - await cntrRect1point2() - await editor.expectEditor - .toContain(`|> angledLine([0, 7.06], %, $rectangleSegmentA003) -|> angledLine([ - segAng(rectangleSegmentA003) + 90, - 4.34 - ], %) -|> angledLine([ - segAng(rectangleSegmentA003), - -segLen(rectangleSegmentA003) - ], %) -|> lineTo([profileStartX(%), profileStartY(%)], %) -|> close(%)`) - await app.page.waitForTimeout(100) - - await cntrRect2point1() - await editor.expectEditor.toContain( - `profile008 = startProfileAt([19.33, -5.56], sketch001)` - ) - await cntrRect2point2() - await editor.expectEditor - .toContain(`|> angledLine([0, 3.12], %, $rectangleSegmentA004) -|> angledLine([ - segAng(rectangleSegmentA004) + 90, - 6.24 - ], %) -|> angledLine([ - segAng(rectangleSegmentA004), - -segLen(rectangleSegmentA004) - ], %) -|> lineTo([profileStartX(%), profileStartY(%)], %) -|> close(%)`) - }) - } - ) - - test2( - 'Can edit a sketch with multiple profiles, dragging segments to edit them, and adding one new profile', - async ({ app, scene, toolbar, editor }) => { - await app.initialise(`sketch001 = startSketchOn('XZ') -profile001 = startProfileAt([6.24, 4.54], sketch001) - |> line([-0.41, 6.99], %) - |> line([8.61, 0.74], %) - |> line([10.99, -5.22], %) -profile002 = startProfileAt([11.19, 5.02], sketch001) - |> angledLine([0, 10.78], %, $rectangleSegmentA001) - |> angledLine([ - segAng(rectangleSegmentA001) - 90, - 4.14 - ], %) - |> angledLine([ - segAng(rectangleSegmentA001), - -segLen(rectangleSegmentA001) - ], %) - |> lineTo([profileStartX(%), profileStartY(%)], %) - |> close(%) -profile003 = circle({ center = [6.92, -4.2], radius = 3.16 }, sketch001) -`) - - const [pointOnSegment] = scene.makeMouseHelpers(590, 141) - const [profileEnd] = scene.makeMouseHelpers(970, 105) - const profileEndMv = scene.makeMouseHelpers(951, 101)[1] - const [newProfileEnd] = scene.makeMouseHelpers(764, 104) - const dragSegmentTo = scene.makeMouseHelpers(850, 104)[1] - - const rectHandle = scene.makeMouseHelpers(901, 150)[1] - const rectDragTo = scene.makeMouseHelpers(901, 180)[1] - - const circleEdge = scene.makeMouseHelpers(691, 331)[1] - const dragCircleTo = scene.makeMouseHelpers(720, 331)[1] - - const [rectStart] = scene.makeMouseHelpers(794, 322) - const [rectEnd] = scene.makeMouseHelpers(757, 395) - - await test2.step('enter sketch and setup', async () => { - await pointOnSegment({ shouldDbClick: true }) - await app.page.waitForTimeout(600) - - await toolbar.lineBtn.click() - await app.page.waitForTimeout(100) - }) - - await test2.step('extend existing profile', async () => { - await profileEnd() - await app.page.waitForTimeout(100) - await newProfileEnd() - await editor.expectEditor.toContain(`|> line([-11.4, 0.71], %)`) - await toolbar.lineBtn.click() - await app.page.waitForTimeout(100) - }) - - await test2.step('edit existing profile', async () => { - await profileEndMv() - await app.page.mouse.down() - await dragSegmentTo() - await app.page.mouse.up() - await editor.expectEditor.toContain(`line([4.16, -4.51], %)`) - }) - - await test2.step('edit existing rect', async () => { - await rectHandle() - await app.page.mouse.down() - await rectDragTo() - await app.page.mouse.up() - await editor.expectEditor.toContain( - `angledLine([-7, 10.2], %, $rectangleSegmentA001)` - ) - }) - - await test2.step('edit existing circl', async () => { - await circleEdge() - await app.page.mouse.down() - await dragCircleTo() - await app.page.mouse.up() - await editor.expectEditor.toContain( - `profile003 = circle({ center = [6.92, -4.2], radius = 4.77 }, sketch001)` - ) - }) - - await test2.step('add new profile', async () => { - await toolbar.rectangleBtn.click() - await app.page.waitForTimeout(100) - await rectStart() - await editor.expectEditor.toContain( - `profile004 = startProfileAt([15.62, -3.83], sketch001)` - ) - await app.page.waitForTimeout(100) - await rectEnd() - await editor.expectEditor - .toContain(`|> angledLine([180, 1.97], %, $rectangleSegmentA002) - |> angledLine([ - segAng(rectangleSegmentA002) + 90, - 3.88 - ], %) - |> angledLine([ - segAng(rectangleSegmentA002), - -segLen(rectangleSegmentA002) - ], %) - |> lineTo([profileStartX(%), profileStartY(%)], %) - |> close(%)`) - }) - } - ) - test2( - 'Can delete a profile in the editor while is sketch mode, and sketch mode does not break, can ctrl+z to undo after constraint with variable was added', - async ({ app, scene, toolbar, editor, cmdBar }) => { - await app.initialise(`sketch001 = startSketchOn('XZ') -profile001 = startProfileAt([6.24, 4.54], sketch001) - |> line([-0.41, 6.99], %) - |> line([8.61, 0.74], %) - |> line([10.99, -5.22], %) -profile002 = startProfileAt([11.19, 5.02], sketch001) - |> angledLine([0, 10.78], %, $rectangleSegmentA001) - |> angledLine([ - segAng(rectangleSegmentA001) - 90, - 4.14 - ], %) - |> angledLine([ - segAng(rectangleSegmentA001), - -segLen(rectangleSegmentA001) - ], %) - |> lineTo([profileStartX(%), profileStartY(%)], %) - |> close(%) -profile003 = circle({ center = [6.92, -4.2], radius = 3.16 }, sketch001) -`) - - const [pointOnSegment] = scene.makeMouseHelpers(590, 141) - const [segment1Click] = scene.makeMouseHelpers(616, 131) - const sketchIsDrawnProperly = async () => { - await test2.step( - 'check the sketch is still drawn properly', - async () => { - await app.page.waitForTimeout(200) - await scene.expectPixelColor( - [255, 255, 255], - { x: 617, y: 163 }, - 15 - ) - await scene.expectPixelColor( - [255, 255, 255], - { x: 629, y: 331 }, - 15 - ) - } - ) - } - - await test2.step('enter sketch and setup', async () => { - await pointOnSegment({ shouldDbClick: true }) - await app.page.waitForTimeout(600) - - await toolbar.lineBtn.click() - await app.page.waitForTimeout(100) - }) - - await test2.step('select and delete code for a profile', async () => {}) - await app.page.getByText('close(%)').click() - await app.page.keyboard.down('Shift') - for (let i = 0; i < 11; i++) { - await app.page.keyboard.press('ArrowUp') - } - await app.page.keyboard.press('Home') - await app.page.keyboard.up('Shift') - await app.page.keyboard.press('Backspace') - - await sketchIsDrawnProperly() - - await test2.step('add random new var between profiles', async () => { - await app.page.keyboard.type('myVar = 5') - await app.page.keyboard.press('Enter') - await app.page.waitForTimeout(600) - }) - - await sketchIsDrawnProperly() - - await test2.step( - 'Adding a constraint with a variable, and than ctrl-z-ing which will remove the variable again does not break sketch mode', - async () => { - await expect(async () => { - await segment1Click() - await editor.expectState({ - diagnostics: [], - activeLines: ['|>line([-0.41,6.99],%)'], - highlightedCode: 'line([-0.41,6.99],%)', - }) - }).toPass({ timeout: 5_000, intervals: [500] }) - - await toolbar.lengthConstraintBtn.click() - await cmdBar.progressCmdBar() - await editor.expectEditor.toContain('length001 = 7') - - // wait for execute defer - await app.page.waitForTimeout(600) - await sketchIsDrawnProperly() - - await app.page.keyboard.down('Meta') - await app.page.keyboard.press('KeyZ') - await app.page.keyboard.up('Meta') - - await editor.expectEditor.not.toContain('length001 = 7') - await sketchIsDrawnProperly() - } - ) - } - ) - - test2( - 'can enter sketch when there is an extrude', - async ({ app, scene, toolbar }) => { - await app.initialise(`sketch001 = startSketchOn('XZ') -profile001 = startProfileAt([-63.43, 193.08], sketch001) - |> line([168.52, 149.87], %) - |> line([190.29, -39.18], %) - |> tangentialArcTo([319.63, 129.65], %) - |> line([-217.65, -21.76], %) - |> lineTo([profileStartX(%), profileStartY(%)], %) - |> close(%) -profile003 = startProfileAt([16.79, 38.24], sketch001) - |> angledLine([0, 182.82], %, $rectangleSegmentA001) - |> angledLine([ - segAng(rectangleSegmentA001) - 90, - 105.71 - ], %) - |> angledLine([ - segAng(rectangleSegmentA001), - -segLen(rectangleSegmentA001) - ], %) - |> lineTo([profileStartX(%), profileStartY(%)], %) - |> close(%) -profile004 = circle({ - center = [280.45, 47.57], - radius = 55.26 -}, sketch001) -extrude002 = extrude(50, profile001) -extrude001 = extrude(5, profile003) -`) - const [pointOnSegment] = scene.makeMouseHelpers(574, 207) - - await pointOnSegment() - await toolbar.editSketch() - // wait for engine animation - await app.page.waitForTimeout(600) - - await test2.step('check the sketch is still drawn properly', async () => { - await scene.expectPixelColor([255, 255, 255], { x: 591, y: 167 }, 15) - await scene.expectPixelColor([255, 255, 255], { x: 638, y: 222 }, 15) - await scene.expectPixelColor([255, 255, 255], { x: 756, y: 214 }, 15) - }) - } - ) - test2( - 'exit new sketch without drawing anything should not be a problem', - async ({ app, scene, toolbar, editor, cmdBar }) => { - await app.initialise(`myVar = 5`) - const [selectXZPlane] = scene.makeMouseHelpers(650, 150) - - await toolbar.startSketchPlaneSelection() - await selectXZPlane() - // timeout wait for engine animation is unavoidable - await app.page.waitForTimeout(600) - - await editor.expectEditor.toContain(`sketch001 = startSketchOn('XZ')`) - await toolbar.exitSketchBtn.click() - - await editor.expectEditor.not.toContain(`sketch001 = startSketchOn('XZ')`) - - await test2.step( - "still renders code, hasn't got into a weird state", - async () => { - await editor.replaceCode( - 'myVar = 5', - `myVar = 5 - sketch001 = startSketchOn('XZ') - profile001 = circle({ - center = [12.41, 3.87], - radius = myVar - }, sketch001)` - ) - - await scene.expectPixelColor([255, 255, 255], { x: 633, y: 211 }, 15) - } - ) - } - ) - test2( - 'A sketch with only "startProfileAt" and no segments should still be able to be continued ', - async ({ app, scene, toolbar, editor }) => { - await app.initialise(`sketch001 = startSketchOn('XZ') -profile001 = startProfileAt([85.19, 338.59], sketch001) - |> line([213.3, -94.52], %) - |> line([-230.09, -55.34], %) - |> lineTo([profileStartX(%), profileStartY(%)], %) - |> close(%) -sketch002 = startSketchOn('XY') -profile002 = startProfileAt([85.81, 52.55], sketch002) - -`) - const [startProfileAt] = scene.makeMouseHelpers(606, 184) - const [nextPoint] = scene.makeMouseHelpers(763, 130) - await app.page - .getByText('startProfileAt([85.81, 52.55], sketch002)') - .click() - await toolbar.editSketch() - // timeout wait for engine animation is unavoidable - await app.page.waitForTimeout(600) - - // equip line tool - await toolbar.lineBtn.click() - await app.page.waitForTimeout(100) - await startProfileAt() - await app.page.waitForTimeout(100) - await nextPoint() - await editor.expectEditor.toContain(`|> line([126.05, 44.12], %)`) - } - ) - test2( - 'old style sketch all in one pipe (with extrude) will break up to allow users to add a new profile to the same sketch', - async ({ app, scene, toolbar, editor }) => { - await app.initialise(`thePart = startSketchOn('XZ') - |> startProfileAt([7.53, 10.51], %) - |> line([12.54, 1.83], %) - |> line([6.65, -6.91], %) - |> line([-6.31, -8.69], %) - |> lineTo([profileStartX(%), profileStartY(%)], %) - |> close(%) -extrude001 = extrude(75, thePart) -`) - const [objClick] = scene.makeMouseHelpers(565, 343) - const [profilePoint1] = scene.makeMouseHelpers(609, 289) - const [profilePoint2] = scene.makeMouseHelpers(714, 389) - - await test2.step('enter sketch and setup', async () => { - await objClick() - await toolbar.editSketch() - // timeout wait for engine animation is unavoidable - await app.page.waitForTimeout(600) - }) - - await test2.step( - 'expect code to match initial conditions still', - async () => { - await editor.expectEditor.toContain(`thePart = startSketchOn('XZ') - |> startProfileAt([7.53, 10.51], %)`) - } - ) - - await test2.step( - 'equiping the line tool should break up the pipe expression', - async () => { - await toolbar.lineBtn.click() - await editor.expectEditor.toContain( - `sketch001 = startSketchOn('XZ')thePart = startProfileAt([7.53, 10.51], sketch001)` - ) - } - ) - - await test2.step( - 'can continue on to add a new profile to this sketch', - async () => { - await profilePoint1() - await editor.expectEditor.toContain( - `profile001 = startProfileAt([19.77, -7.08], sketch001)` - ) - await profilePoint2() - await editor.expectEditor.toContain(`|> line([19.05, -18.14], %)`) - } - ) - } - ) - test2( - 'Can enter sketch on sketch of wall and cap for segment, solid2d, extrude-wall, extrude-cap selections', - async ({ app, scene, toolbar, editor }) => { - // TODO this test should include a test for selecting revolve walls and caps - await app.initialise(`sketch001 = startSketchOn('XZ') -profile001 = startProfileAt([6.71, -3.66], sketch001) - |> line([2.65, 9.02], %, $seg02) - |> line([3.73, -9.36], %, $seg01) - |> lineTo([profileStartX(%), profileStartY(%)], %) - |> close(%) -extrude001 = extrude(20, profile001) -sketch002 = startSketchOn(extrude001, seg01) -profile002 = startProfileAt([0.75, 13.46], sketch002) - |> line([4.52, 3.79], %) - |> line([5.98, -2.81], %) -profile003 = startProfileAt([3.19, 13.3], sketch002) - |> angledLine([0, 6.64], %, $rectangleSegmentA001) - |> angledLine([ - segAng(rectangleSegmentA001) - 90, - 2.81 - ], %) - |> angledLine([ - segAng(rectangleSegmentA001), - -segLen(rectangleSegmentA001) - ], %) - |> lineTo([profileStartX(%), profileStartY(%)], %) - |> close(%) -profile004 = startProfileAt([3.15, 9.39], sketch002) - |> xLine(6.92, %) - |> line([-7.41, -2.85], %) - |> lineTo([profileStartX(%), profileStartY(%)], %) - |> close(%) -profile005 = circle({ center = [5.15, 4.34], radius = 1.66 }, sketch002) -profile006 = startProfileAt([9.65, 3.82], sketch002) - |> line([2.38, 5.62], %) - |> line([2.13, -5.57], %) - |> lineTo([profileStartX(%), profileStartY(%)], %) - |> close(%) -revolve001 = revolve({ - angle = 45, - axis = getNextAdjacentEdge(seg01) -}, profile004) -extrude002 = extrude(4, profile006) -sketch003 = startSketchOn('-XZ') -profile007 = startProfileAt([4.8, 7.55], sketch003) - |> line([7.39, 2.58], %) - |> line([7.02, -2.85], %) -profile008 = startProfileAt([5.54, 5.49], sketch003) - |> line([6.34, 2.64], %) - |> line([6.33, -2.96], %) - |> lineTo([profileStartX(%), profileStartY(%)], %) - |> close(%) -profile009 = startProfileAt([5.23, 1.95], sketch003) - |> line([6.8, 2.17], %) - |> line([7.34, -2.75], %) - |> lineTo([profileStartX(%), profileStartY(%)], %) - |> close(%) -profile010 = circle({ - center = [7.18, -2.11], - radius = 2.67 -}, sketch003) -profile011 = startProfileAt([5.07, -6.39], sketch003) - |> angledLine([0, 4.54], %, $rectangleSegmentA002) - |> angledLine([ - segAng(rectangleSegmentA002) - 90, - 4.17 - ], %) - |> angledLine([ - segAng(rectangleSegmentA002), - -segLen(rectangleSegmentA002) - ], %) - |> lineTo([profileStartX(%), profileStartY(%)], %) - |> close(%) -extrude003 = extrude(2.5, profile011) -revolve002 = revolve({ angle = 45, axis = seg02 }, profile008) -`) - - const camPositionForSelectingSketchOnWallProfiles = () => - scene.moveCameraTo( - { x: 834, y: -680, z: 534 }, - { x: -54, y: -476, z: 148 } - ) - const camPositionForSelectingSketchOnCapProfiles = () => - scene.moveCameraTo( - { x: 404, y: 690, z: 38 }, - { x: 16, y: -140, z: -10 } - ) - const wallSelectionOptions = [ - { - title: 'select wall segment', - selectClick: scene.makeMouseHelpers(598, 211)[0], - }, - { - title: 'select wall solid 2d', - selectClick: scene.makeMouseHelpers(677, 236)[0], - }, - { - title: 'select wall circle', - selectClick: scene.makeMouseHelpers(811, 247)[0], - }, - { - title: 'select wall extrude wall', - selectClick: scene.makeMouseHelpers(793, 136)[0], - }, - { - title: 'select wall extrude cap', - selectClick: scene.makeMouseHelpers(836, 103)[0], - }, - ] as const - const capSelectionOptions = [ - { - title: 'select cap segment', - selectClick: scene.makeMouseHelpers(688, 91)[0], - }, - { - title: 'select cap solid 2d', - selectClick: scene.makeMouseHelpers(733, 204)[0], - }, - // TODO keeps failing - // { - // title: 'select cap circle', - // selectClick: scene.makeMouseHelpers(679, 290)[0], - // }, - { - title: 'select cap extrude wall', - selectClick: scene.makeMouseHelpers(649, 402)[0], - }, - { - title: 'select cap extrude cap', - selectClick: scene.makeMouseHelpers(693, 408)[0], - }, - ] as const - - const verifyWallProfilesAreDrawn = async () => - test2.step('verify wall profiles are drawn', async () => { - // open polygon - await scene.expectPixelColor( - TEST_COLORS.WHITE, - { x: 599, y: 168 }, - 15 - ) - // closed polygon - await scene.expectPixelColor( - TEST_COLORS.WHITE, - { x: 656, y: 171 }, - 15 - ) - // revolved profile - await scene.expectPixelColor( - TEST_COLORS.WHITE, - { x: 655, y: 264 }, - 15 - ) - // extruded profile - await scene.expectPixelColor( - TEST_COLORS.WHITE, - { x: 808, y: 396 }, - 15 - ) - // circle - await scene.expectPixelColor( - [ - TEST_COLORS.WHITE, - TEST_COLORS.BLUE, // When entering via the circle, it's selected and therefore blue - ], - { x: 742, y: 386 }, - 15 - ) - }) - - const verifyCapProfilesAreDrawn = async () => - test2.step('verify wall profiles are drawn', async () => { - // open polygon - await scene.expectPixelColor( - TEST_COLORS.WHITE, - // TEST_COLORS.BLUE, // When entering via the circle, it's selected and therefore blue - { x: 620, y: 58 }, - 15 - ) - // revolved profile - await scene.expectPixelColor( - TEST_COLORS.WHITE, - { x: 641, y: 110 }, - 15 - ) - // closed polygon - await scene.expectPixelColor( - TEST_COLORS.WHITE, - { x: 632, y: 200 }, - 15 - ) - // extruded profile - await scene.expectPixelColor( - TEST_COLORS.WHITE, - { x: 628, y: 410 }, - 15 - ) - // circle - await scene.expectPixelColor( - [ - TEST_COLORS.WHITE, - TEST_COLORS.BLUE, // When entering via the circle, it's selected and therefore blue - ], - { x: 681, y: 303 }, - 15 - ) - }) - - await test2.step('select wall profiles', async () => { - for (const { title, selectClick } of wallSelectionOptions) { - await test2.step(title, async () => { - await camPositionForSelectingSketchOnWallProfiles() - await selectClick() - await toolbar.editSketch() - await app.page.waitForTimeout(600) - await verifyWallProfilesAreDrawn() - await toolbar.exitSketchBtn.click() - await app.page.waitForTimeout(100) - }) - } - }) - - await test2.step('select cap profiles', async () => { - for (const { title, selectClick } of capSelectionOptions) { - await test2.step(title, async () => { - await camPositionForSelectingSketchOnCapProfiles() - await app.page.waitForTimeout(100) - await selectClick() - await app.page.waitForTimeout(100) - await toolbar.editSketch() - await app.page.waitForTimeout(600) - await verifyCapProfilesAreDrawn() - await toolbar.exitSketchBtn.click() - await app.page.waitForTimeout(100) - }) - } - }) - } - ) - test2( - 'Can enter sketch loft edges, base and continue sketch', - async ({ app, scene, toolbar, editor }) => { - await app.initialise(`sketch001 = startSketchOn('XZ') -profile001 = startProfileAt([34, 42.66], sketch001) - |> line([102.65, 151.99], %) - |> line([76, -138.66], %) - |> lineTo([profileStartX(%), profileStartY(%)], %) - |> close(%) -plane001 = offsetPlane('XZ', 50) -sketch002 = startSketchOn(plane001) -profile002 = startProfileAt([39.43, 172.21], sketch002) - |> xLine(183.99, %) - |> line([-77.95, -145.93], %) - |> lineTo([profileStartX(%), profileStartY(%)], %) - |> close(%) - -loft([profile001, profile002]) -`) - const [baseProfileEdgeClick] = scene.makeMouseHelpers(621, 292) - - const [rect1Crn1] = scene.makeMouseHelpers(592, 283) - const [rect1Crn2] = scene.makeMouseHelpers(797, 268) - - await baseProfileEdgeClick() - await toolbar.editSketch() - await app.page.waitForTimeout(600) - await scene.expectPixelColor(TEST_COLORS.WHITE, { x: 562, y: 172 }, 15) - - await toolbar.rectangleBtn.click() - await app.page.waitForTimeout(100) - await rect1Crn1() - await editor.expectEditor.toContain( - `profile003 = startProfileAt([50.72, -18.19], sketch001)` - ) - await rect1Crn2() - await editor.expectEditor.toContain( - `angledLine([0, 113.01], %, $rectangleSegmentA001)` - ) - } - ) - test2( - 'Can enter sketch loft edges offsetPlane and continue sketch', - async ({ app, scene, toolbar, editor }) => { - await app.initialise(`sketch001 = startSketchOn('XZ') -profile001 = startProfileAt([34, 42.66], sketch001) - |> line([102.65, 151.99], %) - |> line([76, -138.66], %) - |> lineTo([profileStartX(%), profileStartY(%)], %) - |> close(%) -plane001 = offsetPlane('XZ', 50) -sketch002 = startSketchOn(plane001) -profile002 = startProfileAt([39.43, 172.21], sketch002) - |> xLine(183.99, %) - |> line([-77.95, -145.93], %) - |> lineTo([profileStartX(%), profileStartY(%)], %) - |> close(%) - -loft([profile001, profile002]) -`) - const topProfileEdgeClickCoords = { x: 602, y: 185 } as const - const [topProfileEdgeClick] = scene.makeMouseHelpers( - topProfileEdgeClickCoords.x, - topProfileEdgeClickCoords.y - ) - - const [rect1Crn1] = scene.makeMouseHelpers(592, 283) - const [rect1Crn2] = scene.makeMouseHelpers(797, 268) - - await scene.moveCameraTo( - { x: 8171, y: -7740, z: 1624 }, - { x: 3302, y: -627, z: 2892 } - ) - - await topProfileEdgeClick() - await toolbar.editSketch() - await app.page.waitForTimeout(600) - await scene.expectPixelColor(TEST_COLORS.BLUE, { x: 788, y: 188 }, 15) - - await toolbar.rectangleBtn.click() - await app.page.waitForTimeout(100) - await rect1Crn1() - await editor.expectEditor.toContain( - `profile003 = startProfileAt([47.76, -17.13], plane001)` - ) - await rect1Crn2() - await editor.expectEditor.toContain( - `angledLine([0, 106.42], %, $rectangleSegmentA001)` - ) - } - ) -}) diff --git a/e2e/playwright/snapshot-tests.spec.ts b/e2e/playwright/snapshot-tests.spec.ts index e7f94b6703..4b2e1b8f49 100644 --- a/e2e/playwright/snapshot-tests.spec.ts +++ b/e2e/playwright/snapshot-tests.spec.ts @@ -446,7 +446,8 @@ test( const startXPx = 600 await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) - code += `profile001 = startProfileAt([7.19, -9.7], sketch001)` + code += ` + |> startProfileAt([7.19, -9.7], %)` await expect(page.locator('.cm-content')).toHaveText(code) await page.waitForTimeout(100) @@ -468,10 +469,6 @@ 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) @@ -594,7 +591,8 @@ test( mask: [page.getByTestId('model-state-indicator')], }) await expect(page.locator('.cm-content')).toHaveText( - `sketch001 = startSketchOn('XZ')profile001 = circle({ center = [14.44, -2.44], radius = 1 }, sketch001)` + `sketch001 = startSketchOn('XZ') + |> circle({ center = [14.44, -2.44], radius = 1 }, %)` ) } ) @@ -638,7 +636,8 @@ test.describe( const startXPx = 600 await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) - code += `profile001 = startProfileAt([7.19, -9.7], sketch001)` + code += ` + |> startProfileAt([7.19, -9.7], %)` await expect(u.codeLocator).toHaveText(code) await page.waitForTimeout(100) @@ -656,10 +655,6 @@ 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 += ` @@ -746,7 +741,8 @@ test.describe( const startXPx = 600 await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) - code += `profile001 = startProfileAt([182.59, -246.32], sketch001)` + code += ` + |> startProfileAt([182.59, -246.32], %)` await expect(u.codeLocator).toHaveText(code) await page.waitForTimeout(100) @@ -764,10 +760,6 @@ 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 += ` diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-1-Google-Chrome-linux.png index dc257dad52..45c6a42417 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-1-Google-Chrome-win32.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-1-Google-Chrome-win32.png index be428bf08e..0571ae9841 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-1-Google-Chrome-win32.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-1-Google-Chrome-win32.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-2-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-2-Google-Chrome-linux.png index a0878b9ea3..3ff97eef3d 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-2-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-2-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-2-Google-Chrome-win32.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-2-Google-Chrome-win32.png index 5cd6926bf1..d0f165e5c8 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-2-Google-Chrome-win32.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-2-Google-Chrome-win32.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-1-Google-Chrome-linux.png index 31d1b085e6..8561d872ae 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-1-Google-Chrome-win32.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-1-Google-Chrome-win32.png index 10a4bf4b8a..0100b1691b 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-1-Google-Chrome-win32.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-1-Google-Chrome-win32.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-2-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-2-Google-Chrome-linux.png index ac2bc65b02..3877bfc48b 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-2-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-2-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-2-Google-Chrome-win32.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-2-Google-Chrome-win32.png index 8deadee79a..f9c30400ad 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-2-Google-Chrome-win32.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-2-Google-Chrome-win32.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-circle-should-look-right-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-circle-should-look-right-1-Google-Chrome-linux.png index 88524a55bb..4787f484d4 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-circle-should-look-right-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-circle-should-look-right-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-circle-should-look-right-1-Google-Chrome-win32.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-circle-should-look-right-1-Google-Chrome-win32.png index 850b72cc13..8af8484e90 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-circle-should-look-right-1-Google-Chrome-win32.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-circle-should-look-right-1-Google-Chrome-win32.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-rectangles-should-look-right-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-rectangles-should-look-right-1-Google-Chrome-linux.png index 0de2002d30..96422ab7ad 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-rectangles-should-look-right-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-rectangles-should-look-right-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-rectangles-should-look-right-1-Google-Chrome-win32.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-rectangles-should-look-right-1-Google-Chrome-win32.png index 0efde30de4..8fdf3768c2 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-rectangles-should-look-right-1-Google-Chrome-win32.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-rectangles-should-look-right-1-Google-Chrome-win32.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-1-Google-Chrome-linux.png index 9a7fd6623f..38d1cb522e 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-1-Google-Chrome-win32.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-1-Google-Chrome-win32.png index 7fa2b6b7c0..fe490db6f6 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-1-Google-Chrome-win32.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-1-Google-Chrome-win32.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-2-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-2-Google-Chrome-linux.png index b3dcfe6356..b0f06ad601 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-2-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-2-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-2-Google-Chrome-win32.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-2-Google-Chrome-win32.png index 7cec6f2e86..974555be9e 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-2-Google-Chrome-win32.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-2-Google-Chrome-win32.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/code-color-goober-code-color-goober-opening-window-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/code-color-goober-code-color-goober-opening-window-1-Google-Chrome-linux.png index c01783eb2b..5f235e51c0 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/code-color-goober-code-color-goober-opening-window-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/code-color-goober-code-color-goober-opening-window-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-XY-1-Google-Chrome-win32.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-XY-1-Google-Chrome-win32.png index b34bda5e33..7c00fa1295 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-XY-1-Google-Chrome-win32.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-XY-1-Google-Chrome-win32.png differ diff --git a/e2e/playwright/test-network-and-connection-issues.spec.ts b/e2e/playwright/test-network-and-connection-issues.spec.ts index e201f4ce4c..e38d08c5ad 100644 --- a/e2e/playwright/test-network-and-connection-issues.spec.ts +++ b/e2e/playwright/test-network-and-connection-issues.spec.ts @@ -1,8 +1,6 @@ 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) @@ -132,16 +130,17 @@ 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')profile001 = startProfileAt(${commonPoints.startAt}, sketch001)` - ) + await expect(page.locator('.cm-content')) + .toHaveText(`sketch001 = startSketchOn('XZ') + |> startProfileAt(${commonPoints.startAt}, %)`) 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')profile001 = startProfileAt(${commonPoints.startAt}, sketch001) + .toHaveText(`sketch001 = startSketchOn('XZ') + |> startProfileAt(${commonPoints.startAt}, %) |> xLine(${commonPoints.num1}, %)`) // Expect the network to be up @@ -189,9 +188,7 @@ test.describe('Test network and connection issues', () => { await page.mouse.click(100, 100) // select a line - await page - .getByText(`startProfileAt(${commonPoints.startAt}, sketch001)`) - .click() + await page.getByText(`startProfileAt(${commonPoints.startAt}, %)`).click() // enter sketch again await u.doAndWaitForCmd( @@ -205,36 +202,11 @@ 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') -profile001 = startProfileAt([12.34, -12.34], sketch001) + |> startProfileAt([12.34, -12.34], %) |> xLine(12.34, %) |> line([-12.34, 12.34], %) @@ -244,7 +216,7 @@ profile001 = startProfileAt([12.34, -12.34], sketch001) await expect.poll(u.normalisedEditorCode) .toBe(`sketch001 = startSketchOn('XZ') -profile001 = startProfileAt([12.34, -12.34], sketch001) + |> startProfileAt([12.34, -12.34], %) |> xLine(12.34, %) |> line([-12.34, 12.34], %) |> xLine(-12.34, %) diff --git a/e2e/playwright/testing-constraints.spec.ts b/e2e/playwright/testing-constraints.spec.ts index f02c0515b1..026568781e 100644 --- a/e2e/playwright/testing-constraints.spec.ts +++ b/e2e/playwright/testing-constraints.spec.ts @@ -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, %) + ` ) }) diff --git a/e2e/playwright/testing-selections.spec.ts b/e2e/playwright/testing-selections.spec.ts index fb4880e6c4..ef984ce504 100644 --- a/e2e/playwright/testing-selections.spec.ts +++ b/e2e/playwright/testing-selections.spec.ts @@ -77,31 +77,30 @@ 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')profile001 = startProfileAt(${commonPoints.startAt}, sketch001)` - ) + await expect(page.locator('.cm-content')) + .toHaveText(`sketch001 = startSketchOn('XZ') + |> startProfileAt(${commonPoints.startAt}, %)`) await page.waitForTimeout(100) await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10) await expect(page.locator('.cm-content')) - .toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${commonPoints.startAt}, sketch001) + .toHaveText(`sketch001 = startSketchOn('XZ') + |> startProfileAt(${commonPoints.startAt}, %) |> 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')profile001 = startProfileAt(${ - commonPoints.startAt - }, sketch001) + .toHaveText(`sketch001 = startSketchOn('XZ') + |> startProfileAt(${commonPoints.startAt}, %) |> 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')profile001 = startProfileAt(${ - commonPoints.startAt - }, sketch001) + .toHaveText(`sketch001 = startSketchOn('XZ') + |> startProfileAt(${commonPoints.startAt}, %) |> xLine(${commonPoints.num1}, %) |> yLine(${commonPoints.num1 + 0.01}, %) |> xLine(${commonPoints.num2 * -1}, %)`) @@ -331,28 +330,6 @@ 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) @@ -385,10 +362,9 @@ profile003 = startProfileAt([40.16, -120.48], sketch006) }) await page.waitForTimeout(100) - const revolve = { x: 635, y: 253 } + const revolve = { x: 646, y: 248 } 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) @@ -454,20 +430,6 @@ profile003 = startProfileAt([40.16, -120.48], sketch006) 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, @@ -1296,15 +1258,12 @@ 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(firstClickCoords.x, firstClickCoords.y) + await page.mouse.click(650, 200) await page.waitForTimeout(600) // Code before exiting the tool - let previousCodeContent = ( - await page.locator('.cm-content').innerText() - ).replace(/\s+/g, '') + let previousCodeContent = await page.locator('.cm-content').innerText() // deselect the line tool by clicking it await page.getByRole('button', { name: 'line Line', exact: true }).click() @@ -1316,23 +1275,14 @@ extrude001 = extrude(50, sketch001) await page.mouse.click(750, 200) await page.waitForTimeout(100) - await expect - .poll(async () => { - let str = await page.locator('.cm-content').innerText() - str = str.replace(/\s+/g, '') - return str - }) - .toBe(previousCodeContent) + // expect no change + await expect(page.locator('.cm-content')).toHaveText(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( diff --git a/e2e/playwright/various.spec.ts b/e2e/playwright/various.spec.ts index 93551bd6b6..e2ce531384 100644 --- a/e2e/playwright/various.spec.ts +++ b/e2e/playwright/various.spec.ts @@ -224,13 +224,8 @@ 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) - - const secondMousePosition = { x: 800, y: 250 } - - await page.mouse.move(secondMousePosition.x, secondMousePosition.y, { - steps: 5, - }) - await page.mouse.click(secondMousePosition.x, secondMousePosition.y) + await page.mouse.move(800, 250, { steps: 5 }) + await page.mouse.click(800, 250) // Unequip line tool await page.keyboard.press('Escape') // Make sure we didn't pop out of sketch mode. @@ -239,17 +234,9 @@ 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') @@ -550,9 +537,9 @@ test('Sketch on face', async ({ page }) => { await expect.poll(u.normalisedEditorCode).toContain( u.normalisedCode(`sketch002 = startSketchOn(extrude001, seg01) -profile001 = startProfileAt([-12.88, 6.66], sketch002) - |> line([2.71, -0.22], %) - |> line([-2.87, -1.38], %) + |> startProfileAt([-12.94, 6.6], %) + |> line([2.45, -0.2], %) + |> line([-2.6, -1.25], %) |> lineTo([profileStartX(%), profileStartY(%)], %) |> close(%)`) ) @@ -567,7 +554,8 @@ profile001 = startProfileAt([-12.88, 6.66], sketch002) 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(500) + await page.waitForTimeout(400) + await page.waitForTimeout(150) await page.setViewportSize({ width: 1200, height: 1200 }) await u.openAndClearDebugPanel() await u.updateCamPosition([452, -152, 1166]) diff --git a/src/Toolbar.tsx b/src/Toolbar.tsx index 6b66be341a..e6e680721d 100644 --- a/src/Toolbar.tsx +++ b/src/Toolbar.tsx @@ -6,6 +6,7 @@ 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' @@ -21,7 +22,6 @@ import { } from 'lib/toolbar' import { isDesktop } from 'lib/isDesktop' import { openExternalBrowserIfDesktop } from 'lib/openWindow' -import { isCursorInFunctionDefinition } from 'lang/queryAst' export function Toolbar({ className = '', @@ -38,12 +38,7 @@ 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 ( - isCursorInFunctionDefinition( - kclManager.ast, - context.selectionRanges.graphSelections[0] - ) - ) + if (!isSingleCursorInPipe(context.selectionRanges, kclManager.ast)) return false return isCursorInSketchCommandRange( engineCommandManager.artifactGraph, diff --git a/src/clientSideScene/ClientSideSceneComp.tsx b/src/clientSideScene/ClientSideSceneComp.tsx index bbf47c586b..b8031e3f8a 100644 --- a/src/clientSideScene/ClientSideSceneComp.tsx +++ b/src/clientSideScene/ClientSideSceneComp.tsx @@ -433,8 +433,6 @@ export async function deleteSegment({ if (!sketchDetails) return await sceneEntitiesManager.updateAstAndRejigSketch( pathToNode, - sketchDetails.sketchNodePaths, - sketchDetails.planeNodePath, modifiedAst, sketchDetails.zAxis, sketchDetails.yAxis, diff --git a/src/clientSideScene/sceneEntities.ts b/src/clientSideScene/sceneEntities.ts index 30cb5ac122..d9560bd2f9 100644 --- a/src/clientSideScene/sceneEntities.ts +++ b/src/clientSideScene/sceneEntities.ts @@ -43,9 +43,11 @@ import { ProgramMemory, recast, Sketch, + Solid, VariableDeclaration, VariableDeclarator, sketchFromKclValue, + sketchFromKclValueOptional, defaultSourceRange, sourceRangeFromRust, resultIsOk, @@ -73,29 +75,20 @@ import { } from 'lang/std/sketch' import { isArray, isOverlap, roundOff } from 'lib/utils' import { + addStartProfileAt, createArrayExpression, createCallExpressionStdLib, - createIdentifier, createLiteral, createObjectExpression, createPipeExpression, createPipeSubstitution, - createVariableDeclaration, findUniqueName, - getInsertIndex, - insertNewStartProfileAt, - updateSketchNodePathsWithInsertIndex, } from 'lang/modifyAst' import { Selections, getEventForSegmentSelection } from 'lib/selections' import { createGridHelper, orthoScale, perspScale } from './helpers' import { Models } from '@kittycad/lib' import { uuidv4 } from 'lib/utils' -import { - SegmentOverlayPayload, - SketchDetails, - SketchDetailsUpdate, - SketchTool, -} from 'machines/modelingMachine' +import { SegmentOverlayPayload, SketchDetails } from 'machines/modelingMachine' import { EngineCommandManager } from 'lang/std/engineConnection' import { getRectangleCallExpressions, @@ -103,13 +96,12 @@ import { updateCenterRectangleSketch, } from 'lib/rectangleTool' import { getThemeColorForThreeJs, Themes } from 'lib/theme' -import { err, reportRejection, trap } from 'lib/trap' +import { err, Reason, reportRejection, trap } from 'lib/trap' import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer' import { Point3d } from 'wasm-lib/kcl/bindings/Point3d' import { SegmentInputs } from 'lang/std/stdTypes' import { Node } from 'wasm-lib/kcl/bindings/Node' import { radToDeg } from 'three/src/math/MathUtils' -import toast from 'react-hot-toast' type DraftSegment = 'line' | 'tangentialArcTo' @@ -341,9 +333,6 @@ export class SceneEntities { from: [point.x, point.y], scale, theme: sceneInfra._theme, - // default is 12, this makes the draft point pop a bit more, - // especially when snapping to the startProfileAt handle as it's it was the exact same size - size: 16, }) draftPoint.layers.set(SKETCH_LAYER) group.add(draftPoint) @@ -357,17 +346,9 @@ export class SceneEntities { setupNoPointsListener({ sketchDetails, afterClick, - currentTool, }: { sketchDetails: SketchDetails - currentTool: SketchTool - afterClick: ( - args: OnClickCallbackArgs, - updatedPaths: { - sketchNodePaths: PathToNode[] - sketchEntryNodePath: PathToNode - } - ) => void + afterClick: (args: OnClickCallbackArgs) => void }) { // TODO: Consolidate shared logic between this and setupSketch // Which should just fire when the sketch mode is entered, @@ -407,31 +388,14 @@ export class SceneEntities { sceneObject.object.name === X_AXIS || sceneObject.object.name === Y_AXIS ) - - const arrowHead = getParentGroup(args.intersects[0].object, [ARROWHEAD]) - const parent = getParentGroup( - args.intersects[0].object, - SEGMENT_BODIES_PLUS_PROFILE_START - ) - if ( - !axisIntersection && - !( - parent?.userData?.isLastInProfile && - (arrowHead || parent?.name === PROFILE_START) - ) - ) - return + if (!axisIntersection) return const { intersectionPoint } = args // We're hovering over an axis, so we should show a draft point const snappedPoint = intersectionPoint.twoD.clone() - if (axisIntersection?.object.name === X_AXIS) { + if (axisIntersection.object.name === X_AXIS) { snappedPoint.setComponent(1, 0) - } else if (axisIntersection?.object.name === X_AXIS) { + } else { snappedPoint.setComponent(0, 0) - } else if (arrowHead) { - snappedPoint.set(arrowHead.position.x, arrowHead.position.y) - } else if (parent?.name === PROFILE_START) { - snappedPoint.set(parent.position.x, parent.position.y) } // Either create a new one or update the existing one const draftPoint = this.getDraftPoint() @@ -467,25 +431,7 @@ export class SceneEntities { if (interaction !== 'none') return if (args.mouseEvent.which !== 1) return const { intersectionPoint } = args - if (!intersectionPoint?.twoD || !sketchDetails?.sketchEntryNodePath) - return - - const parent = getParentGroup( - args?.intersects?.[0]?.object, - SEGMENT_BODIES_PLUS_PROFILE_START - ) - if (parent?.userData?.isLastInProfile) { - afterClick(args, { - sketchNodePaths: sketchDetails.sketchNodePaths, - sketchEntryNodePath: parent.userData.pathToNode, - }) - return - } else if (currentTool === 'tangentialArc') { - toast.error( - 'Tangential Arc must continue an existing profile, please click on the last segment of the profile' - ) - return - } + if (!intersectionPoint?.twoD || !sketchDetails?.sketchPathToNode) return // Snap to either or both axes // if the click intersects their meshes @@ -501,34 +447,27 @@ export class SceneEntities { y: xAxisIntersection ? 0 : intersectionPoint.twoD.y, } - const inserted = insertNewStartProfileAt( + const addStartProfileAtRes = addStartProfileAt( kclManager.ast, - sketchDetails.sketchEntryNodePath, - sketchDetails.sketchNodePaths, - sketchDetails.planeNodePath, - [snappedClickPoint.x, snappedClickPoint.y], - 'end' + sketchDetails.sketchPathToNode, + [snappedClickPoint.x, snappedClickPoint.y] ) - if (trap(inserted)) return - const { modifiedAst } = inserted + if (trap(addStartProfileAtRes)) return + const { modifiedAst } = addStartProfileAtRes await kclManager.updateAst(modifiedAst, false) this.scene.remove(draftPointGroup) // Now perform the caller-specified action - afterClick(args, { - sketchNodePaths: inserted.updatedSketchNodePaths, - sketchEntryNodePath: inserted.updatedEntryNodePath, - }) + afterClick(args) }, }) } async setupSketch({ - sketchEntryNodePath, - sketchNodePaths, + sketchPathToNode, forward, up, position, @@ -536,8 +475,7 @@ export class SceneEntities { draftExpressionsIndices, selectionRanges, }: { - sketchEntryNodePath: PathToNode - sketchNodePaths: PathToNode[] + sketchPathToNode: PathToNode maybeModdedAst: Node draftExpressionsIndices?: { start: number; end: number } forward: [number, number, number] @@ -547,12 +485,11 @@ export class SceneEntities { }): Promise<{ truncatedAst: Node programMemoryOverride: ProgramMemory + sketch: Sketch variableDeclarationName: string }> { - this.createIntersectionPlane() - const prepared = this.prepareTruncatedMemoryAndAst( - sketchNodePaths, + sketchPathToNode || [], maybeModdedAst ) if (err(prepared)) return Promise.reject(prepared) @@ -566,144 +503,139 @@ export class SceneEntities { programMemoryOverride, }) const programMemory = execState.memory - const sketchesInfo = getSketchesInfo({ - sketchNodePaths, + const sketch = sketchFromPathToNode({ + pathToNode: sketchPathToNode, ast: maybeModdedAst, programMemory, }) + if (err(sketch)) return Promise.reject(sketch) + if (!sketch) return Promise.reject('sketch not found') + if (!isArray(sketch?.paths)) + return { + truncatedAst, + programMemoryOverride, + sketch, + variableDeclarationName, + } this.sceneProgramMemory = programMemory const group = new Group() position && group.position.set(...position) group.userData = { type: SKETCH_GROUP_SEGMENTS, - pathToNode: sketchEntryNodePath, + pathToNode: sketchPathToNode, } const dummy = new Mesh() // TODO: When we actually have sketch positions and rotations we can use them here. dummy.position.set(0, 0, 0) const scale = sceneInfra.getClientSceneScaleFactor(dummy) + const segPathToNode = getNodePathFromSourceRange( + maybeModdedAst, + sourceRangeFromRust(sketch.start.__geoMeta.sourceRange) + ) + if (sketch?.paths?.[0]?.type !== 'Circle') { + const _profileStart = createProfileStartHandle({ + from: sketch.start.from, + id: sketch.start.__geoMeta.id, + pathToNode: segPathToNode, + scale, + theme: sceneInfra._theme, + isDraft: false, + }) + _profileStart.layers.set(SKETCH_LAYER) + _profileStart.traverse((child) => { + child.layers.set(SKETCH_LAYER) + }) + group.add(_profileStart) + this.activeSegments[JSON.stringify(segPathToNode)] = _profileStart + } const callbacks: (() => SegmentOverlayPayload | null)[] = [] - - for (const sketchInfo of sketchesInfo) { - const { sketch } = sketchInfo - const segPathToNode = getNodePathFromSourceRange( + sketch.paths.forEach((segment, index) => { + let segPathToNode = getNodePathFromSourceRange( maybeModdedAst, - sourceRangeFromRust(sketch.start.__geoMeta.sourceRange) + sourceRangeFromRust(segment.__geoMeta.sourceRange) ) - if (sketch?.paths?.[0]?.type !== 'Circle') { - const _profileStart = createProfileStartHandle({ - from: sketch.start.from, - id: sketch.start.__geoMeta.id, - pathToNode: segPathToNode, - scale, - theme: sceneInfra._theme, - isDraft: false, - }) - _profileStart.layers.set(SKETCH_LAYER) - _profileStart.traverse((child) => { - child.layers.set(SKETCH_LAYER) - }) - if (!sketch.paths.length) { - _profileStart.userData.isLastInProfile = true - } - group.add(_profileStart) - this.activeSegments[JSON.stringify(segPathToNode)] = _profileStart - } - sketch.paths.forEach((segment, index) => { - const isLastInProfile = - index === sketch.paths.length - 1 && segment.type !== 'Circle' - let segPathToNode = getNodePathFromSourceRange( + if ( + draftExpressionsIndices && + (sketch.paths[index - 1] || sketch.start) + ) { + const previousSegment = sketch.paths[index - 1] || sketch.start + const previousSegmentPathToNode = getNodePathFromSourceRange( maybeModdedAst, - sourceRangeFromRust(segment.__geoMeta.sourceRange) + sourceRangeFromRust(previousSegment.__geoMeta.sourceRange) ) - if ( - draftExpressionsIndices && - (sketch.paths[index - 1] || sketch.start) - ) { - const previousSegment = sketch.paths[index - 1] || sketch.start - const previousSegmentPathToNode = getNodePathFromSourceRange( - maybeModdedAst, - sourceRangeFromRust(previousSegment.__geoMeta.sourceRange) - ) - const bodyIndex = previousSegmentPathToNode[1][0] - segPathToNode = getNodePathFromSourceRange( - truncatedAst, - sourceRangeFromRust(segment.__geoMeta.sourceRange) - ) - segPathToNode[1][0] = bodyIndex - } - const isDraftSegment = - draftExpressionsIndices && - index <= draftExpressionsIndices.end && - index >= draftExpressionsIndices.start && - // the following line is not robust to sketches defined within a function - sketchInfo.pathToNode[1][0] === sketchEntryNodePath[1][0] - const isSelected = selectionRanges?.graphSelections.some((selection) => - isOverlap( - selection?.codeRef?.range, - sourceRangeFromRust(segment.__geoMeta.sourceRange) - ) + const bodyIndex = previousSegmentPathToNode[1][0] + segPathToNode = getNodePathFromSourceRange( + truncatedAst, + sourceRangeFromRust(segment.__geoMeta.sourceRange) ) - - let seg: Group - const _node1 = getNodeFromPath( - maybeModdedAst, - segPathToNode, - 'CallExpression' + segPathToNode[1][0] = bodyIndex + } + const isDraftSegment = + draftExpressionsIndices && + index <= draftExpressionsIndices.end && + index >= draftExpressionsIndices.start + const isSelected = selectionRanges?.graphSelections.some((selection) => + isOverlap( + selection?.codeRef?.range, + sourceRangeFromRust(segment.__geoMeta.sourceRange) ) - if (err(_node1)) return - const callExpName = _node1.node?.callee?.name - - const initSegment = - segment.type === 'TangentialArcTo' - ? segmentUtils.tangentialArcTo.init - : segment.type === 'Circle' - ? segmentUtils.circle.init - : segmentUtils.straight.init - const input: SegmentInputs = - segment.type === 'Circle' - ? { - type: 'arc-segment', - from: segment.from, - center: segment.center, - radius: segment.radius, - } - : { - type: 'straight-segment', - from: segment.from, - to: segment.to, - } - const result = initSegment({ - prevSegment: sketch.paths[index - 1], - callExpName, - input, - id: segment.__geoMeta.id, - pathToNode: segPathToNode, - isDraftSegment, - scale, - texture: sceneInfra.extraSegmentTexture, - theme: sceneInfra._theme, - isSelected, - sceneInfra, - }) - if (err(result)) return - const { group: _group, updateOverlaysCallback } = result - seg = _group - if (isLastInProfile) { - seg.userData.isLastInProfile = true - } - callbacks.push(updateOverlaysCallback) - seg.layers.set(SKETCH_LAYER) - seg.traverse((child) => { - child.layers.set(SKETCH_LAYER) - }) + ) - group.add(seg) - this.activeSegments[JSON.stringify(segPathToNode)] = seg + let seg: Group + const _node1 = getNodeFromPath( + maybeModdedAst, + segPathToNode, + 'CallExpression' + ) + if (err(_node1)) return + const callExpName = _node1.node?.callee?.name + + const initSegment = + segment.type === 'TangentialArcTo' + ? segmentUtils.tangentialArcTo.init + : segment.type === 'Circle' + ? segmentUtils.circle.init + : segmentUtils.straight.init + const input: SegmentInputs = + segment.type === 'Circle' + ? { + type: 'arc-segment', + from: segment.from, + center: segment.center, + radius: segment.radius, + } + : { + type: 'straight-segment', + from: segment.from, + to: segment.to, + } + const result = initSegment({ + prevSegment: sketch.paths[index - 1], + callExpName, + input, + id: segment.__geoMeta.id, + pathToNode: segPathToNode, + isDraftSegment, + scale, + texture: sceneInfra.extraSegmentTexture, + theme: sceneInfra._theme, + isSelected, + sceneInfra, }) - } + if (err(result)) return + const { group: _group, updateOverlaysCallback } = result + seg = _group + callbacks.push(updateOverlaysCallback) + seg.layers.set(SKETCH_LAYER) + seg.traverse((child) => { + child.layers.set(SKETCH_LAYER) + }) + + group.add(seg) + this.activeSegments[JSON.stringify(segPathToNode)] = seg + }) this.currentSketchQuaternion = quaternionFromUpNForward( new Vector3(...up), @@ -724,25 +656,22 @@ export class SceneEntities { return { truncatedAst, programMemoryOverride, + sketch, variableDeclarationName, } } updateAstAndRejigSketch = async ( - sketchEntryNodePath: PathToNode, - sketchNodePaths: PathToNode[], - planeNodePath: PathToNode, - modifiedAst: Node | Error, + sketchPathToNode: PathToNode, + modifiedAst: Node, forward: [number, number, number], up: [number, number, number], origin: [number, number, number] ) => { - if (trap(modifiedAst)) return Promise.reject(modifiedAst) const nextAst = await kclManager.updateAst(modifiedAst, false) - this.tearDownSketch({ removeAxis: false }) + await this.tearDownSketch({ removeAxis: false }) sceneInfra.resetMouseListeners() await this.setupSketch({ - sketchEntryNodePath, - sketchNodePaths, + sketchPathToNode, forward, up, position: origin, @@ -752,16 +681,12 @@ export class SceneEntities { forward, up, position: origin, - sketchEntryNodePath, - sketchNodePaths, - planeNodePath, + pathToNode: sketchPathToNode, }) return nextAst } setupDraftSegment = async ( - sketchEntryNodePath: PathToNode, - sketchNodePaths: PathToNode[], - planeNodePath: PathToNode, + sketchPathToNode: PathToNode, forward: [number, number, number], up: [number, number, number], origin: [number, number, number], @@ -772,7 +697,7 @@ export class SceneEntities { const _node1 = getNodeFromPath( _ast, - sketchEntryNodePath || [], + sketchPathToNode || [], 'VariableDeclaration' ) if (trap(_node1)) return Promise.reject(_node1) @@ -795,7 +720,7 @@ export class SceneEntities { from: lastSeg.to, }, fnName: segmentName, - pathToNode: sketchEntryNodePath, + pathToNode: sketchPathToNode, }) if (trap(mod)) return Promise.reject(mod) const pResult = parse(recast(mod.modifiedAst)) @@ -804,18 +729,18 @@ export class SceneEntities { const draftExpressionsIndices = { start: index, end: index } - if (shouldTearDown) this.tearDownSketch({ removeAxis: false }) + if (shouldTearDown) await this.tearDownSketch({ removeAxis: false }) sceneInfra.resetMouseListeners() - const { truncatedAst, programMemoryOverride } = await this.setupSketch({ - sketchEntryNodePath, - sketchNodePaths, - forward, - up, - position: origin, - maybeModdedAst: modifiedAst, - draftExpressionsIndices, - }) + const { truncatedAst, programMemoryOverride, sketch } = + await this.setupSketch({ + sketchPathToNode, + forward, + up, + position: origin, + maybeModdedAst: modifiedAst, + draftExpressionsIndices, + }) sceneInfra.setCallbacks({ onClick: async (args) => { if (!args) return @@ -832,15 +757,7 @@ export class SceneEntities { .map(({ object }) => getParentGroup(object, [PROFILE_START])) .find((a) => a?.name === PROFILE_START) - let modifiedAst: Program | Error = structuredClone(kclManager.ast) - - const sketch = sketchFromPathToNode({ - pathToNode: sketchEntryNodePath, - ast: kclManager.ast, - programMemory: kclManager.programMemory, - }) - if (err(sketch)) return Promise.reject(sketch) - if (!sketch) return Promise.reject(new Error('No sketch found')) + let modifiedAst // Snapping logic for the profile start handle if (intersectsProfileStart) { @@ -848,7 +765,7 @@ export class SceneEntities { modifiedAst = addCallExpressionsToPipe({ node: kclManager.ast, programMemory: kclManager.programMemory, - pathToNode: sketchEntryNodePath, + pathToNode: sketchPathToNode, expressions: [ createCallExpressionStdLib( lastSegment.type === 'TangentialArcTo' @@ -872,7 +789,7 @@ export class SceneEntities { modifiedAst = addCloseToPipe({ node: modifiedAst, programMemory: kclManager.programMemory, - pathToNode: sketchEntryNodePath, + pathToNode: sketchPathToNode, }) if (trap(modifiedAst)) return Promise.reject(modifiedAst) } else if (intersection2d) { @@ -906,11 +823,7 @@ export class SceneEntities { // This might need to become its own function if we want more // case-based logic for different segment types - if ( - (lastSegment.type === 'TangentialArcTo' && - segmentName !== 'line') || - segmentName === 'tangentialArcTo' - ) { + if (lastSegment.type === 'TangentialArcTo') { resolvedFunctionName = 'tangentialArcTo' } else if (isHorizontal) { // If the angle between is 0 or 180 degrees (+/- the snapping angle), make the line an xLine @@ -932,7 +845,7 @@ export class SceneEntities { to: [snappedPoint.x, snappedPoint.y], }, fnName: resolvedFunctionName, - pathToNode: sketchEntryNodePath, + pathToNode: sketchPathToNode, }) if (trap(tmp)) return Promise.reject(tmp) modifiedAst = tmp.modifiedAst @@ -945,12 +858,10 @@ export class SceneEntities { await kclManager.executeAstMock(modifiedAst) if (intersectsProfileStart) { - sceneInfra.modelingSend({ type: 'Close sketch' }) + sceneInfra.modelingSend({ type: 'CancelSketch' }) } else { await this.setupDraftSegment( - sketchEntryNodePath, - sketchNodePaths, - planeNodePath, + sketchPathToNode, forward, up, origin, @@ -961,23 +872,11 @@ export class SceneEntities { await codeManager.updateEditorWithAstAndWriteToFile(modifiedAst) }, onMove: (args) => { - const expressionIndex = Number(sketchEntryNodePath[1][0]) - const activeSegmentsInCorrectExpression = Object.values( - this.activeSegments - ).filter((seg) => { - return seg.userData.pathToNode[1][0] === expressionIndex - }) - const object = - activeSegmentsInCorrectExpression[ - activeSegmentsInCorrectExpression.length - 1 - ] this.onDragSegment({ intersection2d: args.intersectionPoint.twoD, - object, + object: Object.values(this.activeSegments).slice(-1)[0], intersects: args.intersects, - sketchNodePaths, - sketchEntryNodePath, - planeNodePath, + sketchPathToNode, draftInfo: { truncatedAst, programMemoryOverride, @@ -988,82 +887,41 @@ export class SceneEntities { }) } setupDraftRectangle = async ( - sketchEntryNodePath: PathToNode, - sketchNodePaths: PathToNode[], - planeNodePath: PathToNode, + sketchPathToNode: PathToNode, forward: [number, number, number], up: [number, number, number], sketchOrigin: [number, number, number], rectangleOrigin: [x: number, y: number] - ): Promise => { + ) => { let _ast = structuredClone(kclManager.ast) - const varDec = getNodeFromPath( + const _node1 = getNodeFromPath( _ast, - planeNodePath, - 'VariableDeclarator' - ) - - if (err(varDec)) return varDec - if (varDec.node.type !== 'VariableDeclarator') return new Error('not a var') - - const varName = findUniqueName(_ast, 'profile') - - // first create just the variable declaration, as that's - // all we want the user to see in the editor - const tag = findUniqueName(_ast, 'rectangleSegmentA') - const newDeclaration = createVariableDeclaration( - varName, - createCallExpressionStdLib('startProfileAt', [ - createArrayExpression([ - createLiteral(roundOff(rectangleOrigin[0])), - createLiteral(roundOff(rectangleOrigin[1])), - ]), - createIdentifier(varDec.node.id.name), - ]) + sketchPathToNode || [], + 'VariableDeclaration' ) + if (trap(_node1)) return Promise.reject(_node1) + const variableDeclarationName = _node1.node?.declaration.id?.name || '' + const startSketchOn = _node1.node?.declaration + const startSketchOnInit = startSketchOn?.init - const insertIndex = getInsertIndex(sketchNodePaths, planeNodePath, 'end') + const tags: [string, string, string] = [ + findUniqueName(_ast, 'rectangleSegmentA'), + findUniqueName(_ast, 'rectangleSegmentB'), + findUniqueName(_ast, 'rectangleSegmentC'), + ] - _ast.body.splice(insertIndex, 0, newDeclaration) - const { updatedEntryNodePath, updatedSketchNodePaths } = - updateSketchNodePathsWithInsertIndex({ - insertIndex, - insertType: 'end', - sketchNodePaths, - }) + startSketchOn.init = createPipeExpression([ + startSketchOnInit, + ...getRectangleCallExpressions(rectangleOrigin, tags), + ]) const pResult = parse(recast(_ast)) if (trap(pResult) || !resultIsOk(pResult)) return Promise.reject(pResult) _ast = pResult.program - // do a quick mock execution to get the program memory up-to-date - await kclManager.executeAstMock(_ast) - - const justCreatedNode = getNodeFromPath( - _ast, - updatedEntryNodePath, - 'VariableDeclaration' - ) - - if (trap(justCreatedNode)) return Promise.reject(justCreatedNode) - const startProfileAt = justCreatedNode.node?.declaration - // than add the rest of the profile so we can "animate" it - // as draft segments - startProfileAt.init = createPipeExpression([ - startProfileAt?.init, - ...getRectangleCallExpressions(rectangleOrigin, tag), - ]) - - const code = recast(_ast) - const _recastAst = parse(code) - if (trap(_recastAst) || !resultIsOk(_recastAst)) - return Promise.reject(_recastAst) - _ast = _recastAst.program - const { programMemoryOverride, truncatedAst } = await this.setupSketch({ - sketchEntryNodePath: updatedEntryNodePath, - sketchNodePaths: updatedSketchNodePaths, + sketchPathToNode, forward, up, position: sketchOrigin, @@ -1074,17 +932,12 @@ export class SceneEntities { sceneInfra.setCallbacks({ onMove: async (args) => { // Update the width and height of the draft rectangle - - const nodePathWithCorrectedIndexForTruncatedAst = - structuredClone(updatedEntryNodePath) - nodePathWithCorrectedIndexForTruncatedAst[1][0] = - Number(nodePathWithCorrectedIndexForTruncatedAst[1][0]) - - Number(planeNodePath[1][0]) - - 1 + const pathToNodeTwo = structuredClone(sketchPathToNode) + pathToNodeTwo[1][0] = 0 const _node = getNodeFromPath( truncatedAst, - nodePathWithCorrectedIndexForTruncatedAst, + pathToNodeTwo || [], 'VariableDeclaration' ) if (trap(_node)) return Promise.reject(_node) @@ -1094,7 +947,7 @@ export class SceneEntities { const y = (args.intersectionPoint.twoD.y || 0) - rectangleOrigin[1] if (sketchInit.type === 'PipeExpression') { - updateRectangleSketch(sketchInit, x, y, tag) + updateRectangleSketch(sketchInit, x, y, tags[0]) } const { execState } = await executeAst({ @@ -1105,23 +958,17 @@ export class SceneEntities { }) const programMemory = execState.memory this.sceneProgramMemory = programMemory - const sketch = sketchFromKclValue(programMemory.get(varName), varName) + const sketch = sketchFromKclValue( + programMemory.get(variableDeclarationName), + variableDeclarationName + ) if (err(sketch)) return Promise.reject(sketch) const sgPaths = sketch.paths const orthoFactor = orthoScale(sceneInfra.camControls.camera) - const varDecIndex = Number(updatedEntryNodePath[1][0]) - - this.updateSegment( - sketch.start, - 0, - varDecIndex, - _ast, - orthoFactor, - sketch - ) + this.updateSegment(sketch.start, 0, 0, _ast, orthoFactor, sketch) sgPaths.forEach((seg, index) => - this.updateSegment(seg, index, varDecIndex, _ast, orthoFactor, sketch) + this.updateSegment(seg, index, 0, _ast, orthoFactor, sketch) ) }, onClick: async (args) => { @@ -1139,7 +986,7 @@ export class SceneEntities { const _node = getNodeFromPath( _ast, - updatedEntryNodePath, + sketchPathToNode || [], 'VariableDeclaration' ) if (trap(_node)) return @@ -1149,7 +996,7 @@ export class SceneEntities { return } - updateRectangleSketch(sketchInit, x, y, tag) + updateRectangleSketch(sketchInit, x, y, tags[0]) const newCode = recast(_ast) const pResult = parse(newCode) @@ -1159,92 +1006,77 @@ export class SceneEntities { // Update the primary AST and unequip the rectangle tool await kclManager.executeAstMock(_ast) + sceneInfra.modelingSend({ type: 'Finish rectangle' }) // lee: I had this at the bottom of the function, but it's // possible sketchFromKclValue "fails" when sketching on a face, // and this couldn't wouldn't run. await codeManager.updateEditorWithAstAndWriteToFile(_ast) - sceneInfra.modelingSend({ type: 'Finish rectangle' }) + const { execState } = await executeAst({ + ast: _ast, + engineCommandManager: this.engineCommandManager, + // We make sure to send an empty program memory to denote we mean mock mode. + programMemoryOverride, + }) + const programMemory = execState.memory + + // Prepare to update the THREEjs scene + this.sceneProgramMemory = programMemory + const sketch = sketchFromKclValue( + programMemory.get(variableDeclarationName), + variableDeclarationName + ) + if (err(sketch)) return + const sgPaths = sketch.paths + const orthoFactor = orthoScale(sceneInfra.camControls.camera) + + // Update the starting segment of the THREEjs scene + this.updateSegment(sketch.start, 0, 0, _ast, orthoFactor, sketch) + // Update the rest of the segments of the THREEjs scene + sgPaths.forEach((seg, index) => + this.updateSegment(seg, index, 0, _ast, orthoFactor, sketch) + ) }, }) - return { updatedEntryNodePath, updatedSketchNodePaths } } setupDraftCenterRectangle = async ( - sketchEntryNodePath: PathToNode, - sketchNodePaths: PathToNode[], - planeNodePath: PathToNode, + sketchPathToNode: PathToNode, forward: [number, number, number], up: [number, number, number], sketchOrigin: [number, number, number], rectangleOrigin: [x: number, y: number] - ): Promise => { + ) => { let _ast = structuredClone(kclManager.ast) - - const varDec = getNodeFromPath( + const _node1 = getNodeFromPath( _ast, - planeNodePath, - 'VariableDeclarator' - ) - - if (err(varDec)) return varDec - if (varDec.node.type !== 'VariableDeclarator') return new Error('not a var') - - const varName = findUniqueName(_ast, 'profile') - // first create just the variable declaration, as that's - // all we want the user to see in the editor - const tag = findUniqueName(_ast, 'rectangleSegmentA') - const newDeclaration = createVariableDeclaration( - varName, - createCallExpressionStdLib('startProfileAt', [ - createArrayExpression([ - createLiteral(roundOff(rectangleOrigin[0])), - createLiteral(roundOff(rectangleOrigin[1])), - ]), - createIdentifier(varDec.node.id.name), - ]) + sketchPathToNode || [], + 'VariableDeclaration' ) - const insertIndex = getInsertIndex(sketchNodePaths, planeNodePath, 'end') - - _ast.body.splice(insertIndex, 0, newDeclaration) - const { updatedEntryNodePath, updatedSketchNodePaths } = - updateSketchNodePathsWithInsertIndex({ - insertIndex, - insertType: 'end', - sketchNodePaths, - }) - - let __recastAst = parse(recast(_ast)) - if (trap(__recastAst) || !resultIsOk(__recastAst)) - return Promise.reject(__recastAst) - _ast = __recastAst.program + if (trap(_node1)) return Promise.reject(_node1) - // do a quick mock execution to get the program memory up-to-date - await kclManager.executeAstMock(_ast) + // startSketchOn already exists + const variableDeclarationName = _node1.node?.declaration.id?.name || '' + const startSketchOn = _node1.node?.declaration + const startSketchOnInit = startSketchOn?.init - const justCreatedNode = getNodeFromPath( - _ast, - updatedEntryNodePath, - 'VariableDeclaration' - ) + const tags: [string, string, string] = [ + findUniqueName(_ast, 'rectangleSegmentA'), + findUniqueName(_ast, 'rectangleSegmentB'), + findUniqueName(_ast, 'rectangleSegmentC'), + ] - if (trap(justCreatedNode)) return Promise.reject(justCreatedNode) - const startProfileAt = justCreatedNode.node?.declaration - // than add the rest of the profile so we can "animate" it - // as draft segments - startProfileAt.init = createPipeExpression([ - startProfileAt?.init, - ...getRectangleCallExpressions(rectangleOrigin, tag), + startSketchOn.init = createPipeExpression([ + startSketchOnInit, + ...getRectangleCallExpressions(rectangleOrigin, tags), ]) - const code = recast(_ast) - __recastAst = parse(code) - if (trap(__recastAst) || !resultIsOk(__recastAst)) - return Promise.reject(__recastAst) - _ast = __recastAst.program + + const pResult = parse(recast(_ast)) + if (trap(pResult) || !resultIsOk(pResult)) return Promise.reject(pResult) + _ast = pResult.program const { programMemoryOverride, truncatedAst } = await this.setupSketch({ - sketchEntryNodePath: updatedEntryNodePath, - sketchNodePaths: updatedSketchNodePaths, + sketchPathToNode, forward, up, position: sketchOrigin, @@ -1255,17 +1087,12 @@ export class SceneEntities { sceneInfra.setCallbacks({ onMove: async (args) => { // Update the width and height of the draft rectangle - - const nodePathWithCorrectedIndexForTruncatedAst = - structuredClone(updatedEntryNodePath) - nodePathWithCorrectedIndexForTruncatedAst[1][0] = - Number(nodePathWithCorrectedIndexForTruncatedAst[1][0]) - - Number(planeNodePath[1][0]) - - 1 + const pathToNodeTwo = structuredClone(sketchPathToNode) + pathToNodeTwo[1][0] = 0 const _node = getNodeFromPath( truncatedAst, - nodePathWithCorrectedIndexForTruncatedAst, + pathToNodeTwo || [], 'VariableDeclaration' ) if (trap(_node)) return Promise.reject(_node) @@ -1279,7 +1106,7 @@ export class SceneEntities { sketchInit, x, y, - tag, + tags[0], rectangleOrigin[0], rectangleOrigin[1] ) @@ -1293,23 +1120,17 @@ export class SceneEntities { }) const programMemory = execState.memory this.sceneProgramMemory = programMemory - const sketch = sketchFromKclValue(programMemory.get(varName), varName) + const sketch = sketchFromKclValue( + programMemory.get(variableDeclarationName), + variableDeclarationName + ) if (err(sketch)) return Promise.reject(sketch) const sgPaths = sketch.paths const orthoFactor = orthoScale(sceneInfra.camControls.camera) - const varDecIndex = Number(updatedEntryNodePath[1][0]) - - this.updateSegment( - sketch.start, - 0, - varDecIndex, - _ast, - orthoFactor, - sketch - ) + this.updateSegment(sketch.start, 0, 0, _ast, orthoFactor, sketch) sgPaths.forEach((seg, index) => - this.updateSegment(seg, index, varDecIndex, _ast, orthoFactor, sketch) + this.updateSegment(seg, index, 0, _ast, orthoFactor, sketch) ) }, onClick: async (args) => { @@ -1327,7 +1148,7 @@ export class SceneEntities { const _node = getNodeFromPath( _ast, - updatedEntryNodePath, + sketchPathToNode || [], 'VariableDeclaration' ) if (trap(_node)) return @@ -1338,7 +1159,7 @@ export class SceneEntities { sketchInit, x, y, - tag, + tags[0], rectangleOrigin[0], rectangleOrigin[1] ) @@ -1350,41 +1171,62 @@ export class SceneEntities { // Update the primary AST and unequip the rectangle tool await kclManager.executeAstMock(_ast) + sceneInfra.modelingSend({ type: 'Finish center rectangle' }) // lee: I had this at the bottom of the function, but it's // possible sketchFromKclValue "fails" when sketching on a face, // and this couldn't wouldn't run. await codeManager.updateEditorWithAstAndWriteToFile(_ast) - sceneInfra.modelingSend({ type: 'Finish center rectangle' }) + const { execState } = await executeAst({ + ast: _ast, + engineCommandManager: this.engineCommandManager, + // We make sure to send an empty program memory to denote we mean mock mode. + programMemoryOverride, + }) + const programMemory = execState.memory + + // Prepare to update the THREEjs scene + this.sceneProgramMemory = programMemory + const sketch = sketchFromKclValue( + programMemory.get(variableDeclarationName), + variableDeclarationName + ) + if (err(sketch)) return + const sgPaths = sketch.paths + const orthoFactor = orthoScale(sceneInfra.camControls.camera) + + // Update the starting segment of the THREEjs scene + this.updateSegment(sketch.start, 0, 0, _ast, orthoFactor, sketch) + // Update the rest of the segments of the THREEjs scene + sgPaths.forEach((seg, index) => + this.updateSegment(seg, index, 0, _ast, orthoFactor, sketch) + ) } }, }) - return { updatedEntryNodePath, updatedSketchNodePaths } } setupDraftCircle = async ( - sketchEntryNodePath: PathToNode, - sketchNodePaths: PathToNode[], - planeNodePath: PathToNode, + sketchPathToNode: PathToNode, forward: [number, number, number], up: [number, number, number], sketchOrigin: [number, number, number], circleCenter: [x: number, y: number] - ): Promise => { + ) => { let _ast = structuredClone(kclManager.ast) - const varDec = getNodeFromPath( + const _node1 = getNodeFromPath( _ast, - planeNodePath, - 'VariableDeclarator' + sketchPathToNode || [], + 'VariableDeclaration' ) + if (trap(_node1)) return Promise.reject(_node1) + const variableDeclarationName = _node1.node?.declaration.id?.name || '' + const startSketchOn = _node1.node?.declaration + const startSketchOnInit = startSketchOn?.init - if (err(varDec)) return varDec - if (varDec.node.type !== 'VariableDeclarator') return new Error('not a var') - - const varName = findUniqueName(_ast, 'profile') - const newExpression = createVariableDeclaration( - varName, + startSketchOn.init = createPipeExpression([ + startSketchOnInit, createCallExpressionStdLib('circle', [ createObjectExpression({ center: createArrayExpression([ @@ -1393,19 +1235,9 @@ export class SceneEntities { ]), radius: createLiteral(1), }), - createIdentifier(varDec.node.id.name), - ]) - ) - - const insertIndex = getInsertIndex(sketchNodePaths, planeNodePath, 'end') - - _ast.body.splice(insertIndex, 0, newExpression) - const { updatedEntryNodePath, updatedSketchNodePaths } = - updateSketchNodePathsWithInsertIndex({ - insertIndex, - insertType: 'end', - sketchNodePaths, - }) + createPipeSubstitution(), + ]), + ]) const pResult = parse(recast(_ast)) if (trap(pResult) || !resultIsOk(pResult)) return Promise.reject(pResult) @@ -1415,8 +1247,7 @@ export class SceneEntities { await kclManager.executeAstMock(_ast) const { programMemoryOverride, truncatedAst } = await this.setupSketch({ - sketchEntryNodePath: updatedEntryNodePath, - sketchNodePaths: updatedSketchNodePaths, + sketchPathToNode, forward, up, position: sketchOrigin, @@ -1426,15 +1257,12 @@ export class SceneEntities { sceneInfra.setCallbacks({ onMove: async (args) => { - const nodePathWithCorrectedIndexForTruncatedAst = - structuredClone(updatedEntryNodePath) - nodePathWithCorrectedIndexForTruncatedAst[1][0] = - Number(nodePathWithCorrectedIndexForTruncatedAst[1][0]) - - Number(planeNodePath[1][0]) - - 1 + const pathToNodeTwo = structuredClone(sketchPathToNode) + pathToNodeTwo[1][0] = 0 + const _node = getNodeFromPath( truncatedAst, - nodePathWithCorrectedIndexForTruncatedAst, + pathToNodeTwo || [], 'VariableDeclaration' ) let modded = structuredClone(truncatedAst) @@ -1444,13 +1272,17 @@ export class SceneEntities { const x = (args.intersectionPoint.twoD.x || 0) - circleCenter[0] const y = (args.intersectionPoint.twoD.y || 0) - circleCenter[1] - if (sketchInit.type === 'CallExpression') { + if (sketchInit.type === 'PipeExpression') { const moddedResult = changeSketchArguments( modded, kclManager.programMemory, { type: 'path', - pathToNode: nodePathWithCorrectedIndexForTruncatedAst, + pathToNode: [ + ..._node.deepPath, + ['body', 'PipeExpression'], + [1, 'index'], + ], }, { type: 'arc-segment', @@ -1471,23 +1303,17 @@ export class SceneEntities { }) const programMemory = execState.memory this.sceneProgramMemory = programMemory - const sketch = sketchFromKclValue(programMemory.get(varName), varName) + const sketch = sketchFromKclValue( + programMemory.get(variableDeclarationName), + variableDeclarationName + ) if (err(sketch)) return const sgPaths = sketch.paths const orthoFactor = orthoScale(sceneInfra.camControls.camera) - const varDecIndex = Number(updatedEntryNodePath[1][0]) - - this.updateSegment( - sketch.start, - 0, - varDecIndex, - _ast, - orthoFactor, - sketch - ) + this.updateSegment(sketch.start, 0, 0, _ast, orthoFactor, sketch) sgPaths.forEach((seg, index) => - this.updateSegment(seg, index, varDecIndex, _ast, orthoFactor, sketch) + this.updateSegment(seg, index, 0, _ast, orthoFactor, sketch) ) }, onClick: async (args) => { @@ -1505,20 +1331,24 @@ export class SceneEntities { const _node = getNodeFromPath( _ast, - updatedEntryNodePath || [], + sketchPathToNode || [], 'VariableDeclaration' ) if (trap(_node)) return const sketchInit = _node.node?.declaration.init let modded = structuredClone(_ast) - if (sketchInit.type === 'CallExpression') { + if (sketchInit.type === 'PipeExpression') { const moddedResult = changeSketchArguments( modded, kclManager.programMemory, { type: 'path', - pathToNode: updatedEntryNodePath, + pathToNode: [ + ..._node.deepPath, + ['body', 'PipeExpression'], + [1, 'index'], + ], }, { type: 'arc-segment', @@ -1539,25 +1369,20 @@ export class SceneEntities { // Update the primary AST and unequip the rectangle tool await kclManager.executeAstMock(_ast) - await codeManager.updateEditorWithAstAndWriteToFile(_ast) - sceneInfra.modelingSend({ type: 'Finish circle' }) + + await codeManager.updateEditorWithAstAndWriteToFile(_ast) } }, }) - return { updatedEntryNodePath, updatedSketchNodePaths } } setupSketchIdleCallbacks = ({ - sketchEntryNodePath, - sketchNodePaths, - planeNodePath, + pathToNode, up, forward, position, }: { - sketchEntryNodePath: PathToNode - sketchNodePaths: PathToNode[] - planeNodePath: PathToNode + pathToNode: PathToNode forward: [number, number, number] up: [number, number, number] position?: [number, number, number] @@ -1566,11 +1391,10 @@ export class SceneEntities { sceneInfra.setCallbacks({ onDragEnd: async () => { if (addingNewSegmentStatus !== 'nothing') { - this.tearDownSketch({ removeAxis: false }) + await this.tearDownSketch({ removeAxis: false }) // eslint-disable-next-line @typescript-eslint/no-floating-promises this.setupSketch({ - sketchEntryNodePath, - sketchNodePaths, + sketchPathToNode: pathToNode, maybeModdedAst: kclManager.ast, up, forward, @@ -1578,9 +1402,7 @@ export class SceneEntities { }) // setting up the callbacks again resets value in closures this.setupSketchIdleCallbacks({ - sketchEntryNodePath, - sketchNodePaths, - planeNodePath, + pathToNode, up, forward, position, @@ -1637,11 +1459,10 @@ export class SceneEntities { if (trap(mod)) return await kclManager.executeAstMock(mod.modifiedAst) - this.tearDownSketch({ removeAxis: false }) + await this.tearDownSketch({ removeAxis: false }) // eslint-disable-next-line @typescript-eslint/no-floating-promises this.setupSketch({ - sketchEntryNodePath: pathToNode, - sketchNodePaths, + sketchPathToNode: pathToNode, maybeModdedAst: kclManager.ast, up, forward, @@ -1652,9 +1473,7 @@ export class SceneEntities { const pathToNodeForNewSegment = pathToNode.slice(0, pathToNodeIndex) pathToNodeForNewSegment.push([pipeIndex - 2, 'index']) this.onDragSegment({ - sketchNodePaths, - sketchEntryNodePath: pathToNodeForNewSegment, - planeNodePath, + sketchPathToNode: pathToNodeForNewSegment, object: selected, intersection2d: intersectionPoint.twoD, intersects, @@ -1666,10 +1485,8 @@ export class SceneEntities { this.onDragSegment({ object: selected, intersection2d: intersectionPoint.twoD, - planeNodePath, intersects, - sketchNodePaths, - sketchEntryNodePath, + sketchPathToNode: pathToNode, }) }, onMove: () => {}, @@ -1698,12 +1515,12 @@ export class SceneEntities { }) } prepareTruncatedMemoryAndAst = ( - sketchNodePaths: PathToNode[], + sketchPathToNode: PathToNode, ast?: Node, draftSegment?: DraftSegment ) => prepareTruncatedMemoryAndAst( - sketchNodePaths, + sketchPathToNode, ast || kclManager.ast, kclManager.lastSuccessfulProgramMemory, draftSegment @@ -1711,17 +1528,13 @@ export class SceneEntities { onDragSegment({ object, intersection2d: _intersection2d, - sketchEntryNodePath, - sketchNodePaths, - planeNodePath, + sketchPathToNode, draftInfo, intersects, }: { object: any intersection2d: Vector2 - sketchEntryNodePath: PathToNode - sketchNodePaths: PathToNode[] - planeNodePath: PathToNode + sketchPathToNode: PathToNode intersects: Intersection>[] draftInfo?: { truncatedAst: Node @@ -1764,6 +1577,9 @@ export class SceneEntities { ) return } + if (draftInfo) { + pathToNode[1][0] = 0 + } const from: [number, number] = [ group.userData.from[0], @@ -1772,15 +1588,9 @@ export class SceneEntities { const dragTo: [number, number] = [snappedPoint.x, snappedPoint.y] let modifiedAst = draftInfo ? draftInfo.truncatedAst : { ...kclManager.ast } - const nodePathWithCorrectedIndexForTruncatedAst = - structuredClone(pathToNode) - nodePathWithCorrectedIndexForTruncatedAst[1][0] = - Number(nodePathWithCorrectedIndexForTruncatedAst[1][0]) - - Number(sketchNodePaths[0][1][0]) - const _node = getNodeFromPath>( modifiedAst, - draftInfo ? nodePathWithCorrectedIndexForTruncatedAst : pathToNode, + pathToNode, 'CallExpression' ) if (trap(_node)) return @@ -1858,9 +1668,10 @@ export class SceneEntities { modifiedAst = modded.modifiedAst const info = draftInfo ? draftInfo - : this.prepareTruncatedMemoryAndAst(sketchNodePaths || [], modifiedAst) + : this.prepareTruncatedMemoryAndAst(pathToNode || []) if (trap(info, { suppress: true })) return - const { truncatedAst, programMemoryOverride } = info + const { truncatedAst, programMemoryOverride, variableDeclarationName } = + info ;(async () => { const code = recast(modifiedAst) if (trap(code)) return @@ -1876,43 +1687,42 @@ export class SceneEntities { }) const programMemory = execState.memory this.sceneProgramMemory = programMemory - const sketchesInfo = getSketchesInfo({ - sketchNodePaths, - ast: truncatedAst, - programMemory, - }) - const callBacks: (() => SegmentOverlayPayload | null)[] = [] - for (const sketchInfo of sketchesInfo) { - const { sketch, pathToNode: _pathToNode } = sketchInfo - const varDecIndex = Number(_pathToNode[1][0]) - if (!sketch) return + const maybeSketch = programMemory.get(variableDeclarationName) + let sketch: Sketch | undefined + const sk = sketchFromKclValueOptional( + maybeSketch, + variableDeclarationName + ) + if (!(sk instanceof Reason)) { + sketch = sk + } else if ((maybeSketch as Solid).sketch) { + sketch = (maybeSketch as Solid).sketch + } + if (!sketch) return - const sgPaths = sketch.paths - const orthoFactor = orthoScale(sceneInfra.camControls.camera) + const sgPaths = sketch.paths + const orthoFactor = orthoScale(sceneInfra.camControls.camera) + this.updateSegment( + sketch.start, + 0, + varDecIndex, + modifiedAst, + orthoFactor, + sketch + ) + + const callBacks = sgPaths.map((group, index) => this.updateSegment( - sketch.start, - 0, + group, + index, varDecIndex, modifiedAst, orthoFactor, sketch ) - - callBacks.push( - ...sgPaths.map((group, index) => - this.updateSegment( - group, - index, - varDecIndex, - modifiedAst, - orthoFactor, - sketch - ) - ) - ) - } + ) sceneInfra.overlayCallbacks(callBacks) })().catch(reportRejection) } @@ -1947,6 +1757,7 @@ export class SceneEntities { const group = this.activeSegments[pathToNodeStr] || this.activeSegments[originalPathToNodeStr] + // const prevSegment = sketch.slice(index - 1)[0] const type = group?.userData?.type const factor = (sceneInfra.camControls.camera instanceof OrthographicCamera @@ -2017,7 +1828,12 @@ export class SceneEntities { removeSketchGrid() { if (this.axisGroup) this.scene.remove(this.axisGroup) } - tearDownSketch({ removeAxis = true }: { removeAxis?: boolean }) { + private _tearDownSketch( + callDepth = 0, + resolve: (val: unknown) => void, + reject: () => void, + { removeAxis = true }: { removeAxis?: boolean } + ) { // Remove all draft groups this.draftPointGroups.forEach((draftPointGroup) => { this.scene.remove(draftPointGroup) @@ -2026,6 +1842,7 @@ export class SceneEntities { const sketchSegments = this.scene.children.find( ({ userData }) => userData?.type === SKETCH_GROUP_SEGMENTS ) + let shouldResolve = false if (sketchSegments) { // We have to manually remove the CSS2DObjects // as they don't get removed when the group is removed @@ -2036,9 +1853,36 @@ export class SceneEntities { } }) this.scene.remove(sketchSegments) + shouldResolve = true + } else { + const delay = 100 + const maxTimeRetries = 3000 // 3 seconds + const maxCalls = maxTimeRetries / delay + if (callDepth < maxCalls) { + setTimeout(() => { + this._tearDownSketch(callDepth + 1, resolve, reject, { removeAxis }) + }, delay) + } else { + resolve(true) + } } sceneInfra.camControls.enableRotate = true this.activeSegments = {} + // maybe should reset onMove etc handlers + if (shouldResolve) resolve(true) + } + async tearDownSketch({ + removeAxis = true, + }: { + removeAxis?: boolean + } = {}) { + // I think promisifying this is mostly a side effect of not having + // "setupSketch" correctly capture a promise when it's done + // so we're effectively waiting for to be finished setting up the scene just to tear it down + // TODO is to fix that + return new Promise((resolve, reject) => { + this._tearDownSketch(0, resolve, reject, { removeAxis }) + }) } mouseEnterLeaveCallbacks() { return { @@ -2192,7 +2036,7 @@ export type DefaultPlaneStr = 'XY' | 'XZ' | 'YZ' | '-XY' | '-XZ' | '-YZ' // calculations/pure-functions/easy to test so no excuse not to function prepareTruncatedMemoryAndAst( - sketchNodePaths: PathToNode[], + sketchPathToNode: PathToNode, ast: Node, programMemory: ProgramMemory, draftSegment?: DraftSegment @@ -2200,19 +2044,15 @@ function prepareTruncatedMemoryAndAst( | { truncatedAst: Node programMemoryOverride: ProgramMemory - // can I remove the below? variableDeclarationName: string } | Error { - const bodyStartIndex = Number(sketchNodePaths?.[0]?.[1]?.[0]) || 0 - const bodyEndIndex = - Number(sketchNodePaths[sketchNodePaths.length - 1]?.[1]?.[0]) || - ast.body.length + const bodyIndex = Number(sketchPathToNode?.[1]?.[0]) || 0 const _ast = structuredClone(ast) const _node = getNodeFromPath>( _ast, - sketchNodePaths[0] || [], + sketchPathToNode || [], 'VariableDeclaration' ) if (err(_node)) return _node @@ -2241,7 +2081,7 @@ function prepareTruncatedMemoryAndAst( ]) } ;( - (_ast.body[bodyStartIndex] as VariableDeclaration).declaration + (_ast.body[bodyIndex] as VariableDeclaration).declaration .init as PipeExpression ).body.push(newSegment) // update source ranges to section we just added. @@ -2252,17 +2092,17 @@ function prepareTruncatedMemoryAndAst( const updatedSrcRangeAst = pResult.program const lastPipeItem = ( - (updatedSrcRangeAst.body[bodyStartIndex] as VariableDeclaration) - .declaration.init as PipeExpression + (updatedSrcRangeAst.body[bodyIndex] as VariableDeclaration).declaration + .init as PipeExpression ).body.slice(-1)[0] ;( - (_ast.body[bodyStartIndex] as VariableDeclaration).declaration + (_ast.body[bodyIndex] as VariableDeclaration).declaration .init as PipeExpression ).body.slice(-1)[0].start = lastPipeItem.start _ast.end = lastPipeItem.end - const varDec = _ast.body[bodyStartIndex] as Node + const varDec = _ast.body[bodyIndex] as Node varDec.end = lastPipeItem.end const declarator = varDec.declaration declarator.end = lastPipeItem.end @@ -2272,7 +2112,7 @@ function prepareTruncatedMemoryAndAst( } const truncatedAst: Node = { ..._ast, - body: structuredClone(_ast.body.slice(bodyStartIndex, bodyEndIndex + 1)), + body: [structuredClone(_ast.body[bodyIndex])], } // Grab all the TagDeclarators and TagIdentifiers from memory. @@ -2296,7 +2136,7 @@ function prepareTruncatedMemoryAndAst( }) if (err(programMemoryOverride)) return programMemoryOverride - for (let i = 0; i < bodyStartIndex; i++) { + for (let i = 0; i < bodyIndex; i++) { const node = _ast.body[i] if (node.type !== 'VariableDeclaration') { continue @@ -2392,16 +2232,13 @@ export function getSketchQuaternion( return getQuaternionFromZAxis(massageFormats(zAxis)) } export async function getSketchOrientationDetails( - sketchEntryNodePath: PathToNode + sketchPathToNode: PathToNode ): Promise<{ quat: Quaternion - sketchDetails: Omit< - SketchDetails & { faceId?: string }, - 'sketchNodePaths' | 'sketchEntryNodePath' | 'planeNodePath' - > + sketchDetails: SketchDetails & { faceId?: string } }> { const sketch = sketchFromPathToNode({ - pathToNode: sketchEntryNodePath, + pathToNode: sketchPathToNode, ast: kclManager.ast, programMemory: kclManager.programMemory, }) @@ -2413,6 +2250,7 @@ export async function getSketchOrientationDetails( return { quat: getQuaternionFromZAxis(massageFormats(zAxis)), sketchDetails: { + sketchPathToNode, zAxis: [zAxis.x, zAxis.y, zAxis.z], yAxis: [sketch.on.yAxis.x, sketch.on.yAxis.y, sketch.on.yAxis.z], origin: [0, 0, 0], @@ -2434,6 +2272,7 @@ export async function getSketchOrientationDetails( return { quat: quaternion, sketchDetails: { + sketchPathToNode, zAxis: [z_axis.x, z_axis.y, z_axis.z], yAxis: [y_axis.x, y_axis.y, y_axis.z], origin: [origin.x, origin.y, origin.z], @@ -2512,35 +2351,3 @@ export function getQuaternionFromZAxis(zAxis: Vector3): Quaternion { function massageFormats(a: Vec3Array | Point3d): Vector3 { return isArray(a) ? new Vector3(a[0], a[1], a[2]) : new Vector3(a.x, a.y, a.z) } - -function getSketchesInfo({ - sketchNodePaths, - ast, - programMemory, -}: { - sketchNodePaths: PathToNode[] - ast: Node - programMemory: ProgramMemory -}): { - sketch: Sketch - pathToNode: PathToNode -}[] { - const sketchesInfo: { - sketch: Sketch - pathToNode: PathToNode - }[] = [] - for (const path of sketchNodePaths) { - const sketch = sketchFromPathToNode({ - pathToNode: path, - ast, - programMemory, - }) - if (err(sketch)) continue - if (!sketch) continue - sketchesInfo.push({ - sketch, - pathToNode: path, - }) - } - return sketchesInfo -} diff --git a/src/clientSideScene/segments.ts b/src/clientSideScene/segments.ts index c68fc58fb9..f2e47e09a8 100644 --- a/src/clientSideScene/segments.ts +++ b/src/clientSideScene/segments.ts @@ -691,21 +691,19 @@ 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(size, size, size) // in pixels scaled later + const geometry = new BoxGeometry(12, 12, 12) // in pixels scaled later const baseColor = getThemeColorForThreeJs(theme) const color = isSelected ? 0x0000ff : baseColor const body = new MeshBasicMaterial({ color }) diff --git a/src/components/ModelingMachineProvider.tsx b/src/components/ModelingMachineProvider.tsx index b9b99bed53..3b921bd488 100644 --- a/src/components/ModelingMachineProvider.tsx +++ b/src/components/ModelingMachineProvider.tsx @@ -24,7 +24,7 @@ import { useSetupEngineManager } from 'hooks/useSetupEngineManager' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { isCursorInSketchCommandRange, - updateSketchDetailsNodePaths, + updatePathToNodeFromMap, } from 'lang/util' import { kclManager, @@ -71,24 +71,14 @@ import { replaceValueAtNodePath, sketchOnExtrudedFace, sketchOnOffsetPlane, - splitPipedProfile, startSketchOnDefault, } from 'lang/modifyAst' -import { - PathToNode, - Program, - VariableDeclaration, - parse, - recast, - resultIsOk, -} from 'lang/wasm' +import { PathToNode, Program, parse, recast, resultIsOk } from 'lang/wasm' import { doesSceneHaveExtrudedSketch, doesSceneHaveSweepableSketch, - doesSketchPipeNeedSplitting, - getNodeFromPath, - isCursorInFunctionDefinition, - traverse, + getNodePathFromSourceRange, + isSingleCursorInPipe, } from 'lang/queryAst' import { exportFromEngine } from 'lib/exportFromEngine' import { Models } from '@kittycad/lib/dist/types/src' @@ -96,7 +86,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, reject } from 'lib/trap' +import { err, reportRejection, trap } from 'lib/trap' import { useCommandsContext } from 'hooks/useCommandsContext' import { modelingMachineEvent } from 'editor/manager' import { hasValidEdgeTreatmentSelection } from 'lang/modifyAst/addEdgeTreatment' @@ -110,10 +100,6 @@ 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 = { state: StateFrom @@ -304,7 +290,7 @@ export const ModelingMachineProvider = ({ return { sketchDetails: { ...sketchDetails, - sketchEntryNodePath: event.data, + sketchPathToNode: event.data, }, } }), @@ -427,17 +413,9 @@ export const ModelingMachineProvider = ({ selectionRanges: setSelections.selection, sketchDetails: { ...sketchDetails, - sketchEntryNodePath: - setSelections.updatedSketchEntryNodePath || - sketchDetails?.sketchEntryNodePath || - [], - sketchNodePaths: - setSelections.updatedSketchNodePaths || - sketchDetails?.sketchNodePaths || - [], - planeNodePath: - setSelections.updatedPlaneNodePath || - sketchDetails?.planeNodePath || + sketchPathToNode: + setSelections.updatedPathToNode || + sketchDetails?.sketchPathToNode || [], }, } @@ -647,6 +625,7 @@ export const ModelingMachineProvider = ({ } const canShell = canShellSelection(selectionRanges) + console.log('canShellSelection', canShellSelection(selectionRanges)) if (err(canShell)) return false return canShell }, @@ -669,12 +648,7 @@ export const ModelingMachineProvider = ({ 'Selection is on face': ({ context: { selectionRanges }, event }) => { if (event.type !== 'Enter sketch') return false if (event.data?.forceNewSketch) return false - if ( - isCursorInFunctionDefinition( - kclManager.ast, - selectionRanges.graphSelections[0] - ) - ) + if (!isSingleCursorInPipe(selectionRanges, kclManager.ast)) return false return !!isCursorInSketchCommandRange( engineCommandManager.artifactGraph, @@ -705,32 +679,10 @@ 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.planeNodePath[1][0] - - const varDec = getNodeFromPath( - 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 - + const varDecIndex = sketchDetails.sketchPathToNode[1][0] // remove body item at varDecIndex newAst.body = newAst.body.filter((_, i) => i !== varDecIndex) await kclManager.executeAstMock(newAst) - await codeManager.updateEditorWithAstAndWriteToFile(newAst) } sceneInfra.setCallbacks({ onClick: () => {}, @@ -740,7 +692,7 @@ export const ModelingMachineProvider = ({ } ), 'animate-to-face': fromPromise(async ({ input }) => { - if (!input) return null + if (!input) return undefined if (input.type === 'extrudeFace' || input.type === 'offsetPlane') { const sketched = input.type === 'extrudeFace' @@ -767,9 +719,7 @@ export const ModelingMachineProvider = ({ await letEngineAnimateAndSyncCamAfter(engineCommandManager, id) sceneInfra.camControls.syncDirection = 'clientToEngine' return { - sketchEntryNodePath: [], - planeNodePath: pathToNewSketchNode, - sketchNodePaths: [], + sketchPathToNode: pathToNewSketchNode, zAxis: input.zAxis, yAxis: input.yAxis, origin: input.position, @@ -789,9 +739,7 @@ export const ModelingMachineProvider = ({ ) return { - sketchEntryNodePath: [], - planeNodePath: pathToNode, - sketchNodePaths: [], + sketchPathToNode: pathToNode, zAxis: input.zAxis, yAxis: input.yAxis, origin: [0, 0, 0], @@ -799,14 +747,12 @@ export const ModelingMachineProvider = ({ }), 'animate-to-sketch': fromPromise( async ({ input: { selectionRanges } }) => { - const sketchPathToNode = - selectionRanges.graphSelections[0]?.codeRef?.pathToNode - const plane = getPlaneFromArtifact( - selectionRanges.graphSelections[0].artifact, - engineCommandManager.artifactGraph + const sourceRange = + selectionRanges.graphSelections[0]?.codeRef?.range + const sketchPathToNode = getNodePathFromSourceRange( + kclManager.ast, + sourceRange ) - if (err(plane)) return Promise.reject(plane) - const info = await getSketchOrientationDetails( sketchPathToNode || [] ) @@ -814,17 +760,8 @@ export const ModelingMachineProvider = ({ engineCommandManager, info?.sketchDetails?.faceId || '' ) - 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, + sketchPathToNode: sketchPathToNode || [], zAxis: info.sketchDetails.zAxis || null, yAxis: info.sketchDetails.yAxis || null, origin: info.sketchDetails.origin.map( @@ -836,7 +773,7 @@ export const ModelingMachineProvider = ({ 'Get horizontal info': fromPromise( async ({ input: { selectionRanges, sketchDetails } }) => { - const { modifiedAst, pathToNodeMap, exprInsertIndex } = + const { modifiedAst, pathToNodeMap } = await applyConstraintHorzVertDistance({ constraint: 'setHorzDistance', selectionRanges, @@ -848,23 +785,13 @@ export const ModelingMachineProvider = ({ if (!sketchDetails) return Promise.reject(new Error('No sketch details')) - - const { - updatedSketchEntryNodePath, - updatedSketchNodePaths, - updatedPlaneNodePath, - } = updateSketchDetailsNodePaths({ - sketchEntryNodePath: sketchDetails.sketchEntryNodePath, - sketchNodePaths: sketchDetails.sketchNodePaths, - planeNodePath: sketchDetails.planeNodePath, - exprInsertIndex, - }) - + const updatedPathToNode = updatePathToNodeFromMap( + sketchDetails.sketchPathToNode, + pathToNodeMap + ) const updatedAst = await sceneEntitiesManager.updateAstAndRejigSketch( - updatedSketchEntryNodePath, - updatedSketchNodePaths, - updatedPlaneNodePath, + updatedPathToNode, _modifiedAst, sketchDetails.zAxis, sketchDetails.yAxis, @@ -885,15 +812,13 @@ export const ModelingMachineProvider = ({ return { selectionType: 'completeSelection', selection, - updatedSketchEntryNodePath, - updatedSketchNodePaths, - updatedPlaneNodePath, + updatedPathToNode, } } ), 'Get vertical info': fromPromise( async ({ input: { selectionRanges, sketchDetails } }) => { - const { modifiedAst, pathToNodeMap, exprInsertIndex } = + const { modifiedAst, pathToNodeMap } = await applyConstraintHorzVertDistance({ constraint: 'setVertDistance', selectionRanges, @@ -904,23 +829,13 @@ export const ModelingMachineProvider = ({ const _modifiedAst = pResult.program if (!sketchDetails) return Promise.reject(new Error('No sketch details')) - - const { - updatedSketchEntryNodePath, - updatedSketchNodePaths, - updatedPlaneNodePath, - } = updateSketchDetailsNodePaths({ - sketchEntryNodePath: sketchDetails.sketchEntryNodePath, - sketchNodePaths: sketchDetails.sketchNodePaths, - planeNodePath: sketchDetails.planeNodePath, - exprInsertIndex, - }) - + const updatedPathToNode = updatePathToNodeFromMap( + sketchDetails.sketchPathToNode, + pathToNodeMap + ) const updatedAst = await sceneEntitiesManager.updateAstAndRejigSketch( - updatedSketchEntryNodePath, - updatedSketchNodePaths, - updatedPlaneNodePath, + updatedPathToNode, _modifiedAst, sketchDetails.zAxis, sketchDetails.yAxis, @@ -941,9 +856,7 @@ export const ModelingMachineProvider = ({ return { selectionType: 'completeSelection', selection, - updatedSketchEntryNodePath, - updatedSketchNodePaths, - updatedPlaneNodePath, + updatedPathToNode, } } ), @@ -953,15 +866,14 @@ export const ModelingMachineProvider = ({ selectionRanges, }) if (err(info)) return Promise.reject(info) - const { modifiedAst, pathToNodeMap, exprInsertIndex } = - await (info.enabled - ? applyConstraintAngleBetween({ - selectionRanges, - }) - : applyConstraintAngleLength({ - selectionRanges, - angleOrLength: 'setAngle', - })) + const { modifiedAst, pathToNodeMap } = 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')) @@ -970,23 +882,13 @@ export const ModelingMachineProvider = ({ if (!sketchDetails) return Promise.reject(new Error('No sketch details')) - - const { - updatedSketchEntryNodePath, - updatedSketchNodePaths, - updatedPlaneNodePath, - } = updateSketchDetailsNodePaths({ - sketchEntryNodePath: sketchDetails.sketchEntryNodePath, - sketchNodePaths: sketchDetails.sketchNodePaths, - planeNodePath: sketchDetails.planeNodePath, - exprInsertIndex, - }) - + const updatedPathToNode = updatePathToNodeFromMap( + sketchDetails.sketchPathToNode, + pathToNodeMap + ) const updatedAst = await sceneEntitiesManager.updateAstAndRejigSketch( - updatedSketchEntryNodePath, - updatedSketchNodePaths, - updatedPlaneNodePath, + updatedPathToNode, _modifiedAst, sketchDetails.zAxis, sketchDetails.yAxis, @@ -1007,9 +909,7 @@ export const ModelingMachineProvider = ({ return { selectionType: 'completeSelection', selection, - updatedSketchEntryNodePath, - updatedSketchNodePaths, - updatedPlaneNodePath, + updatedPathToNode, } } ), @@ -1024,30 +924,20 @@ export const ModelingMachineProvider = ({ length: lengthValue, }) if (err(constraintResult)) return Promise.reject(constraintResult) - const { modifiedAst, pathToNodeMap, exprInsertIndex } = - constraintResult + const { modifiedAst, pathToNodeMap } = 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 { - updatedSketchEntryNodePath, - updatedSketchNodePaths, - updatedPlaneNodePath, - } = updateSketchDetailsNodePaths({ - sketchEntryNodePath: sketchDetails.sketchEntryNodePath, - sketchNodePaths: sketchDetails.sketchNodePaths, - planeNodePath: sketchDetails.planeNodePath, - exprInsertIndex, - }) + const updatedPathToNode = updatePathToNodeFromMap( + sketchDetails.sketchPathToNode, + pathToNodeMap + ) const updatedAst = await sceneEntitiesManager.updateAstAndRejigSketch( - updatedSketchEntryNodePath, - updatedSketchNodePaths, - updatedPlaneNodePath, + updatedPathToNode, _modifiedAst, sketchDetails.zAxis, sketchDetails.yAxis, @@ -1068,15 +958,13 @@ export const ModelingMachineProvider = ({ return { selectionType: 'completeSelection', selection, - updatedSketchEntryNodePath, - updatedSketchNodePaths, - updatedPlaneNodePath, + updatedPathToNode, } } ), 'Get perpendicular distance info': fromPromise( async ({ input: { selectionRanges, sketchDetails } }) => { - const { modifiedAst, pathToNodeMap, exprInsertIndex } = + const { modifiedAst, pathToNodeMap } = await applyConstraintIntersect({ selectionRanges, }) @@ -1086,22 +974,13 @@ export const ModelingMachineProvider = ({ const _modifiedAst = pResult.program if (!sketchDetails) return Promise.reject(new Error('No sketch details')) - - const { - updatedSketchEntryNodePath, - updatedSketchNodePaths, - updatedPlaneNodePath, - } = updateSketchDetailsNodePaths({ - sketchEntryNodePath: sketchDetails.sketchEntryNodePath, - sketchNodePaths: sketchDetails.sketchNodePaths, - planeNodePath: sketchDetails.planeNodePath, - exprInsertIndex, - }) + const updatedPathToNode = updatePathToNodeFromMap( + sketchDetails.sketchPathToNode, + pathToNodeMap + ) const updatedAst = await sceneEntitiesManager.updateAstAndRejigSketch( - updatedSketchEntryNodePath, - updatedSketchNodePaths, - updatedPlaneNodePath, + updatedPathToNode, _modifiedAst, sketchDetails.zAxis, sketchDetails.yAxis, @@ -1122,15 +1001,13 @@ export const ModelingMachineProvider = ({ return { selectionType: 'completeSelection', selection, - updatedSketchEntryNodePath, - updatedSketchNodePaths, - updatedPlaneNodePath, + updatedPathToNode, } } ), 'Get ABS X info': fromPromise( async ({ input: { selectionRanges, sketchDetails } }) => { - const { modifiedAst, pathToNodeMap, exprInsertIndex } = + const { modifiedAst, pathToNodeMap } = await applyConstraintAbsDistance({ constraint: 'xAbs', selectionRanges, @@ -1141,22 +1018,13 @@ export const ModelingMachineProvider = ({ const _modifiedAst = pResult.program if (!sketchDetails) return Promise.reject(new Error('No sketch details')) - - const { - updatedSketchEntryNodePath, - updatedSketchNodePaths, - updatedPlaneNodePath, - } = updateSketchDetailsNodePaths({ - sketchEntryNodePath: sketchDetails.sketchEntryNodePath, - sketchNodePaths: sketchDetails.sketchNodePaths, - planeNodePath: sketchDetails.planeNodePath, - exprInsertIndex, - }) + const updatedPathToNode = updatePathToNodeFromMap( + sketchDetails.sketchPathToNode, + pathToNodeMap + ) const updatedAst = await sceneEntitiesManager.updateAstAndRejigSketch( - updatedSketchEntryNodePath, - updatedSketchNodePaths, - updatedPlaneNodePath, + updatedPathToNode, _modifiedAst, sketchDetails.zAxis, sketchDetails.yAxis, @@ -1177,15 +1045,13 @@ export const ModelingMachineProvider = ({ return { selectionType: 'completeSelection', selection, - updatedSketchEntryNodePath, - updatedSketchNodePaths, - updatedPlaneNodePath, + updatedPathToNode, } } ), 'Get ABS Y info': fromPromise( async ({ input: { selectionRanges, sketchDetails } }) => { - const { modifiedAst, pathToNodeMap, exprInsertIndex } = + const { modifiedAst, pathToNodeMap } = await applyConstraintAbsDistance({ constraint: 'yAbs', selectionRanges, @@ -1196,22 +1062,13 @@ export const ModelingMachineProvider = ({ const _modifiedAst = pResult.program if (!sketchDetails) return Promise.reject(new Error('No sketch details')) - - const { - updatedSketchEntryNodePath, - updatedSketchNodePaths, - updatedPlaneNodePath, - } = updateSketchDetailsNodePaths({ - sketchEntryNodePath: sketchDetails.sketchEntryNodePath, - sketchNodePaths: sketchDetails.sketchNodePaths, - planeNodePath: sketchDetails.planeNodePath, - exprInsertIndex, - }) + const updatedPathToNode = updatePathToNodeFromMap( + sketchDetails.sketchPathToNode, + pathToNodeMap + ) const updatedAst = await sceneEntitiesManager.updateAstAndRejigSketch( - updatedSketchEntryNodePath, - updatedSketchNodePaths, - updatedPlaneNodePath, + updatedPathToNode, _modifiedAst, sketchDetails.zAxis, sketchDetails.yAxis, @@ -1232,9 +1089,7 @@ export const ModelingMachineProvider = ({ return { selectionType: 'completeSelection', selection, - updatedSketchEntryNodePath, - updatedSketchNodePaths, - updatedPlaneNodePath, + updatedPathToNode, } } ), @@ -1254,11 +1109,9 @@ export const ModelingMachineProvider = ({ let result: { modifiedAst: Node 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 @@ -1288,7 +1141,6 @@ 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, @@ -1319,22 +1171,10 @@ export const ModelingMachineProvider = ({ parsed = parsed as Node 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( - updatedSketchEntryNodePath, - updatedSketchNodePaths, - updatedPlaneNodePath, + result.pathToReplaced || [], parsed, sketchDetails.zAxis, sketchDetails.yAxis, @@ -1355,140 +1195,7 @@ export const ModelingMachineProvider = ({ return { selectionType: 'completeSelection', selection, - 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, + updatedPathToNode: result.pathToReplaced, } } ), diff --git a/src/components/Spinner.tsx b/src/components/Spinner.tsx index 63a5b356f1..dea0c20fd1 100644 --- a/src/components/Spinner.tsx +++ b/src/components/Spinner.tsx @@ -2,12 +2,7 @@ import { SVGProps } from 'react' export const Spinner = (props: SVGProps) => { return ( - + pathToNodeMap: PathToNodeMap - exprInsertIndex: number }> { const info = intersectInfo({ selectionRanges, @@ -175,7 +174,6 @@ export async function applyConstraintIntersect({ return { modifiedAst, pathToNodeMap, - exprInsertIndex: -1, } } // transform again but forcing certain values @@ -194,7 +192,6 @@ export async function applyConstraintIntersect({ const { modifiedAst: _modifiedAst, pathToNodeMap: _pathToNodeMap } = transform2 - let exprInsertIndex = -1 if (variableName) { const newBody = [..._modifiedAst.body] newBody.splice( @@ -207,11 +204,9 @@ 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, } } diff --git a/src/components/Toolbar/RemoveConstrainingValues.tsx b/src/components/Toolbar/RemoveConstrainingValues.tsx index 0100a6bafb..e4fe8eb15e 100644 --- a/src/components/Toolbar/RemoveConstrainingValues.tsx +++ b/src/components/Toolbar/RemoveConstrainingValues.tsx @@ -28,7 +28,7 @@ export function removeConstrainingValuesInfo({ | Error { const _nodes = selectionRanges.graphSelections.map(({ codeRef }) => { const tmp = getNodeFromPath(kclManager.ast, codeRef.pathToNode) - if (tmp instanceof Error) return tmp + if (err(tmp)) return tmp return tmp.node }) const _err1 = _nodes.find(err) diff --git a/src/components/Toolbar/SetAbsDistance.tsx b/src/components/Toolbar/SetAbsDistance.tsx index 03f8838bdd..f1b9652d65 100644 --- a/src/components/Toolbar/SetAbsDistance.tsx +++ b/src/components/Toolbar/SetAbsDistance.tsx @@ -93,7 +93,6 @@ export async function applyConstraintAbsDistance({ }): Promise<{ modifiedAst: Program pathToNodeMap: PathToNodeMap - exprInsertIndex: number }> { const info = absDistanceInfo({ selectionRanges, @@ -133,7 +132,6 @@ 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( @@ -146,9 +144,8 @@ 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, exprInsertIndex } + return { modifiedAst: _modifiedAst, pathToNodeMap } } export function applyConstraintAxisAlign({ diff --git a/src/components/Toolbar/SetAngleBetween.tsx b/src/components/Toolbar/SetAngleBetween.tsx index dc91d39c38..14a0fe72a2 100644 --- a/src/components/Toolbar/SetAngleBetween.tsx +++ b/src/components/Toolbar/SetAngleBetween.tsx @@ -86,7 +86,6 @@ export async function applyConstraintAngleBetween({ }): Promise<{ modifiedAst: Program pathToNodeMap: PathToNodeMap - exprInsertIndex: number }> { const info = angleBetweenInfo({ selectionRanges }) if (err(info)) return Promise.reject(info) @@ -123,7 +122,6 @@ export async function applyConstraintAngleBetween({ return { modifiedAst, pathToNodeMap, - exprInsertIndex: -1, } } @@ -143,7 +141,6 @@ export async function applyConstraintAngleBetween({ const { modifiedAst: _modifiedAst, pathToNodeMap: _pathToNodeMap } = transformed2 - let exprInsertIndex = -1 if (variableName) { const newBody = [..._modifiedAst.body] newBody.splice( @@ -156,11 +153,9 @@ 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, } } diff --git a/src/components/Toolbar/SetHorzVertDistance.tsx b/src/components/Toolbar/SetHorzVertDistance.tsx index b96d9fc388..172ebfa799 100644 --- a/src/components/Toolbar/SetHorzVertDistance.tsx +++ b/src/components/Toolbar/SetHorzVertDistance.tsx @@ -87,13 +87,15 @@ 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, @@ -131,12 +133,13 @@ export async function applyConstraintHorzVertDistance({ return { modifiedAst, pathToNodeMap, - exprInsertIndex: -1, } } else { if (!isExprBinaryPart(valueNode)) return Promise.reject('Invalid valueNode, is not a BinaryPart') - let finalValue = removeDoubleNegatives(valueNode, sign, variableName) + let finalValue = isAlign + ? createLiteral(0) + : removeDoubleNegatives(valueNode, sign, variableName) // transform again but forcing certain values const transformed = transformSecondarySketchLinesTagFirst({ ast: kclManager.ast, @@ -149,7 +152,6 @@ 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( @@ -162,12 +164,10 @@ 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, } } } diff --git a/src/components/Toolbar/setAngleLength.tsx b/src/components/Toolbar/setAngleLength.tsx index 6ea63e53b5..5453ef684c 100644 --- a/src/components/Toolbar/setAngleLength.tsx +++ b/src/components/Toolbar/setAngleLength.tsx @@ -70,14 +70,10 @@ 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 Promise.reject(angleLength) + if (err(angleLength)) return angleLength const { transforms } = angleLength let distanceExpression: Expr = length.valueAst @@ -98,7 +94,7 @@ export async function applyConstraintLength({ } if (!isExprBinaryPart(distanceExpression)) { - return Promise.reject('Invalid valueNode, is not a BinaryPart') + return new Error('Invalid valueNode, is not a BinaryPart') } const retval = transformAstSketchLines({ @@ -116,12 +112,6 @@ export async function applyConstraintLength({ return { modifiedAst: _modifiedAst, pathToNodeMap, - exprInsertIndex: - 'variableName' in length && - length.variableName && - length.insertIndex !== undefined - ? length.insertIndex - : -1, } } @@ -134,7 +124,6 @@ export async function applyConstraintAngleLength({ }): Promise<{ modifiedAst: Program pathToNodeMap: PathToNodeMap - exprInsertIndex: number }> { const angleLength = angleLengthInfo({ selectionRanges, angleOrLength }) if (err(angleLength)) return Promise.reject(angleLength) @@ -219,6 +208,5 @@ export async function applyConstraintAngleLength({ return { modifiedAst: _modifiedAst, pathToNodeMap, - exprInsertIndex: variableName ? newVariableInsertIndex : -1, } } diff --git a/src/lang/KclSingleton.ts b/src/lang/KclSingleton.ts index e1222ccae0..1688d4cbee 100644 --- a/src/lang/KclSingleton.ts +++ b/src/lang/KclSingleton.ts @@ -359,8 +359,10 @@ 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, @@ -402,7 +404,6 @@ export class KclManager { this._logs = logs this.addDiagnostics(kclErrorsToDiagnostics(errors)) - this._execState = execState this._programMemory = execState.memory if (!errors.length) { @@ -414,7 +415,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 && artifact.codeRef)) return + if (!('codeRef' in artifact)) return const _node1 = getNodeFromPath>( this.ast, artifact.codeRef.pathToNode, diff --git a/src/lang/langHelpers.ts b/src/lang/langHelpers.ts index d8f12f1add..64920e7ae7 100644 --- a/src/lang/langHelpers.ts +++ b/src/lang/langHelpers.ts @@ -1,6 +1,6 @@ import { Program, - executor, + _executor, ProgramMemory, kclLint, emptyExecState, @@ -64,9 +64,10 @@ export async function executeAst({ try { const execState = await (programMemoryOverride ? enginelessExecutor(ast, programMemoryOverride) - : executor(ast, engineCommandManager)) + : _executor(ast, engineCommandManager)) await engineCommandManager.waitForAllCommands() + return { logs: [], errors: [], diff --git a/src/lang/modifyAst.test.ts b/src/lang/modifyAst.test.ts index aa067e9dd3..261e8875c6 100644 --- a/src/lang/modifyAst.test.ts +++ b/src/lang/modifyAst.test.ts @@ -16,7 +16,6 @@ import { deleteSegmentFromPipeExpression, removeSingleConstraintInfo, deleteFromSelection, - splitPipedProfile, } from './modifyAst' import { enginelessExecutor } from '../lib/testHelpers' import { findUsesOfTagInPipe, getNodePathFromSourceRange } from './queryAst' @@ -919,63 +918,3 @@ 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) - }) -}) diff --git a/src/lang/modifyAst.ts b/src/lang/modifyAst.ts index e770073cf1..6a1ae63456 100644 --- a/src/lang/modifyAst.ts +++ b/src/lang/modifyAst.ts @@ -29,8 +29,6 @@ import { getNodePathFromSourceRange, isNodeSafeToReplace, traverse, - getBodyIndex, - isCallExprWithName, } from './queryAst' import { addTagForSketchOnFace, getConstraintInfo } from './std/sketch' import { @@ -48,7 +46,6 @@ 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, @@ -81,54 +78,41 @@ export function startSketchOnDefault( } } -export function insertNewStartProfileAt( +export function addStartProfileAt( node: Node, - sketchEntryNodePath: PathToNode, - sketchNodePaths: PathToNode[], - planeNodePath: PathToNode, - at: [number, number], - insertType: 'start' | 'end' = 'end' -): - | { - modifiedAst: Node - updatedSketchNodePaths: PathToNode[] - updatedEntryNodePath: PathToNode - } - | Error { - const varDec = getNodeFromPath( + pathToNode: PathToNode, + at: [number, number] +): { modifiedAst: Node; pathToNode: PathToNode } | Error { + const _node1 = getNodeFromPath( node, - planeNodePath, - 'VariableDeclarator' + pathToNode, + 'VariableDeclaration' ) - 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), + 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, ]) - ) - 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, - updatedSketchNodePaths, - updatedEntryNodePath, + pathToNode, } } @@ -269,7 +253,7 @@ export function mutateObjExpProp( export function extrudeSketch( node: Node, pathToNode: PathToNode, - artifact?: Artifact, + shouldPipe = false, distance: Expr = createLiteral(4) ): | { @@ -278,14 +262,10 @@ 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( @@ -294,6 +274,9 @@ export function extrudeSketch( 'PipeExpression' ) if (err(_node2)) return _node2 + const { node: pipeExpression } = _node2 + + const isInPipeExpression = pipeExpression.type === 'PipeExpression' const _node3 = getNodeFromPath( _node, @@ -301,23 +284,49 @@ export function extrudeSketch( 'VariableDeclarator' ) if (err(_node3)) return _node3 - const { node: variableDeclarator } = _node3 + const { node: variableDeclarator, shallowPath: pathToDecleration } = _node3 const extrudeCall = createCallExpressionStdLib('extrude', [ distance, - createIdentifier(variableDeclarator.id.name), + shouldPipe + ? createPipeSubstitution() + : 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 lastSketchNodePath = - orderedSketchNodePaths[orderedSketchNodePaths.length - 1] - - console.log('lastSketchNodePath', lastSketchNodePath, orderedSketchNodePaths) - const sketchIndexInBody = Number(lastSketchNodePath[1][0]) + const sketchIndexInPathToNode = + pathToDecleration.findIndex((a) => a[0] === 'body') + 1 + const sketchIndexInBody = pathToDecleration[ + sketchIndexInPathToNode + ][0] as number _node.body.splice(sketchIndexInBody + 1, 0, VariableDeclaration) const pathToExtrudeArg: PathToNode = [ @@ -1286,8 +1295,7 @@ export async function deleteFromSelection( const pipeBody = varDec.node.init.body if ( pipeBody[0].type === 'CallExpression' && - (pipeBody[0].callee.name === 'startSketchOn' || - pipeBody[0].callee.name === 'startProfileAt') + pipeBody[0].callee.name === 'startSketchOn' ) { // remove varDec const varDecIndex = varDec.shallowPath[1][0] as number @@ -1302,149 +1310,3 @@ 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( - _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, - } -} diff --git a/src/lang/modifyAst/addEdgeTreatment.test.ts b/src/lang/modifyAst/addEdgeTreatment.test.ts index 7f00e250f1..9305992297 100644 --- a/src/lang/modifyAst/addEdgeTreatment.test.ts +++ b/src/lang/modifyAst/addEdgeTreatment.test.ts @@ -275,7 +275,7 @@ const runModifyAstCloneWithEdgeTreatmentAndTag = async ( const selection: Selections = { graphSelections: segmentRanges.map((segmentRange) => { const maybeArtifact = [...artifactGraph].find(([, a]) => { - if (!('codeRef' in a && a.codeRef)) return false + if (!('codeRef' in a)) return false return isOverlap(a.codeRef.range, segmentRange) }) return { diff --git a/src/lang/modifyAst/addRevolve.ts b/src/lang/modifyAst/addRevolve.ts index 769135f2ca..d9af1917a0 100644 --- a/src/lang/modifyAst/addRevolve.ts +++ b/src/lang/modifyAst/addRevolve.ts @@ -5,6 +5,7 @@ import { PathToNode, Expr, CallExpression, + PipeExpression, VariableDeclarator, } from 'lang/wasm' import { Selections } from 'lib/selections' @@ -14,6 +15,7 @@ import { createCallExpressionStdLib, createObjectExpression, createIdentifier, + createPipeExpression, findUniqueName, createVariableDeclaration, } from 'lang/modifyAst' @@ -22,13 +24,12 @@ import { mutateAstWithTagForSketchSegment, getEdgeTagCall, } from 'lang/modifyAst/addEdgeTreatment' -import { Artifact, getPathsFromArtifact } from 'lang/std/artifactGraph' export function revolveSketch( ast: Node, pathToSketchNode: PathToNode, + shouldPipe = false, angle: Expr = createLiteral(4), - axis: Selections, - artifact?: Artifact + axis: Selections ): | { modifiedAst: Node @@ -36,11 +37,6 @@ 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 @@ -71,13 +67,29 @@ 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( + clonedAst, + pathToSketchNode, + 'PipeExpression' + ) + if (err(sketchPipeExpressionNode)) return sketchPipeExpressionNode + const { node: sketchPipeExpression } = sketchPipeExpressionNode + const isInPipeExpression = sketchPipeExpression.type === 'PipeExpression' + const sketchVariableDeclaratorNode = getNodeFromPath( clonedAst, pathToSketchNode, 'VariableDeclarator' ) if (err(sketchVariableDeclaratorNode)) return sketchVariableDeclaratorNode - const { node: sketchVariableDeclarator } = sketchVariableDeclaratorNode + const { + node: sketchVariableDeclarator, + shallowPath: sketchPathToDecleration, + } = sketchVariableDeclaratorNode const axisSelection = axis?.graphSelections[0]?.artifact @@ -91,13 +103,37 @@ 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 lastSketchNodePath = - orderedSketchNodePaths[orderedSketchNodePaths.length - 1] - const sketchIndexInBody = Number(lastSketchNodePath[1][0]) + const sketchIndexInPathToNode = + sketchPathToDecleration.findIndex((a) => a[0] === 'body') + 1 + const sketchIndexInBody = sketchPathToDecleration[sketchIndexInPathToNode][0] if (typeof sketchIndexInBody !== 'number') return new Error('expected sketchIndexInBody to be a number') clonedAst.body.splice(sketchIndexInBody + 1, 0, VariableDeclaration) diff --git a/src/lang/queryAst.ts b/src/lang/queryAst.ts index ca09cce7c0..1509e843d8 100644 --- a/src/lang/queryAst.ts +++ b/src/lang/queryAst.ts @@ -33,7 +33,6 @@ 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,13 +596,7 @@ export function findAllPreviousVariables( type ReplacerFn = ( _ast: Node, varName: string -) => - | { - modifiedAst: Node - pathToReplaced: PathToNode - exprInsertIndex: number - } - | Error +) => { modifiedAst: Node; pathToReplaced: PathToNode } | Error export function isNodeSafeToReplacePath( ast: Program, @@ -655,7 +648,7 @@ export function isNodeSafeToReplacePath( if (err(_nodeToReplace)) return _nodeToReplace const nodeToReplace = _nodeToReplace.node as any nodeToReplace[last[0]] = identifier - return { modifiedAst: _ast, pathToReplaced, exprInsertIndex: index } + return { modifiedAst: _ast, pathToReplaced } } const hasPipeSub = isTypeInValue(finVal as Expr, 'PipeSubstitution') @@ -774,15 +767,8 @@ 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( - sg2, + sg, secondaryLine?.codeRef?.range ) if (err(_segment)) return _segment @@ -1115,57 +1101,3 @@ export function getObjExprProperty( if (index === -1) return null return { expr: node.properties[index].value, index } } - -export function isCursorInFunctionDefinition( - ast: Node, - selectionRanges: Selection -): boolean { - if (!selectionRanges?.codeRef?.pathToNode) return false - const node = getNodeFromPath( - 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, - pathToPipe: PathToNode -): boolean | Error { - const varDec = getNodeFromPath( - 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 -} diff --git a/src/lang/std/__snapshots__/artifactGraph.test.ts.snap b/src/lang/std/__snapshots__/artifactGraph.test.ts.snap index db2304b746..d31574a2d3 100644 --- a/src/lang/std/__snapshots__/artifactGraph.test.ts.snap +++ b/src/lang/std/__snapshots__/artifactGraph.test.ts.snap @@ -212,7 +212,6 @@ Map { "type": "wall", }, "UUID-10" => { - "codeRef": undefined, "edgeCutEdgeIds": [], "id": "UUID", "pathIds": [ diff --git a/src/lang/std/artifactGraph.test.ts b/src/lang/std/artifactGraph.test.ts index 1c75c1a6e5..2870fe42a3 100644 --- a/src/lang/std/artifactGraph.test.ts +++ b/src/lang/std/artifactGraph.test.ts @@ -22,7 +22,6 @@ 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. @@ -172,7 +171,7 @@ afterAll(() => { describe('testing createArtifactGraph', () => { describe('code with offset planes and a sketch:', () => { - let ast: Node + let ast: Program let theMap: ReturnType it('setup', () => { @@ -218,7 +217,7 @@ describe('testing createArtifactGraph', () => { }) }) describe('code with an extrusion, fillet and sketch of face:', () => { - let ast: Node + let ast: Program let theMap: ReturnType it('setup', () => { // putting this logic in here because describe blocks runs before beforeAll has finished @@ -313,7 +312,7 @@ describe('testing createArtifactGraph', () => { }) describe(`code with sketches but no extrusions or other 3D elements`, () => { - let ast: Node + let ast: Program let theMap: ReturnType it(`setup`, () => { // putting this logic in here because describe blocks runs before beforeAll has finished @@ -378,7 +377,7 @@ describe('testing createArtifactGraph', () => { describe('capture graph of sketchOnFaceOnFace...', () => { describe('code with an extrusion, fillet and sketch of face:', () => { - let ast: Node + let ast: Program let theMap: ReturnType it('setup', async () => { // putting this logic in here because describe blocks runs before beforeAll has finished @@ -400,9 +399,7 @@ describe('capture graph of sketchOnFaceOnFace...', () => { }) }) -function getCommands( - codeKey: CodeKey -): CacheShape[CodeKey] & { ast: Node } { +function getCommands(codeKey: CodeKey): CacheShape[CodeKey] & { ast: Program } { const ast = assertParse(codeKey) const file = fs.readFileSync(fullPath, 'utf-8') const parsed: CacheShape = JSON.parse(file) diff --git a/src/lang/std/artifactGraph.ts b/src/lang/std/artifactGraph.ts index 9ecab816d7..d5a9e50984 100644 --- a/src/lang/std/artifactGraph.ts +++ b/src/lang/std/artifactGraph.ts @@ -1,19 +1,7 @@ -import { - Expr, - PathToNode, - Program, - SourceRange, - VariableDeclaration, -} from 'lang/wasm' +import { PathToNode, Program, SourceRange } from 'lang/wasm' import { Models } from '@kittycad/lib' -import { - getNodeFromPath, - getNodePathFromSourceRange, - traverse, -} from 'lang/queryAst' +import { getNodePathFromSourceRange } 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 @@ -46,14 +34,14 @@ export interface PathArtifact extends BaseArtifact { codeRef: CodeRef } -interface Solid2DArtifact extends BaseArtifact { +interface solid2D extends BaseArtifact { type: 'solid2D' pathId: ArtifactId } export interface PathArtifactRich extends BaseArtifact { type: 'path' /** A path must always lie on a plane */ - plane: PlaneArtifact | WallArtifact | CapArtifact + plane: PlaneArtifact | WallArtifact /** A path must always contain 0 or more segments */ segments: Array /** A path may not result in a sweep artifact */ @@ -73,7 +61,7 @@ interface SegmentArtifactRich extends BaseArtifact { type: 'segment' path: PathArtifact surf: WallArtifact - edges: Array + edges: Array edgeCut?: EdgeCut codeRef: CodeRef } @@ -92,7 +80,7 @@ interface SweepArtifactRich extends BaseArtifact { subType: 'extrusion' | 'revolve' path: PathArtifact surfaces: Array - edges: Array + edges: Array codeRef: CodeRef } @@ -102,9 +90,6 @@ interface WallArtifact extends BaseArtifact { edgeCutEdgeIds: Array sweepId: ArtifactId pathIds: Array - // 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' @@ -112,12 +97,9 @@ interface CapArtifact extends BaseArtifact { edgeCutEdgeIds: Array sweepId: ArtifactId pathIds: Array - // 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 SweepEdgeArtifact extends BaseArtifact { +interface SweepEdge extends BaseArtifact { type: 'sweepEdge' segId: ArtifactId sweepId: ArtifactId @@ -147,10 +129,10 @@ export type Artifact = | SweepArtifact | WallArtifact | CapArtifact - | SweepEdgeArtifact + | SweepEdge | EdgeCut | EdgeCutEdge - | Solid2DArtifact + | solid2D export type ArtifactGraph = Map @@ -177,7 +159,7 @@ export function createArtifactGraph({ }: { orderedCommands: Array responseMap: ResponseMap - ast: Node + ast: Program }) { const myMap = new Map() @@ -256,7 +238,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: Node + ast: Program }): Array<{ id: ArtifactId artifact: Artifact @@ -292,13 +274,6 @@ 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, @@ -309,29 +284,6 @@ 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, }, }, ] @@ -376,18 +328,6 @@ 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') { @@ -793,7 +733,7 @@ export function getCapCodeRef( } export function getSolid2dCodeRef( - solid2D: Solid2DArtifact, + solid2D: solid2D, artifactGraph: ArtifactGraph ): CodeRef | Error { const path = getArtifactOfTypes( @@ -817,7 +757,7 @@ export function getWallCodeRef( } export function getSweepEdgeCodeRef( - edge: SweepEdgeArtifact, + edge: SweepEdge, artifactGraph: ArtifactGraph ): CodeRef | Error { const seg = getArtifactOfTypes( @@ -931,281 +871,3 @@ 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, - pathToNode: PathToNode -): CodeRef | Error { - const varDec = getNodeFromPath( - 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, - } -} diff --git a/src/lang/std/artifactMapGraphs/sketchOnFaceOnFaceEtc.png b/src/lang/std/artifactMapGraphs/sketchOnFaceOnFaceEtc.png index f245108720..1bec224d59 100644 Binary files a/src/lang/std/artifactMapGraphs/sketchOnFaceOnFaceEtc.png and b/src/lang/std/artifactMapGraphs/sketchOnFaceOnFaceEtc.png differ diff --git a/src/lang/std/engineConnection.ts b/src/lang/std/engineConnection.ts index a591317445..1f7418f10a 100644 --- a/src/lang/std/engineConnection.ts +++ b/src/lang/std/engineConnection.ts @@ -37,7 +37,6 @@ 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 @@ -2116,7 +2115,7 @@ export class EngineCommandManager extends EventTarget { Object.values(this.pendingCommands).map((a) => a.promise) ) } - updateArtifactGraph(ast: Node) { + updateArtifactGraph(ast: Program) { this.artifactGraph = createArtifactGraph({ orderedCommands: this.orderedCommands, responseMap: this.responseMap, @@ -2214,11 +2213,7 @@ export class EngineCommandManager extends EventTarget { commandTypeToTarget: string ): string | undefined { for (const [artifactId, artifact] of this.artifactGraph) { - if ( - 'codeRef' in artifact && - artifact.codeRef && - isOverlap(range, artifact.codeRef.range) - ) { + if ('codeRef' in artifact && isOverlap(range, artifact.codeRef.range)) { if (commandTypeToTarget === artifact.type) return artifactId } } diff --git a/src/lang/std/sketch.ts b/src/lang/std/sketch.ts index a4933a8aec..947db85c7e 100644 --- a/src/lang/std/sketch.ts +++ b/src/lang/std/sketch.ts @@ -297,20 +297,14 @@ export const lineTo: SketchLineHelper = { add: ({ node, pathToNode, segmentInput, replaceExistingCallback }) => { if (segmentInput.type !== 'straight-segment') return STRAIGHT_SEGMENT_ERR const to = segmentInput.to - const _node = structuredClone(node) + const _node = { ...node } const nodeMeta = getNodeFromPath( _node, pathToNode, 'PipeExpression' ) if (err(nodeMeta)) return nodeMeta - const varDec = getNodeFromPath( - _node, - pathToNode, - 'VariableDeclaration' - ) - if (err(varDec)) return varDec - const dec = varDec.node.declaration + const { node: pipe } = nodeMeta const newVals: [Expr, Expr] = [ createLiteral(roundOff(to[0], 2)), @@ -339,20 +333,14 @@ export const lineTo: SketchLineHelper = { ]) if (err(result)) return result const { callExp, valueUsedInTransform } = result - if (dec.init.type === 'PipeExpression') { - dec.init.body[callIndex] = callExp - } else { - dec.init = callExp - } + pipe.body[callIndex] = callExp return { modifiedAst: _node, pathToNode, valueUsedInTransform: valueUsedInTransform, } - } else if (dec.init.type === 'PipeExpression') { - dec.init.body = [...dec.init.body, newLine] } else { - dec.init = createPipeExpression([dec.init, newLine]) + pipe.body = [...pipe.body, newLine] } return { modifiedAst: _node, @@ -675,11 +663,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 = structuredClone(node) + const _node = { ...node } const getNode = getNodeFromPathCurry(_node, pathToNode) - const varDec = getNode('VariableDeclaration') - if (err(varDec)) return varDec - const dec = varDec.node.declaration + const _node1 = getNode('PipeExpression') + if (err(_node1)) return _node1 + const { node: pipe } = _node1 const newVal = createLiteral(roundOff(to[0] - from[0], 2)) @@ -694,11 +682,7 @@ export const xLine: SketchLineHelper = { ]) if (err(result)) return result const { callExp, valueUsedInTransform } = result - if (dec.init.type === 'PipeExpression') { - dec.init.body[callIndex] = callExp - } else { - dec.init = callExp - } + pipe.body[callIndex] = callExp return { modifiedAst: _node, pathToNode, @@ -710,11 +694,7 @@ export const xLine: SketchLineHelper = { newVal, createPipeSubstitution(), ]) - if (dec.init.type === 'PipeExpression') { - dec.init.body = [...dec.init.body, newLine] - } else { - dec.init = createPipeExpression([dec.init, newLine]) - } + pipe.body = [...pipe.body, newLine] return { modifiedAst: _node, pathToNode } }, updateArgs: ({ node, pathToNode, input }) => { @@ -751,11 +731,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 = structuredClone(node) + const _node = { ...node } const getNode = getNodeFromPathCurry(_node, pathToNode) - const varDec = getNode('VariableDeclaration') - if (err(varDec)) return varDec - const dec = varDec.node.declaration + const _node1 = getNode('PipeExpression') + if (err(_node1)) return _node1 + const { node: pipe } = _node1 const newVal = createLiteral(roundOff(to[1] - from[1], 2)) if (replaceExistingCallback) { const { index: callIndex } = splitPathAtPipeExpression(pathToNode) @@ -768,11 +748,7 @@ export const yLine: SketchLineHelper = { ]) if (err(result)) return result const { callExp, valueUsedInTransform } = result - if (dec.init.type === 'PipeExpression') { - dec.init.body[callIndex] = callExp - } else { - dec.init = callExp - } + pipe.body[callIndex] = callExp return { modifiedAst: _node, pathToNode, @@ -784,11 +760,7 @@ export const yLine: SketchLineHelper = { newVal, createPipeSubstitution(), ]) - if (dec.init.type === 'PipeExpression') { - dec.init.body = [...dec.init.body, newLine] - } else { - dec.init = createPipeExpression([dec.init, newLine]) - } + pipe.body = [...pipe.body, newLine] return { modifiedAst: _node, pathToNode } }, updateArgs: ({ node, pathToNode, input }) => { @@ -2173,6 +2145,8 @@ 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 @@ -2253,6 +2227,7 @@ 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, diff --git a/src/lang/util.ts b/src/lang/util.ts index 9bb2d47376..f7147fd041 100644 --- a/src/lang/util.ts +++ b/src/lang/util.ts @@ -9,47 +9,20 @@ import { import { ArtifactGraph, filterArtifacts } from 'lang/std/artifactGraph' import { isOverlap } from 'lib/utils' -/** - * 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 +export function updatePathToNodeFromMap( + oldPath: PathToNode, + pathToNodeMap: { [key: number]: PathToNode } ): PathToNode { - 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 - planeNodePath: PathToNode - exprInsertIndex: number -}) { - return { - updatedSketchEntryNodePath: updatePathToNodePostExprInjection( - sketchEntryNodePath, - exprInsertIndex - ), - updatedSketchNodePaths: sketchNodePaths.map((path) => - updatePathToNodePostExprInjection(path, exprInsertIndex) - ), - updatedPlaneNodePath: updatePathToNodePostExprInjection( - planeNodePath, - exprInsertIndex - ), - } + 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 } export function isCursorInSketchCommandRange( @@ -58,7 +31,7 @@ export function isCursorInSketchCommandRange( ): string | false { const overlappingEntries = filterArtifacts( { - types: ['segment', 'path', 'plane'], + types: ['segment', 'path'], predicate: (artifact) => { return selectionRanges.graphSelections.some( (selection) => diff --git a/src/lang/wasm.ts b/src/lang/wasm.ts index ab9ae4866a..c53482aaeb 100644 --- a/src/lang/wasm.ts +++ b/src/lang/wasm.ts @@ -499,6 +499,26 @@ 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, + engineCommandManager: EngineCommandManager, + programMemoryOverride: ProgramMemory | Error | null = null +): Promise => { + if (programMemoryOverride !== null && err(programMemoryOverride)) + return Promise.reject(programMemoryOverride) + try { let jsAppSettings = default_app_settings() if (!TEST) { diff --git a/src/lib/rectangleTool.ts b/src/lib/rectangleTool.ts index 6fb9ff0294..6b9fdc66f9 100644 --- a/src/lib/rectangleTool.ts +++ b/src/lib/rectangleTool.ts @@ -29,7 +29,7 @@ import { */ export const getRectangleCallExpressions = ( rectangleOrigin: [number, number], - tag: string + tags: [string, string, string] ) => [ createCallExpressionStdLib('angledLine', [ createArrayExpression([ @@ -37,28 +37,30 @@ export const getRectangleCallExpressions = ( createLiteral(0), // This will be the width of the rectangle ]), createPipeSubstitution(), - createTagDeclarator(tag), + createTagDeclarator(tags[0]), ]), createCallExpressionStdLib('angledLine', [ createArrayExpression([ createBinaryExpression([ - createCallExpressionStdLib('segAng', [createIdentifier(tag)]), + createCallExpressionStdLib('segAng', [createIdentifier(tags[0])]), '+', 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(tag)]), // same angle as the first line + createCallExpressionStdLib('segAng', [createIdentifier(tags[0])]), // same angle as the first line createUnaryExpression( - createCallExpressionStdLib('segLen', [createIdentifier(tag)]), + createCallExpressionStdLib('segLen', [createIdentifier(tags[0])]), '-' ), // negative height ]), createPipeSubstitution(), + createTagDeclarator(tags[2]), ]), createCallExpressionStdLib('lineTo', [ createArrayExpression([ @@ -83,12 +85,12 @@ export function updateRectangleSketch( y: number, tag: string ) { - ;((pipeExpression.body[1] as CallExpression) + ;((pipeExpression.body[2] as CallExpression) .arguments[0] as ArrayExpression) = createArrayExpression([ createLiteral(x >= 0 ? 0 : 180), createLiteral(Math.abs(x)), ]) - ;((pipeExpression.body[2] as CallExpression) + ;((pipeExpression.body[3] as CallExpression) .arguments[0] as ArrayExpression) = createArrayExpression([ createBinaryExpression([ createCallExpressionStdLib('segAng', [createIdentifier(tag)]), @@ -118,7 +120,7 @@ export function updateCenterRectangleSketch( let startY = originY - Math.abs(deltaY) // pipeExpression.body[1] is startProfileAt - let callExpression = pipeExpression.body[0] + let callExpression = pipeExpression.body[1] if (isCallExpression(callExpression)) { const arrayExpression = callExpression.arguments[0] if (isArrayExpression(arrayExpression)) { @@ -132,7 +134,7 @@ export function updateCenterRectangleSketch( const twoX = deltaX * 2 const twoY = deltaY * 2 - callExpression = pipeExpression.body[1] + callExpression = pipeExpression.body[2] if (isCallExpression(callExpression)) { const arrayExpression = callExpression.arguments[0] if (isArrayExpression(arrayExpression)) { @@ -146,7 +148,7 @@ export function updateCenterRectangleSketch( } } - callExpression = pipeExpression.body[2] + callExpression = pipeExpression.body[3] if (isCallExpression(callExpression)) { const arrayExpression = callExpression.arguments[0] if (isArrayExpression(arrayExpression)) { diff --git a/src/lib/selections.ts b/src/lib/selections.ts index 872b068af2..d74da02dd3 100644 --- a/src/lib/selections.ts +++ b/src/lib/selections.ts @@ -280,19 +280,18 @@ export function getEventForSegmentSelection( } if (!id || !group) return null const artifact = engineCommandManager.artifactGraph.get(id) - if (!artifact) return null - const node = getNodeFromPath(kclManager.ast, group.userData.pathToNode) - if (err(node)) return null + const codeRefs = getCodeRefsByArtifactId( + id, + engineCommandManager.artifactGraph + ) + if (!artifact || !codeRefs) return null return { type: 'Set selection', data: { selectionType: 'singleCodeCursor', selection: { artifact, - codeRef: { - pathToNode: group?.userData?.pathToNode, - range: [node.node.start, node.node.end, true], - }, + codeRef: codeRefs[0], }, }, } @@ -676,7 +675,8 @@ 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,6 +685,7 @@ export function getSelectionTypeDisplayText( .replace('solid2D', 'face') .replace('segment', 'face')}${count > 1 ? 's' : ''}` ) + .toArray() .join(', ') } @@ -694,7 +695,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 && @@ -717,7 +718,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 && artifact.codeRef)) return null + if (!('codeRef' in artifact)) return null return isOverlap(artifact.codeRef.range, selection.range) ? { artifact, @@ -960,6 +961,7 @@ export function updateSelections( JSON.stringify(pathToNode) ) { artifact = a + console.log('found artifact', a) break } } diff --git a/src/lib/testHelpers.ts b/src/lib/testHelpers.ts index 720bceec22..7453086ae3 100644 --- a/src/lib/testHelpers.ts +++ b/src/lib/testHelpers.ts @@ -1,16 +1,20 @@ import { Program, ProgramMemory, - executor, + _executor, SourceRange, ExecState, } from '../lang/wasm' -import { EngineCommandManager } from 'lang/std/engineConnection' +import { + EngineCommandManager, + EngineCommandManagerEvents, +} 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 } from 'lib/trap' +import { err, reportRejection } from 'lib/trap' +import { toSync } from './utils' import { Node } from 'wasm-lib/kcl/bindings/Node' type WebSocketResponse = Models['WebSocketResponse_type'] @@ -90,7 +94,36 @@ 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, + pmo: ProgramMemory = ProgramMemory.empty() +): Promise { + 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) + ) + }) +} diff --git a/src/lib/toolbar.ts b/src/lib/toolbar.ts index 8d3061a2a4..e5512cf74d 100644 --- a/src/lib/toolbar.ts +++ b/src/lib/toolbar.ts @@ -2,6 +2,8 @@ import { CustomIconName } from 'components/CustomIcon' import { DEV } from 'env' import { commandBarMachine } from 'machines/commandBarMachine' import { + canRectangleOrCircleTool, + isClosedSketch, isEditingExistingSketch, modelingMachine, pipeHasCircle, @@ -71,7 +73,7 @@ export const toolbarConfig: Record = { status: 'available', disabled: (state) => !state.matches('idle'), title: ({ sketchPathId }) => - sketchPathId ? 'Edit Sketch' : 'Start Sketch', + `${sketchPathId ? 'Edit' : 'Start'} Sketch`, showTitle: true, hotkey: 'S', description: 'Start drawing a 2D sketch', @@ -313,14 +315,22 @@ export const toolbarConfig: Record = { { id: 'line', onClick: ({ modelingState, modelingSend }) => { - modelingSend({ - type: 'change tool', - data: { - tool: !modelingState.matches({ Sketch: 'Line tool' }) - ? 'line' - : 'none', - }, - }) + 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', + }, + }) + } }, icon: 'line', status: 'available', @@ -331,7 +341,8 @@ export const toolbarConfig: Record = { }) || state.matches({ Sketch: { 'Circle tool': 'Awaiting Radius' }, - }), + }) || + isClosedSketch(state.context), title: 'Line', hotkey: (state) => state.matches({ Sketch: 'Line tool' }) ? ['Esc', 'L'] : 'L', @@ -411,7 +422,10 @@ export const toolbarConfig: Record = { icon: 'circle', status: 'available', title: 'Center circle', - disabled: (state) => state.matches('Sketch no face'), + disabled: (state) => + state.matches('Sketch no face') || + (!canRectangleOrCircleTool(state.context) && + !state.matches({ Sketch: 'Circle tool' })), isActive: (state) => state.matches({ Sketch: 'Circle tool' }), hotkey: (state) => state.matches({ Sketch: 'Circle tool' }) ? ['Esc', 'C'] : 'C', @@ -450,7 +464,10 @@ export const toolbarConfig: Record = { }), icon: 'rectangle', status: 'available', - disabled: (state) => state.matches('Sketch no face'), + disabled: (state) => + state.matches('Sketch no face') || + (!canRectangleOrCircleTool(state.context) && + !state.matches({ Sketch: 'Rectangle tool' })), title: 'Corner rectangle', hotkey: (state) => state.matches({ Sketch: 'Rectangle tool' }) ? ['Esc', 'R'] : 'R', @@ -473,7 +490,10 @@ export const toolbarConfig: Record = { }), icon: 'arc', status: 'available', - disabled: (state) => state.matches('Sketch no face'), + disabled: (state) => + state.matches('Sketch no face') || + (!canRectangleOrCircleTool(state.context) && + !state.matches({ Sketch: 'Center Rectangle tool' })), title: 'Center rectangle', hotkey: (state) => state.matches({ Sketch: 'Center Rectangle tool' }) diff --git a/src/lib/trap.ts b/src/lib/trap.ts index e62b7fcc84..ab4551a42a 100644 --- a/src/lib/trap.ts +++ b/src/lib/trap.ts @@ -97,7 +97,3 @@ export function trap( }) return true } - -export function reject(errOrString: Error | string): Promise { - return Promise.reject(errOrString) -} diff --git a/src/machines/modelingMachine.ts b/src/machines/modelingMachine.ts index f8f14d78bc..3bb313b9d1 100644 --- a/src/machines/modelingMachine.ts +++ b/src/machines/modelingMachine.ts @@ -1,6 +1,7 @@ import { PathToNode, ProgramMemory, + VariableDeclaration, VariableDeclarator, parse, recast, @@ -81,7 +82,6 @@ import { Vector3 } from 'three' import { MachineManager } from 'components/MachineManagerProvider' import { addShell } from 'lang/modifyAst/addShell' import { KclCommandValue } from 'lib/commandTypes' -import { getPathsFromPlaneArtifact } from 'lang/std/artifactGraph' export const MODELING_PERSIST_KEY = 'MODELING_PERSIST_KEY' @@ -101,9 +101,7 @@ export type SetSelections = | { selectionType: 'completeSelection' selection: Selections - updatedSketchEntryNodePath?: PathToNode - updatedSketchNodePaths?: PathToNode[] - updatedPlaneNodePath?: PathToNode + updatedPathToNode?: PathToNode } | { selectionType: 'mirrorCodeMirrorSelections' @@ -128,20 +126,12 @@ export type MouseState = } export interface SketchDetails { - sketchEntryNodePath: PathToNode - sketchNodePaths: PathToNode[] - planeNodePath: PathToNode + sketchPathToNode: PathToNode zAxis: [number, number, number] yAxis: [number, number, number] origin: [number, number, number] } -export interface SketchDetailsUpdate { - updatedEntryNodePath: PathToNode - updatedSketchNodePaths: PathToNode[] - updatedPlaneNodePath?: PathToNode -} - export interface SegmentOverlay { windowCoords: Coords2d angle: number @@ -250,14 +240,7 @@ export type ModelingMachineEvent = | { type: 'Toggle gui mode' } | { type: 'Cancel' } | { type: 'CancelSketch' } - | { - type: 'Add start point' | 'Continue existing profile' - data: { - sketchNodePaths: PathToNode[] - sketchEntryNodePath: PathToNode - } - } - | { type: 'Close sketch' } + | { type: 'Add start point' } | { type: 'Make segment horizontal' } | { type: 'Make segment vertical' } | { type: 'Constrain horizontal distance' } @@ -305,14 +288,6 @@ export type ModelingMachineEvent = } | { type: 'xstate.done.actor.animate-to-sketch'; output: SketchDetails } | { type: `xstate.done.actor.do-constrain${string}`; output: SetSelections } - | { - type: - | 'xstate.done.actor.set-up-draft-circle' - | 'xstate.done.actor.set-up-draft-rectangle' - | 'xstate.done.actor.set-up-draft-center-rectangle' - | 'xstate.done.actor.split-sketch-pipe-if-needed' - output: SketchDetailsUpdate - } | { type: 'Set mouse state'; data: MouseState } | { type: 'Set context'; data: Partial } | { @@ -394,9 +369,7 @@ export const modelingMachineDefaultContext: ModelingMachineContext = { graphSelections: [], }, sketchDetails: { - sketchEntryNodePath: [], - planeNodePath: [], - sketchNodePaths: [], + sketchPathToNode: [], zAxis: [0, 0, 1], yAxis: [0, 1, 0], origin: [0, 0, 0], @@ -427,6 +400,23 @@ export const modelingMachine = setup({ 'has valid edge treatment selection': () => false, 'Has exportable geometry': () => false, 'has valid selection for deletion': () => false, + 'has made first point': ({ context }) => { + if (!context.sketchDetails?.sketchPathToNode) return false + const variableDeclaration = getNodeFromPath( + kclManager.ast, + context.sketchDetails.sketchPathToNode, + 'VariableDeclarator' + ) + if (err(variableDeclaration)) return false + if (variableDeclaration.node.type !== 'VariableDeclarator') return false + const pipeExpression = variableDeclaration.node.init + if (pipeExpression.type !== 'PipeExpression') return false + const hasStartSketchOn = pipeExpression.body.some( + (item) => + item.type === 'CallExpression' && item.callee.name === 'startSketchOn' + ) + return hasStartSketchOn && pipeExpression.body.length > 1 + }, 'is editing existing sketch': ({ context: { sketchDetails } }) => isEditingExistingSketch({ sketchDetails }), 'Can make selection horizontal': ({ context: { selectionRanges } }) => { @@ -572,12 +562,14 @@ export const modelingMachine = setup({ currentTool === 'tangentialArc' && isEditingExistingSketch({ sketchDetails }), - 'next is rectangle': ({ context: { currentTool } }) => - currentTool === 'rectangle', - 'next is center rectangle': ({ context: { currentTool } }) => - currentTool === 'center rectangle', - 'next is circle': ({ context: { currentTool } }) => - currentTool === 'circle', + 'next is rectangle': ({ context: { sketchDetails, currentTool } }) => + currentTool === 'rectangle' && + canRectangleOrCircleTool({ sketchDetails }), + 'next is center rectangle': ({ context: { sketchDetails, currentTool } }) => + currentTool === 'center rectangle' && + canRectangleOrCircleTool({ sketchDetails }), + 'next is circle': ({ context: { sketchDetails, currentTool } }) => + currentTool === 'circle' && canRectangleOrCircleTool({ sketchDetails }), 'next is line': ({ context }) => context.currentTool === 'line', 'next is none': ({ context }) => context.currentTool === 'none', }, @@ -596,11 +588,11 @@ export const modelingMachine = setup({ 'enter modeling mode': assign({ currentMode: 'modeling' }), 'set sketchMetadata from pathToNode': assign( ({ context: { sketchDetails } }) => { - if (!sketchDetails?.sketchEntryNodePath || !sketchDetails) return {} + if (!sketchDetails?.sketchPathToNode || !sketchDetails) return {} return { sketchDetails: { ...sketchDetails, - sketchEntryNodePath: sketchDetails.sketchEntryNodePath, + sketchPathToNode: sketchDetails.sketchPathToNode, }, } } @@ -642,19 +634,7 @@ export const modelingMachine = setup({ ;(async () => { if (!event.data) return const { selection, distance } = event.data - let ast = structuredClone(kclManager.ast) - const extrudeSketchRes = extrudeSketch( - ast, - selection.graphSelections[0]?.codeRef?.pathToNode, - selection.graphSelections[0]?.artifact, - 'variableName' in distance - ? distance.variableIdentifierAst - : distance.valueAst - ) - if (trap(extrudeSketchRes)) return - const { modifiedAst, pathToExtrudeArg } = extrudeSketchRes - ast = modifiedAst - let updatedPathToExtrudeArg = structuredClone(pathToExtrudeArg) + let ast = kclManager.ast if ( 'variableName' in distance && distance.variableName && @@ -667,13 +647,24 @@ export const modelingMachine = setup({ distance.variableDeclarationAst ) ast.body = newBody - // bump body index since we added a new variable above - updatedPathToExtrudeArg[1][0] = - Number(updatedPathToExtrudeArg[1][0]) + 1 } + const pathToNode = getNodePathFromSourceRange( + ast, + selection.graphSelections[0]?.codeRef.range + ) + const extrudeSketchRes = extrudeSketch( + ast, + pathToNode, + false, + 'variableName' in distance + ? distance.variableIdentifierAst + : distance.valueAst + ) + if (trap(extrudeSketchRes)) return + const { modifiedAst, pathToExtrudeArg } = extrudeSketchRes const updatedAst = await kclManager.updateAst(modifiedAst, true, { - focusPath: [updatedPathToExtrudeArg], + focusPath: [pathToExtrudeArg], zoomToFit: true, zoomOnRangeAndType: { range: selection.graphSelections[0]?.codeRef.range, @@ -713,11 +704,11 @@ export const modelingMachine = setup({ const revolveSketchRes = revolveSketch( ast, pathToNode, + false, 'variableName' in angle ? angle.variableIdentifierAst : angle.valueAst, - axis, - selection.graphSelections[0]?.artifact + axis ) if (trap(revolveSketchRes)) return const { modifiedAst, pathToRevolveArg } = revolveSketchRes @@ -807,12 +798,11 @@ export const modelingMachine = setup({ if (!sketchDetails) return ;(async () => { if (Object.keys(sceneEntitiesManager.activeSegments).length > 0) { - sceneEntitiesManager.tearDownSketch({ removeAxis: false }) + await sceneEntitiesManager.tearDownSketch({ removeAxis: false }) } sceneInfra.resetMouseListeners() await sceneEntitiesManager.setupSketch({ - sketchEntryNodePath: sketchDetails?.sketchEntryNodePath || [], - sketchNodePaths: sketchDetails.sketchNodePaths, + sketchPathToNode: sketchDetails?.sketchPathToNode || [], forward: sketchDetails.zAxis, up: sketchDetails.yAxis, position: sketchDetails.origin, @@ -820,33 +810,28 @@ export const modelingMachine = setup({ selectionRanges, }) sceneInfra.resetMouseListeners() - sceneEntitiesManager.setupSketchIdleCallbacks({ - sketchEntryNodePath: sketchDetails?.sketchEntryNodePath || [], + pathToNode: sketchDetails?.sketchPathToNode || [], forward: sketchDetails.zAxis, up: sketchDetails.yAxis, position: sketchDetails.origin, - sketchNodePaths: sketchDetails.sketchNodePaths, - planeNodePath: sketchDetails.planeNodePath, }) })().catch(reportRejection) }, 'tear down client sketch': () => { if (sceneEntitiesManager.activeSegments) { + // eslint-disable-next-line @typescript-eslint/no-floating-promises sceneEntitiesManager.tearDownSketch({ removeAxis: false }) } }, 'remove sketch grid': () => sceneEntitiesManager.removeSketchGrid(), - 'set up draft line': assign(({ context: { sketchDetails }, event }) => { - if (!sketchDetails) return {} - if (event.type !== 'Add start point') return {} + 'set up draft line': ({ context: { sketchDetails } }) => { + if (!sketchDetails) return // eslint-disable-next-line @typescript-eslint/no-floating-promises sceneEntitiesManager .setupDraftSegment( - event.data.sketchEntryNodePath || sketchDetails.sketchEntryNodePath, - event.data.sketchNodePaths || sketchDetails.sketchNodePaths, - sketchDetails.planeNodePath, + sketchDetails.sketchPathToNode, sketchDetails.zAxis, sketchDetails.yAxis, sketchDetails.origin, @@ -855,24 +840,14 @@ export const modelingMachine = setup({ .then(() => { return codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast) }) - return { - sketchDetails: { - ...sketchDetails, - sketchEntryNodePath: event.data.sketchEntryNodePath, - sketchNodePaths: event.data.sketchNodePaths, - }, - } - }), - 'set up draft arc': assign(({ context: { sketchDetails }, event }) => { - if (!sketchDetails) return {} - if (event.type !== 'Continue existing profile') return {} + }, + 'set up draft arc': ({ context: { sketchDetails } }) => { + if (!sketchDetails) return // eslint-disable-next-line @typescript-eslint/no-floating-promises sceneEntitiesManager .setupDraftSegment( - event.data.sketchEntryNodePath || sketchDetails.sketchEntryNodePath, - event.data.sketchNodePaths || sketchDetails.sketchNodePaths, - sketchDetails.planeNodePath, + sketchDetails.sketchPathToNode, sketchDetails.zAxis, sketchDetails.yAxis, sketchDetails.origin, @@ -881,32 +856,12 @@ export const modelingMachine = setup({ .then(() => { return codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast) }) - return { - sketchDetails: { - ...sketchDetails, - sketchEntryNodePath: event.data.sketchEntryNodePath, - sketchNodePaths: event.data.sketchNodePaths, - }, - } - }), + }, 'listen for rectangle origin': ({ context: { sketchDetails } }) => { if (!sketchDetails) return - const quaternion = quaternionFromUpNForward( - new Vector3(...sketchDetails.yAxis), - new Vector3(...sketchDetails.zAxis) - ) - - // Position the click raycast plane - if (sceneEntitiesManager.intersectionPlane) { - sceneEntitiesManager.intersectionPlane.setRotationFromQuaternion( - quaternion - ) - sceneEntitiesManager.intersectionPlane.position.copy( - new Vector3(...(sketchDetails?.origin || [0, 0, 0])) - ) - } - sceneInfra.setCallbacks({ - onClick: (args) => { + sceneEntitiesManager.setupNoPointsListener({ + sketchDetails, + afterClick: (args) => { const twoD = args.intersectionPoint?.twoD if (twoD) { sceneInfra.modelingSend({ @@ -922,22 +877,10 @@ export const modelingMachine = setup({ 'listen for center rectangle origin': ({ context: { sketchDetails } }) => { if (!sketchDetails) return - const quaternion = quaternionFromUpNForward( - new Vector3(...sketchDetails.yAxis), - new Vector3(...sketchDetails.zAxis) - ) - - // Position the click raycast plane - if (sceneEntitiesManager.intersectionPlane) { - sceneEntitiesManager.intersectionPlane.setRotationFromQuaternion( - quaternion - ) - sceneEntitiesManager.intersectionPlane.position.copy( - new Vector3(...(sketchDetails?.origin || [0, 0, 0])) - ) - } - sceneInfra.setCallbacks({ - onClick: (args) => { + // setupNoPointsListener has the code for startProfileAt onClick + sceneEntitiesManager.setupNoPointsListener({ + sketchDetails, + afterClick: (args) => { const twoD = args.intersectionPoint?.twoD if (twoD) { sceneInfra.modelingSend({ @@ -972,7 +915,7 @@ export const modelingMachine = setup({ if (!args) return if (args.mouseEvent.which !== 1) return const { intersectionPoint } = args - if (!intersectionPoint?.twoD || !sketchDetails?.sketchEntryNodePath) + if (!intersectionPoint?.twoD || !sketchDetails?.sketchPathToNode) return const twoD = args.intersectionPoint?.twoD if (twoD) { @@ -986,68 +929,81 @@ export const modelingMachine = setup({ }, }) }, - 'update sketchDetails': assign(({ event, context }) => { - if ( - event.type !== 'xstate.done.actor.set-up-draft-circle' && - event.type !== 'xstate.done.actor.set-up-draft-rectangle' && - event.type !== 'xstate.done.actor.set-up-draft-center-rectangle' && - event.type !== 'xstate.done.actor.split-sketch-pipe-if-needed' - ) - return {} - if (!context.sketchDetails) return {} - return { - sketchDetails: { - ...context.sketchDetails, - planeNodePath: - event.output.updatedPlaneNodePath || - context.sketchDetails?.planeNodePath || - [], - sketchEntryNodePath: event.output.updatedEntryNodePath, - sketchNodePaths: event.output.updatedSketchNodePaths, - }, - } - }), - 're-eval nodePaths': assign(({ context: { sketchDetails } }) => { - if (!sketchDetails) return {} - const planeArtifact = [ - ...engineCommandManager.artifactGraph.values(), - ].find( - (artifact) => - artifact.type === 'plane' && - JSON.stringify(artifact.codeRef.pathToNode) === - JSON.stringify(sketchDetails.planeNodePath) + 'set up draft rectangle': ({ context: { sketchDetails }, event }) => { + if (event.type !== 'Add rectangle origin') return + if (!sketchDetails || !event.data) return + + // eslint-disable-next-line @typescript-eslint/no-floating-promises + sceneEntitiesManager + .setupDraftRectangle( + sketchDetails.sketchPathToNode, + sketchDetails.zAxis, + sketchDetails.yAxis, + sketchDetails.origin, + event.data + ) + .then(() => { + return codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast) + }) + }, + 'set up draft center rectangle': ({ + context: { sketchDetails }, + event, + }) => { + if (event.type !== 'Add center rectangle origin') return + if (!sketchDetails || !event.data) return + // eslint-disable-next-line @typescript-eslint/no-floating-promises + sceneEntitiesManager.setupDraftCenterRectangle( + sketchDetails.sketchPathToNode, + sketchDetails.zAxis, + sketchDetails.yAxis, + sketchDetails.origin, + event.data ) - if (planeArtifact?.type !== 'plane') return {} - const newPaths = getPathsFromPlaneArtifact(planeArtifact) - return { - sketchDetails: { - ...sketchDetails, - sketchNodePaths: newPaths, - sketchEntryNodePath: newPaths[0], - }, - selectionRanges: { - otherSelections: [], - graphSelections: [], - }, - } - }), + }, + 'set up draft circle': ({ context: { sketchDetails }, event }) => { + if (event.type !== 'Add circle origin') return + if (!sketchDetails || !event.data) return + + // eslint-disable-next-line @typescript-eslint/no-floating-promises + sceneEntitiesManager + .setupDraftCircle( + sketchDetails.sketchPathToNode, + sketchDetails.zAxis, + sketchDetails.yAxis, + sketchDetails.origin, + event.data + ) + .then(() => { + return codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast) + }) + }, + 'set up draft line without teardown': ({ context: { sketchDetails } }) => { + if (!sketchDetails) return + + // eslint-disable-next-line @typescript-eslint/no-floating-promises + sceneEntitiesManager + .setupDraftSegment( + sketchDetails.sketchPathToNode, + sketchDetails.zAxis, + sketchDetails.yAxis, + sketchDetails.origin, + 'line', + false + ) + .then(() => { + return codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast) + }) + }, 'show default planes': () => { // eslint-disable-next-line @typescript-eslint/no-floating-promises kclManager.showPlanes() }, - 'setup noPoints onClick listener': ({ - context: { sketchDetails, currentTool }, - }) => { + 'setup noPoints onClick listener': ({ context: { sketchDetails } }) => { if (!sketchDetails) return sceneEntitiesManager.setupNoPointsListener({ sketchDetails, - currentTool, - afterClick: (_, data) => - sceneInfra.modelingSend( - currentTool === 'tangentialArc' - ? { type: 'Continue existing profile', data } - : { type: 'Add start point', data } - ), + afterClick: () => sceneInfra.modelingSend({ type: 'Add start point' }), }) }, 'add axis n grid': ({ context: { sketchDetails } }) => { @@ -1056,7 +1012,7 @@ export const modelingMachine = setup({ // eslint-disable-next-line @typescript-eslint/no-floating-promises sceneEntitiesManager.createSketchAxis( - sketchDetails.sketchEntryNodePath || [], + sketchDetails.sketchPathToNode || [], sketchDetails.zAxis, sketchDetails.yAxis, sketchDetails.origin @@ -1150,8 +1106,6 @@ export const modelingMachine = setup({ if (!sketchDetails) return let updatedAst = await sceneEntitiesManager.updateAstAndRejigSketch( pathToNodeMap[0], - sketchDetails.sketchNodePaths, - sketchDetails.planeNodePath, constraint.modifiedAst, sketchDetails.zAxis, sketchDetails.yAxis, @@ -1188,9 +1142,7 @@ export const modelingMachine = setup({ const { modifiedAst, pathToNodeMap } = constraint if (!sketchDetails) return const updatedAst = await sceneEntitiesManager.updateAstAndRejigSketch( - sketchDetails.sketchEntryNodePath, - sketchDetails.sketchNodePaths, - sketchDetails.planeNodePath, + sketchDetails.sketchPathToNode, modifiedAst, sketchDetails.zAxis, sketchDetails.yAxis, @@ -1225,9 +1177,7 @@ export const modelingMachine = setup({ const { modifiedAst, pathToNodeMap } = constraint if (!sketchDetails) return const updatedAst = await sceneEntitiesManager.updateAstAndRejigSketch( - sketchDetails.sketchEntryNodePath || [], - sketchDetails.sketchNodePaths, - sketchDetails.planeNodePath, + sketchDetails.sketchPathToNode || [], modifiedAst, sketchDetails.zAxis, sketchDetails.yAxis, @@ -1260,9 +1210,7 @@ export const modelingMachine = setup({ const { modifiedAst, pathToNodeMap } = constraint if (!sketchDetails) return const updatedAst = await sceneEntitiesManager.updateAstAndRejigSketch( - sketchDetails?.sketchEntryNodePath || [], - sketchDetails.sketchNodePaths, - sketchDetails.planeNodePath, + sketchDetails?.sketchPathToNode || [], modifiedAst, sketchDetails.zAxis, sketchDetails.yAxis, @@ -1296,9 +1244,7 @@ export const modelingMachine = setup({ const { modifiedAst, pathToNodeMap } = constraint if (!sketchDetails) return const updatedAst = await sceneEntitiesManager.updateAstAndRejigSketch( - sketchDetails?.sketchEntryNodePath || [], - sketchDetails.sketchNodePaths, - sketchDetails.planeNodePath, + sketchDetails?.sketchPathToNode || [], modifiedAst, sketchDetails.zAxis, sketchDetails.yAxis, @@ -1332,9 +1278,7 @@ export const modelingMachine = setup({ const { modifiedAst, pathToNodeMap } = constraint if (!sketchDetails) return const updatedAst = await sceneEntitiesManager.updateAstAndRejigSketch( - sketchDetails?.sketchEntryNodePath || [], - sketchDetails.sketchNodePaths, - sketchDetails.planeNodePath, + sketchDetails?.sketchPathToNode || [], modifiedAst, sketchDetails.zAxis, sketchDetails.yAxis, @@ -1368,9 +1312,7 @@ export const modelingMachine = setup({ const { modifiedAst, pathToNodeMap } = constraint if (!sketchDetails) return const updatedAst = await sceneEntitiesManager.updateAstAndRejigSketch( - sketchDetails?.sketchEntryNodePath || [], - sketchDetails.sketchNodePaths, - sketchDetails.planeNodePath, + sketchDetails?.sketchPathToNode || [], modifiedAst, sketchDetails.zAxis, sketchDetails.yAxis, @@ -1411,9 +1353,7 @@ export const modelingMachine = setup({ if (err(recastAst) || !resultIsOk(recastAst)) return const updatedAst = await sceneEntitiesManager.updateAstAndRejigSketch( - sketchDetails?.sketchEntryNodePath || [], - sketchDetails.sketchNodePaths, - sketchDetails.planeNodePath, + sketchDetails?.sketchPathToNode || [], recastAst.program, sketchDetails.zAxis, sketchDetails.yAxis, @@ -1447,9 +1387,7 @@ export const modelingMachine = setup({ const { modifiedAst, pathToNodeMap } = constraint if (!sketchDetails) return const updatedAst = await sceneEntitiesManager.updateAstAndRejigSketch( - sketchDetails?.sketchEntryNodePath || [], - sketchDetails.sketchNodePaths, - sketchDetails.planeNodePath, + sketchDetails?.sketchPathToNode || [], modifiedAst, sketchDetails.zAxis, sketchDetails.yAxis, @@ -1511,12 +1449,24 @@ export const modelingMachine = setup({ ), 'animate-to-face': fromPromise( async (_: { input?: ExtrudeFacePlane | DefaultPlane | OffsetPlane }) => { - return {} as ModelingMachineContext['sketchDetails'] + return {} as + | undefined + | { + sketchPathToNode: PathToNode + zAxis: [number, number, number] + yAxis: [number, number, number] + origin: [number, number, number] + } } ), 'animate-to-sketch': fromPromise( async (_: { input: Pick }) => { - return {} as ModelingMachineContext['sketchDetails'] + return {} as { + sketchPathToNode: PathToNode + zAxis: [number, number, number] + yAxis: [number, number, number] + origin: [number, number, number] + } } ), 'Get horizontal info': fromPromise( @@ -1713,49 +1663,10 @@ export const modelingMachine = setup({ } } ), - 'set-up-draft-circle': fromPromise( - async (_: { - input: Pick & { - data: [x: number, y: number] - } - }) => { - return {} as SketchDetailsUpdate - } - ), - 'set-up-draft-rectangle': fromPromise( - async (_: { - input: Pick & { - data: [x: number, y: number] - } - }) => { - return {} as SketchDetailsUpdate - } - ), - 'set-up-draft-center-rectangle': fromPromise( - async (_: { - input: Pick & { - data: [x: number, y: number] - } - }) => { - return {} as SketchDetailsUpdate - } - ), - 'setup-client-side-sketch-segments': fromPromise( - async (_: { - input: Pick - }) => { - return undefined - } - ), - 'split-sketch-pipe-if-needed': fromPromise( - async (_: { input: Pick }) => { - return {} as SketchDetailsUpdate - } - ), }, // end services }).createMachine({ - /** @xstate-layout N4IgpgJg5mDOIC5QFkD2EwBsCWA7KAxAMICGuAxlgNoAMAuoqAA6qzYAu2qujIAHogC0ANhoBWAHQAOAMwB2KQEY5AFgCcGqWqkAaEAE9Ew0RLEqa64TIBMKmTUXCAvk71oMOfAQDKYdgAJYLDByTm5aBiQQFjYwniiBBEEpYSkJOUUaOWsxeylrWzk9QwQClQkrVOsZNWExFItnVxB3LDxCXwCAW1QAVyDA9hJ2MAjeGI4ueNBE5KkaCWspZfMM+XE1YsQZMQWxNQtxao0ZeRc3dDavTv9ybhG+djGoibjeWZSZCRUxJa1flTCNTWLYIYS2CT2RSKdRyGhZawWc4tS6eDp+fy+KBdMC4AIAeQAbmAAE6YEj6WDPZisSbcd5CYHCSFqOQ1NSKWRiMRA0HqSTKblSX48+pKZGtNHEXEjEm3Eg4kkkfzcQLBUJTanRWlvBKM1LpFSKXKOTKKDmbAyIMwSGgHRFyXL5OE0KQS1HtCTYCCYMAEACieNJgQA1n5yAALLWvKYMpI5RTfNQyYRm-LqHKg7JqdJSBR5hTCI05d0eT3e30BoNy2Bh9iRqiKSI02KxvXxiEKaxVc35uogq1g1OmbnQotiOTaDmlq5QL0+v3+x4k3oYaM6tszIQT6zpcHj+FqI2nUGCtI7UTaeYFLJiGdo+eVgBKYEJqEwxPXrfp7cEE8Tvz5PMKhSCoCgyKewppLsyb7DIoE2DI97lguBAADKoAAZk89DjBuP5bkkE7MoBppLIiSgQYO1i9ukJqTtYGQgTsyH4I+freBGWCYF+dLTPw25yMy0I7rk9TgkeoLVPB0gTkoVjzI43asXOFZ+gAYtgmC+jhzbat+-GzBOXxHssNDGGy8IFKeKjmOkORZDIijVPkSHNJKKGVkuLAkrpeEGXGf6qJCxpst2xz2AOJSOPUiyuqywI0DYGiKCp7EEMgJBhrxuqEUFkg-CBrK-KI3JUdFZjMsIqhWOZPzyKoaVqQQAAiwQjGqvoauEuEvPhhmCV8SjwkJVjQsBp5qLFKjWMmQmInas1uRcZZsc1AAqYCPII7CoIIRAAIItTlm4CURwUKNoig1LZMgzaed0LFowJCjytlGk1qH4phmFBAETDkrgoy9S2fGBROkhJeIqjwcm3YqFJ5m7ssj2usK5rXZ9voSBG3pgAACoDcAEAdvnYJhJChP4UBKkwEb+CwTC9OSIwQCdBFnSI4gVCBWRgRYzpRYgmRFpCqQKDCuSprYWNgBIsARqgADuhNkMTpOcBTVM0yQdP+GAXRMJwkDswNRHaHRqiMRk3ZwkLCDOZyFQWFNwK22yTQrbOEjeHWkbEGQlCYL74ZRiD+lg+2V6mLNNCzea3JiFm8jfCmC1w4x5lpSH9YRq17VgGq2IyqbcaS7aRZ5ox9qIuVQjVJISz7EltRyNkSxyNnft53cGD6xAHD+BAvQku0oah6X7bQoCe4WHdM31PDfJ2JCOyMeY1VaElKhd6HPvdwAkqhmVhkXOJ4v4xJk+QJA8eHMYc4kmTJs7tkdylsi6IOtQ5qmqjGo4JytRd6533qHI+lYT6FyCMXC+itR4AC97i30noRTIbdbRQhSAUBiCNBwpDkNIWqHJuwwk3iAyMYDc4QL9EQbgsB2BKjwP4eB2AkF4lvkPbADDA7Az0g-M2mQeTfAUgcKaI0JyTV+JCbIW94oijvO5D0bEc6UNURGGhxB6GMJIMwq+nAb6YC4TwigfD-KRzQVkRM8hbqMR5LYRQkFZrSCNBOWyWRyFKNWnOdRVDIyaLobgBhTDcD+AOgAIW8P4AAGqgs6mQ8yyTEaIQEtlgRSTENCRY-8gR1AUp7FE3i-ERmKQE7RISwmRP8AATTiU-V0hDjA2JTHka6Ulxy2hmkeZyNRQoUJKeospQSdHMLIFAX0dThbl3mGYO0QJTg7C-iUREVhTCOlbvBbIzkCkeRUd3UpqFAnBN0aE30+B2Bh34f1Mu10cwKAyPaMCYF7Y0UdLaFI8xHSOlsMArx3tfGDMOeUk5DNSRMFxAPcgzMSBygHiYygkyHZQwkIlLQOxlAOEdFJeQiYkqyLZGYBoO8-kPgBYfIFwyKmsPYUMbS+h-C32wFAXAiKRaN3mOZIspUxqngRBUa61QCWTiEv0g5lYjkjNCfo7AhjMD0sZcy1lrpEzClsqIMa3J7bjUTAcDIKNrrEq9qS-ZgLxXAuYbAXAut-C7RiaykSFRhQ1EPKcV2p59jQTsOIcEadhSitNbQ81oTLXWttbU++1yp4OthFoZYvxSGnjhENYwIkzD3M7iSz0ZLwEUuOcwsAABHXonCzlQAuUqqaKKWmiHyBYIRp4ljMlUEJLQqQgRKDUP68lZrKUgqYDC2+vo75XIClG5xtkO3JnerNd1SUUXr3EJk4UmKu05p7Xm0JJIDaoGJLcIN7AqQRtHZYzJmDzDyHZE5TJD1EmnDsJkcyd0NCruoahF8ggtohF6CMVlNjFhAhbbVeCIFQT7EIcKXYMJgTXUvC+-xqFe6F0gIPYeo98Dj1zvameshQquu0ElYQoGsiQm5HHeEUIcidszXsveAatG9uYUrDg9MrU4ggJfW+vQzF9WPfEuO5QEJ2DsPMOojgpKTibeYMCZhlDzF+UarNJru1+kjGMwuu13yIoKLFaeQJ40lUcYOIEOZbKOmqv2NkxpRUHSVrogI1LkFGLhUMUx-g8CYVQAQCA3A5Z4DfGGCQMB2CCAcxwzAgh3OoC0yLIhk4DgQYNZNISEgMU0WrrUeZ1nbOD1C7S4xLnKBudwB5ggpISSoBJBIAGwwPMki6IFvwIWKtsMcxF4rUWj0WLOjRYjYkNC2BSYCOuDtkzngnFObQTkPZZbs5fUkBjOHOd4UVkr3mgZelwP5uWQXBDSsMW1jz0WYQjmTByEC1R1B4OiqBBY5p4bKCsPUECM3B57cW9wgrhdIulZJOVyr1X2C1fqztt74XItHcboxR6scNBFEHMoC253+sODzA4F7AQIlROiStzza3fObdQAFnbJAABGsBBB8AOx1kdXXEg0XNClrksgUx5hTFdxACY0g-AUOZR9SJqM+P2TZ2bmOYk45+39qrLMgcNeC6T8nlPweddyt16EOYIOpLhBOCw7PSjPIqEJCwUPMngnR5UqJ1Txd4421t2Xgh5eCH0FTo7aQ25snWLZE07TqjSEAeYGid1qpm9F5b77ZWKtS5qxV4HjWHdO6VzTlXdO1ffAzJyeCUGlkc+yAsTZ4hORKAUIohTNHQHC8HmM30VufM28J9t2P+BfTO+V6dZPDP24ZCsI6dBcPlnQi+IULehYljiDN5Xr77WJcR4BzL4njewDN8T63jnjsUs5DhMCFMAtdcOi+F612ZlTs7OUYLveB0mAA3paWi5e6GN4i8zXvzdeJAHW8OtQQdw7+CGv5c8xSeV-KCyQ1DfJor7A74OKQifLmT9bOTF6FL-JC4X5yr+A-634brsBT7-bS7R4v5v4f5Brf64hlq-48a04AHQQ0SnZLBpyWh94WwJR5gcjmQF5wG7Kn5l7Zb-RgoQoyrQqwofbLbfbW5P5E6Nbgokjgq4CQp8GCBLamKL5-7L6lDmSSDyAaDQzyCyC0Ec4Z6OpxrgizSjZm7iGSHSHkj8HwoT4lbh5YFR51Z24mE8FQrmGyECHyEJ6KGPwc4qEyLqHGTgTaGlALJ6HCgGEnBugC7FISBoR4DqaoDvgECqb4BxGaYt5eEIAphvKIgiRtyNAiZZiIiLDGSAipDJh3TLTwHGp7wxFAw2rxGYASAHy4AcAkwQDsY8K+QMyoB4B+SkH-4IDFhfAEpTiMTjj2wAamD2CTgzTGBTisEn5RE1EpENFNEtGkCmLDqeGCJHgLCtwRRtwlEZLJpZBCQzSZF1DzFFK+JLF1HvgSC4DR63zECYCsDQLdyIpGjZBr7ZA1BAicigjLDq6fCaDaAzQpCiqrEYGIpKBpC6pLDPIEZWBZhaC5iQY8gHDmYREl7sGUKQkECNhL7pGqohS2DCjNrGj5AAkgQpZxxCrolIyirrRqZ4jYCcIwrkB1GJERhqa3GbF9FKGZGNwOChRwiAgSYNrVRFGAisjmDXTQyMnMmcBskkgcm7SNHNEYGBKcC4Bcb6x8AfZjxMDlaYRaTcagz9F2A0QuLGCgTjQ2CGbRRQYpZKQzRkKgmGqVGKZ7xMnJEsnKmqmoD3GPGYDPGvEYYNhpFmxGhZI7FCZ2k0QNqxSpjgiunSmgQelsFRG9BSGoCDAwrsDqL4i4AP7rYiFyyv7v7ZneaCAdEFndxFmsqUGmC3hwgJwIjDbOQLCylxynC2ALK-CiovihDj68lck8kaZ8nmkCnVRClmhCSVQ5BJzURQTOmFj3KpC5CDkhAubjLLEv6cFjxBCf7sZ3AkhAwkgECaTNEKz+BbrDnz4fF6ZAGaqCjdiEaDhOTgguI5CZycj1DKBbn3m7m8n7l2ZjzNZQB4CtHsZ3k7lV4QV4CPnzDOnGi87whQRSTdK2jOQchfLVTNqAVwV7lXncJcTsZDlEUln4625-S9BMAf44Ayg1l4w1ndw1lgCwIHqIq9LlCbK2Bxz7DXRLklBTG7gH5u6pDCYZrYlREUUjkTkSAkBtFjwQBKjYS3nbnj5UW14BZ-SCB0WyFqXBawXj6PkmA54zIzLOS97bBG7pDiLGgaDqAsSRG+JyXz4gVKUDzoaqUkDqUmXz6YGR6A44F6UGW+XYSCABW7kfFOTlACUzlQ4gQOkc5Hi7iZJNwpi7CTgVGZm+JEAyjBjuXAUTljnJG8ncUzlxQJJCQSa-BOLq5GjmBTj8ZWauX7IFXVj+DFVV4KXl7alQB7pnmkiXl4CkW3CFVyjRUTKRlxiAgol5jmCCU3hYrw5HiELu43i-AiR1CiqdWyjdWaUeV9UHnoYIXFkHRtETVdXTWFznWPnUmF6MGwwHC66OB1AVD2A7G1BLr1B7WTWHVAW9X1ESAkUKyQD+D7VFVHW7naVlnyx+AGXkCMV4jMUYCsWhzsWcWwDWDcUHB778YrLXjaCQRwgkYbl1Du7pn-VdU9V7leVDxGXXUHW3Vw0E66WNbhVGUf6TVRUw0zWElRn7jpAphTSZElHciJrCiLA3io5Giuh2A00HV02eVXURUBCUA3X81+g2HBUy5hX0Xq083Vh81A1mkRwWndgmb2jepryZjw6pBPTgjLr5Bpb84yW+LI1gBkCBAsx+iNmAG0m3KVCpigizGQgXZOUwjnpYmeml6UKBLSroa2qsYQ2EicZ+jCHs1ywkAMISohIAByCokAAAahnVpicZMQ6EVOCH+eJm8pkuyByAnDYntdwEnYNSncXexunZgFxkFTPjgbnewPnSckXWxmXX3ebQInGIiMCNIP1tkASktDvhyMJJ7lkBlpyCKu1WfpwWgZKhpT0Lup-ugbAGzbbtWafZKnzcfQvtfSEr0VOekQ6MyEoJBktKQlNDerikBFlc5D8JcQgXvbNg-SCrloOvoBfc-lfQQRA3StFhYNhaqvnmSUlACbFHHDCO7hsK7WbmA3ovNjKpA9AwFrA1-qDnKkdomGsLIEcEmvbFNJIKoLSXGoA85Pg0Giws1jSpAwyjgMyqQ3LOQxuk1ogo5nKvbgIyyrNe2HPeUHNCLHqs6O+dFD4eFEHQxIuZw3fnNtfHwwqsWVnZfXtAQ7gLtkQ7Kk7oYxXclnmG4pkqFCTfDo4AsI7Y6CBG3AcJyDoxuoEFakwHUTEkIxICIzfSGvRbtBTog18NDLNICNXJiu6hyKnJNiVKyOnr44fRE0E9UiE2EyEjWQEztHtPoIg2kGij8BjCkLJqeFUJ9bUPkNCIKssFkxUoWsWkYj-vk6YwQR07fIQeciQc-WbPI5CENjYKmPYGcQ2l8fNSjtHdJXHTiSUv1QfRUv2kqNpFgD0-gV-ps4OtQLI4RPTtYtQZUBvjsDZEeCOGBLkT8MmDCHtdyeVetPUW5r9Picc91v1tILeLktJDMgCZOLaHaE5OaGKfBM8zyW8++B8+fQSVsbPb858uiT6vBOIKCHNKC48xC9VFC7vaAkQC8zAP4LC0YuTAi9YILci-PaiwC6cEC9-JWi6mrpC7HXlR1SS4XOS-C-iTIDS3Iyi-87UIC5i-DuILE2C2y-i5ONC68+85S-iSoIKyc8K2ZqK4y+K46Vkqy3i2yHK4Swndy2S4q581QGIKqz83SyK+i0y46UCDi+C0eLK1Rh7R1dgCqcDQkUkaSxOZVVkQ4JinUAk5LdRJXVbfUBi-YDyLlQsflZ617SBf1eBaPJBRdVdeQIm-BWm4hd84kKkpIFNGyI0943HNikaHuD9ZRswUeHtdm3uSm+hk+EpdgP0KNdefTFm169PZGoRCUYmH8Sw+C-YhkjNDImng+vUKyPWz2yBWDWRZDQ2yE7RfRcjdgExWwOjbWJjTAufAeioHjcsCijdrXN2MsEWFJPyBUKILYNUMqmjkayUkQA26rd5YNerbcMu8Y8-gbYZX5cFt217Y+RCHnqMcKGUFe5W2OBFBe6cLO0mwpV5SpUzUB5WLrYPfYX+0bWh727xgW5RosDhmKICI3dioxBUEeJkqoPeimPK6S+S-LADBwANeGfTEwNgOCiu8x8Fju7nIIBx+ChFphIIEDJACbPmxzuiSIjDIbtgm3LypWyBG9K6YiIUPRzyyDbADx6x3x5GAzJxzrb9tPtgVhzxxjfx4JwvuTKJ2AOJ2zJJ8oQUClqyHmHdhluCFmKsilJkM64cBy-G1yzC1p4jYE3p-TLVqOT+xzewEjSjbxyxeF1jfu7ADIFpjKaYOmddEoIxJkLrjVC4jYKBMKI839U+xIMS8F3cau2x-4JF6VRh6Z-VquwxRu6jVuwvkl3uzKKl6yqVFWm+ee1NMYMJYgExDLVykV8BI4P0v4A8XV5TBxOqAEBgBTMzP9ETIioIAnCOFM7UKRzRDvsYP+gSonGltNpEWQNgF0MMGPLatVkDCE1dzdyMCU4IFrHh2QQMSkCis-Ajt0mLe0htbK7AU5HVGlM97d8nbmQ90Z5Lph-VpD691Ex942Y3EWG3PMPBPE0JKCMAZ9W-OaM6NshD80S93d7mVtAaehuF092T8MAvlE+Fx8T90wVNMNMaLMao0YF+YcBlQJSPkAw+EjxT3qdT4NbT413YYj-T8j3tMz45+vHuNdN8lrrYFi4ASkMCFR8-K2mlOfpfuBT9H9AzETCE1hL9H4KrEDAdAwu4Fps4naNmHdLILMfBIjKLPBAsnptBklPr0gfoEb5bxt2rAPU1xIBb39Nb2ALb+wPb4r84kJM3TsLqnqjvp2aYEaPt8xG4v74b+hi8dhCE4X+wLH-H1a3Tj8IsE5XdH8FoJyNz6UJyG45RJqhezvTJQb3KmPCX2H9LxICX2X+gFplXxk0sDscukJMNgUHdKYOaNATrqlJEV34HzT1xNpCu+v5gEPw5xXxzpWzYJleZHCNmORwsHijDJkmmtyHn932v9xH3yFVh1vzvyPzqutcqikKkB8tihdNyFNAansRNBmgDxDAPACiBsEkWv4LXLaDKLaB044gLPEkEP6okamVkJagFyKRqQoBeUSmrANdS1o7QiAgEgcFzDYMpsygJqrLBxh4xo+4AkZoFEvD4CaghAg4PUFPAp5GC57IEEsFuTUCFYysOgTgM5hrxcwTlJKHQziy8ocgRHVCrNBP5Vx+kwgxILkRRQIhQIOeFMEiUHCCAA89kBEKVH4rOU4MGiBcMoOFiIhmG8-UQOyA9g2UEAjoLsuJDU5FgxwZueBvlkELtZzBDsaqE7UXLwCGI9giFmkGMBW044GgVIAS3dYgNXsljd7JYRxw+DHAoETLo5VI6LNAizfZkHdFCjPxckZwcrms1FzY5IsyQp8rsDAh5c246+ewaSQ2r-wzMmuJfjEI4Ii4qkoebwfyXSKOAbm4iBwPHFgipBvcAELpM8lmgOIlmnLWIQEBHJlDuhgidtLaBNy2BG6baF5D4RhAANfg0bIsNZgD4oEiCN+MxuwHKF0sEw0pO6DsPT4vxn4DUL6lr2MLcEpCvBcwp4NczzCGBU8OoDmBojzB-g2RNTtihkh4p6h2CBpKKhuITkfB8EXIJ9Rg7bxte9g1MOfzyBQxTinwSEbERAqQkYR3jeEUpERHyCswyWDIJyAxRGh36QvL0qAihEg0HidWW+D4LsAfUFabsQEjyBSoIAZk6QNuMmVSRFRqR8dEpLiIWFxhsEFcF0Ffx2DMRk4hCFkenEP5whj8VxfZD6RgB+kjE7JOojCIaCwCbS0ZWQAmQlbDhkyDkYbslTjaqjvSipVklqJVJ1F1SHAPEa6H1GSV4yXI6eDmBg6pl20tkBUr6SVL2iAyQZRkZgBdFpA5k7o9PMaOiinA3GLpQ7n6IzKBc94VZXMrWULL8QZ6U8dQDqm1wWQYI-waQbyMeaUiHkQEQivJXqK6jjufFf3IJSubURvk5NdFiNB6RVjjqINJthLxCDcATyFWc8syJQH8xWQW+MwJIg-JjhxmccLQPij97lcVaJ1MCmdVzbZi+2Z0LpJIAAwpISuriKSFoVkgpo0kjsHkJ2JKog0F2ENOmsyMFSrxXe4EY-sNiAQ6okoOCZpu4hVHANQES4kGshx8pM1bqt4lOIy0lhW1sgq9F+PcLAzrUBy5XKGnKF-HvhdR3Ib4ITXMjE0kBMIVIZBhK7i0EQStaGmbWTanVBqp5IcWKPbCWkcweKPFISmo6QRpE3IZ5M3RAg+pCJiE7WiRJXGDVzqt46RBhOSiLQmG7qL4vCD-KLpJwQiDiYDSIrzsxq4NdjAhNknj5bxxoIjjuLAgphJoFHIROLFsCwgAK8EgGkhIaIM1P2mtFmtrTUnso4Q79aEJLETRLBU4icFnJK2iHLMoiXtH2rAD9rJCDUKKc7B41VQpBQMX5TJMVExQn93ank-Ku3SIbQ85u3dDjFPXKH7AZatcI8GKU3r10qoawBeIbhsBTDUxbQweGYyPo7pC4Jw+gRbSUKOAZ4U2V6kwQ7SN8YMOYL6paSBAgQtA1gNpuAx4YSMSgXwtBFyhCiTgXYCSe2KBGZCFBq2awNOH1MIb6M6U5QhqQjgmmuhGGNoa6AnE+Qww9hRQ-euVPgbIFDGq0nIQjiWpXpNUvKOEd1Kv4XtEoKY60aVI1pcNKG8qaRudLGk7ETQicYIdhLUFbJuwpGNeItODQBMgm0SNKb8L+GzIek10DsuNhvZWBjgBxAVODP8ahpcy1SGGTLT-KylqgiMupqkKLDDdMUcpaEJjP6ZdMjhEYb6Y1KunGgbp8OHIEMTuZpJBKEsTGQc22bhjKJI0icKYB+FXC5SNpGyPkDIHHh5asaK0d+ONZVcKWv0GEfCCLYXMUgOwycSUCabLDTgnI5Mk6gQ7et+Zw0s6DdF2Kns3x3-S9tRERkDdlA3eLIO-SNmNtSJKoNccOMKKTpUksceqtRAKCJhLS9gBwOOGBAuzuJkwZtq236CezCErIV0IhCz7sDqIl2FLKIGb7FgG44chSleKUkNtgJewWqmBA1k5UMkQs6jlBnEQ-xipL0hOq+yQ7KUAJAHL9j21vGVtN4VtTZLsFG5BFEk6MBaLkBqEzt4JJrclj4IGy7Fvk9QJNNVCzCVtKMxoCKGIjllVEiWo8rTjp0PLdwDO4KcebsFhJTzUcmRLFg4DoiJQrAYEfbhp1NbVdQutXerjWIFndZMk5QMwBvDsATgMgPc1NIsBwrpg3ODgIUSszm65kPuPg3QdIhG6eMZomU58S-A1k2Ajg5ofrEAsUqy9ResPHwYkh9S10WRrvaoO0gKhUF1AP1GqKT2u5Q9O6lPfUgwi3mhwfBILaYtyBtJFcjQjfNtDLQGGPQyRK8z0CvyD4m9MFT82YCkMdSphp2j2BMIjBtBhQJMSaWEb1OX4B8e+WEU4cIqEDWV7IuCtuNCCdkvI-yzpFJGTLXhutPJ-C+-tpHAVCUDcU6G8DP20zYp1JOwHnD+XvRsgXALgIAA */ + /** @xstate-layout N4IgpgJg5mDOIC5QFkD2EwBsCWA7KAxAMICGuAxlgNoAMAuoqAA6qzYAu2qujIAHogC0ANhoBWAHQAOAMwB2KQEY5AFgCcGqWqkAaEAE9Ew0RLEqa64TIBMKmTUXCAvk71oMOfAQDKYdgAJYLDByTm5aBiQQFjYwniiBBEEpYSkJOUUaOWsxeylrWzk9QwQClQkrVOsZNWExFItnVxB3LDxCXwCAW1QAVyDA9hJ2MAjeGI4ueNBE5KkaCWspZfMM+XE1YsQZMQWxNQtxao0ZeRc3dDavTv9ybhG+djGoibjeWZSZCRUxJa1flTCNTWLYIYS2CT2RSKdRyGhZawWc4tS6eDp+fy+KBdMC4AIAeQAbmAAE6YEj6WDPZisSbcd5CYHCSFqOQ1NSKWRiMRA0HqSTKblSX48+pKZGtNHEXEjEm3Eg4kkkfzcQLBUJTanRWlvKIlQQ86zSKwcmjCOS7FTLPSJGQpRQSTkHVkOLL7CWo9oSbAQTBgAgAUTxpMCAGs-OQABZa15TBlJHIO9QyYSZTm2YFiUHZNTpKQKfMKYQqRQ5D0eL0+v2B4Ny2Dh9hRqiKSI02JxhJCMp56xVRSs5Z1EEGIyp0zc6HFsRybQc8tXKDe33+gOPEm9DAxnUdmZCadGuTgqfwtQl06gwVpHaibTzApu+dopfVgBKYEJqEwxK37fpnaS04Or8+TzFaKgKDIF7CmkuxqDscFSHY1SPpWy4EAAYtgmB+k89DjNuf67gBbISKeyxmua9g0AUF4qOY6Q5FkMilnayHNJKqHVquLAkrhrbar+0z8HuqiQoo042OCNT2MOJSOPUiw0FoM6IjYGiKCh+DPv6yAkOGP50kJszTpIPxWqyvyiNykEjgg4mAhUqhWGaPzyKommLlW-oACLBCMap+hq4R4S8BFGSJXxKPCh5WNCoEXmoCkqNYcGHoiNDAnBHnaQQAAqYCPII7CoIIRAAILeQZupEQaokKNoig1HRMjJReLULFomb1DydEltlXkEPiABmQ1BAETDkrgowhW2hnxrVkg0PY052FokkqKCiLgtIsjmEpwr9o1-XLhIkY+mAAAKk1wAQZW8dgQ0kKE-hQEqTCRv4LBML05IjBAVU7sJSRWRUVpZOBFj5NkF4uZCqQKDCuSprYx1+hIsCRqgADuV1kDdd2cI9z2vSQ73+GAXRMJwkAA4RQMGto6QrdYcgZL2cKyYgpachUFiJcC7Nsk0FwVlp3gNlGxBkJQmDixG0YzQJc3-ol5TGCWCgZPUGSbLZpbVBUiJCzsBR1DI2Vy42kYEL5OFgGq2IyrT4V2T8EgufmLMHPeNn6tUkhLPsS21KzBT5hbEvW3cGDkxAHD+BAvQku0Yby878bQo1pHw8x4iZMWoKskayMaEtShWtYGnsZ6YuRxIltRgAkmhunhg7OJ4v4xL3eQJCYOn-6ZPs6Q1NOsj9rs060dy0gOIl4jjwcUgR-L9eR831at-bQSO53mPJwAXvcfcD0RmR2I6Zs7Ko+SAm1Zju+BR6pDCR4r1ba-yxv-pENwsDsEqPA-h97YCPniPuCdsD-2ltNfisY6aJEyAUUwpdzAQxqBaTaq1xyqByLBX4S135Rk-lbb+xA-4AJIEA7unBe6YEgdAigsD8KCQzktOQpgMr1AKIeHkvtEDVEcO7XsmRtAs0okQyMJCm5oV-rgf+gDcD+DKgAIW8P4AAGqfIGQ8vgaGSj8Hk5oMibQ0Bwn4+ZxBZCsPMOQkjpGRjIXIhRVClGqPUQATW0Yg8QXwZzLDguJaEOxQTMWYosFmT86gcjEEdauotFwNykUkpxFDFH+DIFAP03iuaZ1IuYbkwJGp1FZptMwuZDxWKWAiLI9iUmyLSa4-wfp8DsAVnAsKGch6kQKKcVkxiEKbWMA6FmjVkoaABCoOp68GnyMoUApgpJFm4DjuQH6JA5Rx0YZQHJCAzHux5BYew8g+kbVsoHDhBR0wsxvtyaZX9ZkuKASAsBQxsL6AyTgKAuBdmZHCSWX44ltD7SzHrIU6RWSh1ZjOOEdj4kLgcQ41Jcz0k0OwHQzAHy+7YG+b8iwHDZypgcLE-MNQLzGDSBlZQ1Q2SnLhSLBFSSkWPPmUo2AuBSb+GKpovFShpCIlyLURqGQFCbSUuUbIFoOR1HBMveFT4mX1OrM41lgQOVMC5agfwXjFbwJdpkPlVh7BWnkDkECm1+wLCsXUDK5p8zKHuaQll6SwAAEdegQJaVANpeLtpZCCUtVYNgzlyVDqRGwBRYlmn6cLFECTEVKp-o0hZGy+5+n7rqzpg8uGmFkC1Qc4FYIJQOI6aEp4Mp2ksFM+VXpFUzOVcmpRJIKaoGJLcRt7AqSZtYdm5Ql8OQpA5LUeQUESIksQmYXaCFHUyNfGAQQBUQi9BGL89q3w+acnElkCiMNELu1HrsFqjVuSxo4rXVeiaCDR3tpAeOidk74FTlbX50JyjB0sdRS0pYYYpgqDYY2dgRWnprokuul65E0M1V3DZ2ASAACNsnduVmfbmEThQzltfuUFJRg25g5KM9Q8wZLAfjXWh51YoyZPtsVT8uyCgQgzIhcChjZAXk5JIKwlcloImyFYexZUsZUICC84+9CtlDCYf4PAQ1UAEAgNwMA3pcAfnDBIGA7BBAifAZgQQ0nUB0aYu7JaMJ2FmB5Fg7aOxmP5H7Orfjgn45abeQwiTlApO4BkwQUkJJUAkgkBNYYMmSRdDU34TTvnQGid0x5-TSHqpAyNl8c0r7+y2NSCGgRBjTDJRFOYZi6X7NCa7qSWhEDxMwPc55+TU0lMqcU+pwQaK6HRZkwZ+YFRTxmEyNRZL2GBGSUhKzZQGQuQzjEIV+OTWytQNc-bPTXmSQ+b8wF9gQWQsNamzpvTbWHQplsFoJQjV+ybSWGrc0vxVB2iBNOCbAR3GaMq7J6rim8B1dCxp+DsBBB8Ba7FjpPaiKImFGJUlfwDjdVCfCNWyUj0msDfSuNjK64CaK-djRj2FtLf879Nb73BCfe+79trkhS6pmSr2HrfXSiJTSA0Me4lYk9Vu8otR2qMfPdq6gVTDWCf6CJ3FwGiQgflHw1aGVVpcihOhFa+E+Y+EtTJ8z+7HiMfed89jwLvn1thd5-z-7yGEtKUkPUOw5oDjpiWqErapgtakocKzLKNbz0fxR-HTJfp2cKc59znX+A-R65YQboXRvpBjzqP8cZuhzn7FzEhZK4IzQs3G070Dq9XcBHd3NmLmP1crdxzzv387tsC4QQIpSlzRDQQtFYOEVO8G5lTOU3IJ4Wr8aYBND5Xq2ntpRa49gcmveva54psq3hcqCDuL3vAggu-tMD-F4PshSJaFLGDWoSg6+StMEOAx3MJ5t47803E3qPqT6eXiHPy2cda4kKP8fZ-WUz+Pz6kvLthcVGnPtIjiVFBDOB44OiRGERWQZnRZEkZZVZdZTZGbCrebDnIfH3DTMAiA9FKAwQcrJhAPUKAHQ3cJRncQEUJQVMfhUoFIDhUlGoAAk4FmUApZXESA8kaA7ZLPTzNXK-TXYLPHZA+g1Axg9AmAzA4vfXBfMvPArdbkM1RwRqTaJQXMCg6VdQOCGglPRFAAGTwGo1QE-CvUjCo01Vo1f3jGqBZmzmMBSnakymEELj9XyCJTsEBBsDlQZQVTrnUKmn0MwAkEblwA4AIAM3EBZABE9lZDtGzANlUCUmLH2n7G0HsTcM0M-C8J8P72bGEMFzLyzhqFUjNwOxolslkD2FOHty4QtEIRUKZXiI8IkAADktULpUA8BO1boIAIBBgNlxoGi8Q6MSwHRshmJXJOtGhswzR3ZlBw8zAgRRBq1nDa1XCNCqjaj-B6jGjYApYmEM00jS87JUwOF6gORSw2Qlp5AqdLtSIGpOQUx-1alyi65vDfDdkFBFoDEWpbAzRlhf89Yk9s5EpYlpdet7E7iUiWx590iEB8gFg6JDwTZzQrQ2oWpFhPYMgxjzd7FcoqM8RYN6ENlyBNUdC9CaMNiQStiXiOFeF5gDgLR1gSCVJFJmJWYlIeEq4ZjndiFegVktVoFeIkl8RcAB8asECR8x9BA2T5NBBOT2BuSflDDB4UwgJCxK4LEY8MsEALQHQADJ0x5wJTx7E3xQhM8PC8T8AEjCTsCg8BEbMbduQxtKJh0YYrBHRVBgQQIUpOQEcz1U8P5dSJMsljTb8HNOBH0ghJ9Wi7gSQpoSQMI8AoEPpm09TC9dl+i302RhRzt+kPiShwcdpz5ThhQ+objV4vT9SCS-ShMU4IsoA8BmjWjYzvSPdyy8AEyWIHSUxIVjhlBswL5n4lhWRI8nDEcXCCyQhazfTMIfCMZIB-BCzC8-DpSz5kpejHBUg5cblOY9lWRvhSjUxUgjcfh7EiAZQQwpyfSDTKMjSPDtF9RGpIpjBahwRC16grCgZDEziCF8xzBqg6I9yDy5QjyPdiz08U5QzwzIyxzT9vz-AazM8EyoQWRK5RRQ4d0wUYR0gE8oSHBiwlgvzaxJyhyiytDPCALH16zeSyoWjbhwLILC8VRk4KypTNiXY1IA5-Z8sNAblaJjAUKjZnIADML8yP59zsLfyRyozxzWiBLZQcK4yfSZz6KjDiwHRZVy0IibAqdlAshZ5-VD1xESMkdV5yA-QyBAhfp-QX0l9HAw4YkgRmIo85IGdOEiMTU7BtAdKBz+LuA0VH1uUOUcRWjCQ+5eh-R4DlNh8JASB-4VVFFqiFRIAAA1fy5hU0kQ0oI5Coc0MoEsOod8q3McIEYUFqQVXIawPc9ykrFOLy6K3y+Ky-DXVbG-MK9gCK1xKKnyuKzAAKgzYeQNcnPBRvKndqI0NSI5K7dMZnB-dJZtHoNtMavvVYoKt7UU6a6fCa1tedRaro2cw3ZKUiXYcyn4FKEsVczkc0RYSiHWYNQjUaxtYBCLV5NNfQPkl7YK1TBaxtcLQ+UTTFAzOER0JGA4MuLQQ6lqcoLaWXbIfYVmS6qfJRTbTFB673RTF6qGxrUqjFfQL68g8yeYfFHYEJPWQAnpK0GESJCYyG8-a6967TTFT5HFXkuakKxG8-N6yLSmvnbFXFDa4PMcBEHIKSDkWEC8Ccb4QEQ0GPe8Iqvi4hdPHvMmmGrFL5WmwfJ6hGkqNa5GnuO6-HeWgzC+V0LQBw1MfsIoPWYsZkFIX4b2AcWoZPZkj0yW-06W1VdlTlblDROGgUiQBmx-J2pgIqEqPgAzIER0YEXqGEfIWwMVVmQ2QDN41kOJG2xFKWtatVZ2rVDxN2pWj2lW167232wQNGjmsvC0YRBoGEO0U8C1VIflecykmlHYUm1VN1D1ehWfdO+arOpGxuvuJ-VpOfRK0EkG74cuGzU4FMOiC8C0NIIcayHqIEcCeu9JJgVNbCLAVu+m9uxmxepUZek02aJKo2cxKwWJZifDKePWYFEeQJWwaJXsPc3Qs83KfCmSokt-QEI0ewtMA2tmNjHIRYOCfYWxSNNieOplIgO+mAfwB+7Q1I5+uS39eSNKseO8b+gOP+4FbIHIIB-s2Y1eUBvQyBzAPw6wWS-8JYb6zU34GoGzXWWyn+8w-+7mklW+vBx+qgGQYhoiPpXMScUQcEbqNkZB3++CABjB82CWqRXB++lhlQdhhLIjSEVyM0OEZ0FMNjfIJmNYHWMedyMRiQCR8B-BvwsQGRoXOR-K+wc0DKCtR8uSV4iJRR2KJQm+nRogbAEkfS40w08Bgk3ZZ8iZIHCIj85UwQTdY0SYu8i0B8vc1x9xqowiqAainFSs0ikM6JusmihsguhAPIY3WJS7XqLQLIMVRmV08EYELWCCKJtxv8-CksyYR9F8EgOOfoEC6M24VJhK3e0E+wOCRYLSxqHITBc5AVM4lKIcOEYJa2rBlk8R9pqo0c6MiclxqpkyzJnmh0DkGcVWEbcx8e9SrGrWJabSyRfwXALVImf0XwQKAIDAR6H6caa6XZYJxKccYg2oYWhUoZZkdmHYbkQFFmFMbKMgbALoYYMqrVALKaVe1TIFkFkYXO85nxlIUiURZQfRCeEg9mFCtkNmXIfOaYqZxcGF0Fzy8F66aqvPOqnw2F+dYqQQBFzJ0sAOYsVmIjfIU8Q8UJdcqwOiSuMRNmFyr0IlgM+J7lAqGbFOeseWKFxTIVmlkqSV59TJsXZFrhKKcSG86x0cI0Q4WJREDB+YSZ902-dvTFMskaMaT6Mlum1TVAc1vwXGKaMqf+dwR5qQ74CnGcVIAdaiYtBS0sYzIEaiQEAVrSMqE1-QM10aDECF-0Ngmq3HW1qN9gB1sAJ19gF1pVtIf2OERwpymcX1iof1lioNvjZEU5jAeAKId0mB-8QQWvd2OCSh9KcHGyvcPde1aNT4cEUsVGMAGtmqEpBtvpcEyx+oUEFfb4V+BwaEXIPIXt06c6FNytzprYkQDKIdptk8BeNjfsaQFSVIIEJYRqNQedjGbGJd-t+mE2PMfRMuewTZmGWhjfaiF0TWcW4ByOS9xIVYF8-x98oG0EQQWECoWQGcDWJYGwGdRxZcL93JDIUPZKDKYEeEB8i8aoZkYsZiE9RCaXPso1plKWpzabZgx7WD7Y39MxJR-NDMLBeExCdBwCEOFIZnTbFzWAmLMj+SBYDIGEjWZEo2koAoYEUwERM0WJa7RCJXVndHPTTj4ZP9bQGPCyT10xEiGnJEiiYWkN22qRKW5XUjvurYxch0I3XamoO1AT7YTIUkyJWw9QdD5nfU2Twz-VdWR0Mg5SvaVt0oQWlShQMTojJkglhO8No-Huh2xRdgOThSWoF9qcMCSXc5M3QR-DHMT4N0kDBO+27glZXgjZNjyTZzld1zn+nG44Q4HlmQhjSuBQRDhPURj91eSogkzj+1P9ad4g+w80UEVMWnY5ECHkZQHtnR5rmpwEzjoddrycMZBXSzlU+EilXsdfTrT8kb+Y4s054LPuMj02ZkQNDYW3eETVlUi+TQOKS4lma4xrj+UbxIxY5YvEZdpWPeq5aQShzWO0LWcdiwPMfr6oA1nkAE5IsjkCRSOCwEZMub-WJLRLQ+02XIVE9EzgCBbEzVMjoG0yEpTQaVTkHrkiejWUpQ4+7TxFEUjkoYLkyOHkzjixBtq5cENYV9UEMUbOStWLs6oL-DuuISjwnbiCX+oTiHwmswHr9dqVSVOwMzTnzLplHn-8-0iVkIbgEM3zcMnb8eAX-amEjfZUidIW2wFiLGstHU3Cqi+X0soi9JoSPVIwz4A5OoJBNsub04rs5SXsk3qS6pxI+Z0SyS4c9XmeIuDzhEY5QueiHLK83YXsSgrCiSuX-Cnb7QDhA1SyLaYJOEo0f4I4ksMpw1mXuucSw8034883up+JoC0kdHpaAUI3XsNP6QpCiVVC7ijCvD-PnB8C+PxIuJhJ2iqvnYR0WvhPA2kgqg3p9QFieEZQNv0jAvzv4vr3zwn3yMRZ+fz3vtlz+MY5Gv4UOvs0dPvWfn1QVmCuJPuO4Lpldxwy2AYyzj8yhtouTWQsCwNjEY1+n4Bwe8Hs4q5TUqklk5iqtBjaob9iuGcXhpCEAgBILGKUObpJEz5CgHcsXZQtdztpFYk6y1Kah2ie429B4yMb4M3jMB2AUsqlYUKZDgjr5X0Kweek0iI7vI5OpYfActGaivpAaNgb4CpSO4chTg0vWfmnntpJ1Za9At+gQOYFE1aI9EfLMmFhBEDqBzyG6h9Tlo00hBjAyxnnELAC0agjkZaI1GjTiRZB0NFGndWprfJlBCjP6iSlZjHdHAA-KlDfDdBlEUBunfgVdW9pQYNEcnUrp-mr6SQTYYqOoKHiNzkCIiM-XSi7mcFQ1k6GqblB4g8GSBr48IMrnDwtTfVdoXCfYKWFqD6DyY7qT1M-kjCmDm85g10lDzZB4YI0RGGcNShPY6NE6V1TemmiwAeC-EutPLDmXzAJRK6VtMiAUTWhMNJGn4HbpCVMKlMLCozVRigyEYMNMGXPHBrMxa6b8SG0KdgadR-y8YrBbzSEIiDoiWgeQBwEniAzmE1Me+xFdXj-VNzYtVhvYY7oiGKb5hSmoyMeA1wv4F9Dh3fBXvU0abYB+gpw0yCmAuG5MrhJ2NgduXebjNj0lTGJsWWX6LN2mPw5Yf8OpTGBNofw5slCF7ApB1AxzU5v4HOZkdBAGDVKpOlwSqQ4InLa8oVSOAxFMwgLKlsSxFaks8YIPDhFJG7am5ZAUkIZGQKyKKESkgIGkcCzpFQYxW-8CVp+wWFEQZwEKQxBSlzaOAeuSLe8CxFWDBJsoYbDvJGwtYxtL2xUJgGXgfhsgvYFjHMr8D0B+ghoUXJII4Enr2g9icPWJHoDgyoB2AxULoAIiQQjxDRObO0CaJAA0VIwFooDtaMbxaA7RGUPQFjB9BtIuYiIPQCvxxT+jEAsJemFaJA7Bjh0IodMnqIlT-oQ4RxXfi4BcBAA */ id: 'Modeling', context: ({ input }) => ({ @@ -1968,7 +1879,6 @@ export const modelingMachine = setup({ 'change tool': { target: 'Change Tool', - reenter: true, }, }, @@ -2097,23 +2007,30 @@ export const modelingMachine = setup({ states: { Init: { + always: [ + { + target: 'normal', + guard: 'has made first point', + actions: 'set up draft line', + }, + 'No Points', + ], + }, + + normal: {}, + + 'No Points': { entry: 'setup noPoints onClick listener', on: { 'Add start point': { target: 'normal', - actions: 'set up draft line', + actions: 'set up draft line without teardown', }, Cancel: '#Modeling.Sketch.undo startSketchOn', }, }, - - normal: { - on: { - 'Close sketch': 'Init', - }, - }, }, initial: 'Init', @@ -2121,7 +2038,6 @@ export const modelingMachine = setup({ on: { 'change tool': { target: 'Change Tool', - reenter: true, }, }, }, @@ -2137,33 +2053,13 @@ export const modelingMachine = setup({ }, 'Tangential arc to': { + entry: 'set up draft arc', + on: { 'change tool': { target: 'Change Tool', - reenter: true, }, }, - - states: { - Init: { - on: { - 'Continue existing profile': { - target: 'normal', - actions: 'set up draft arc', - }, - }, - - entry: 'setup noPoints onClick listener', - }, - - normal: { - on: { - 'Close sketch': 'Init', - }, - }, - }, - - initial: 'Init', }, 'undo startSketchOn': { @@ -2179,6 +2075,8 @@ export const modelingMachine = setup({ }, 'Rectangle tool': { + entry: ['listen for rectangle origin'], + states: { 'Awaiting second corner': { on: { @@ -2189,47 +2087,14 @@ export const modelingMachine = setup({ 'Awaiting origin': { on: { 'Add rectangle origin': { - target: 'adding draft rectangle', - reenter: true, + target: 'Awaiting second corner', + actions: 'set up draft rectangle', }, }, - - entry: 'listen for rectangle origin', }, 'Finished Rectangle': { - invoke: { - src: 'setup-client-side-sketch-segments', - id: 'setup-client-side-sketch-segments', - onDone: 'Awaiting origin', - input: ({ context: { sketchDetails, selectionRanges } }) => ({ - sketchDetails, - selectionRanges, - }), - }, - }, - - 'adding draft rectangle': { - invoke: { - src: 'set-up-draft-rectangle', - id: 'set-up-draft-rectangle', - onDone: { - target: 'Awaiting second corner', - actions: 'update sketchDetails', - }, - onError: 'Awaiting origin', - input: ({ context: { sketchDetails }, event }) => { - if (event.type !== 'Add rectangle origin') - return { - sketchDetails, - data: [0, 0], - } - return { - sketchDetails, - data: event.data, - } - }, - }, + always: '#Modeling.Sketch.SketchIdle', }, }, @@ -2238,12 +2103,13 @@ export const modelingMachine = setup({ on: { 'change tool': { target: 'Change Tool', - reenter: true, }, }, }, 'Center Rectangle tool': { + entry: ['listen for center rectangle origin'], + states: { 'Awaiting corner': { on: { @@ -2254,47 +2120,15 @@ export const modelingMachine = setup({ 'Awaiting origin': { on: { 'Add center rectangle origin': { - target: 'add draft center rectangle', - reenter: true, + target: 'Awaiting corner', + // TODO + actions: 'set up draft center rectangle', }, }, - - entry: 'listen for center rectangle origin', }, 'Finished Center Rectangle': { - invoke: { - src: 'setup-client-side-sketch-segments', - id: 'setup-client-side-sketch-segments2', - onDone: 'Awaiting origin', - input: ({ context: { sketchDetails, selectionRanges } }) => ({ - sketchDetails, - selectionRanges, - }), - }, - }, - - 'add draft center rectangle': { - invoke: { - src: 'set-up-draft-center-rectangle', - id: 'set-up-draft-center-rectangle', - onDone: { - target: 'Awaiting corner', - actions: 'update sketchDetails', - }, - onError: 'Awaiting origin', - input: ({ context: { sketchDetails }, event }) => { - if (event.type !== 'Add center rectangle origin') - return { - sketchDetails, - data: [0, 0], - } - return { - sketchDetails, - data: event.data, - } - }, - }, + always: '#Modeling.Sketch.SketchIdle', }, }, @@ -2303,14 +2137,12 @@ export const modelingMachine = setup({ on: { 'change tool': { target: 'Change Tool', - reenter: true, }, }, }, 'clean slate': { always: 'SketchIdle', - entry: 're-eval nodePaths', }, 'Converting to named value': { @@ -2480,7 +2312,7 @@ export const modelingMachine = setup({ }, }, - 'Change Tool ifs': { + 'Change Tool': { always: [ { target: 'SketchIdle', @@ -2507,26 +2339,22 @@ export const modelingMachine = setup({ guard: 'next is center rectangle', }, ], - }, + entry: ['assign tool in context', 'reset selections'], + }, 'Circle tool': { on: { - 'change tool': { - target: 'Change Tool', - reenter: true, - }, + 'change tool': 'Change Tool', }, states: { 'Awaiting origin': { on: { 'Add circle origin': { - target: 'adding draft circle', - reenter: true, + target: 'Awaiting Radius', + actions: 'set up draft circle', }, }, - - entry: 'listen for circle origin', }, 'Awaiting Radius': { @@ -2536,77 +2364,12 @@ export const modelingMachine = setup({ }, 'Finished Circle': { - invoke: { - src: 'setup-client-side-sketch-segments', - id: 'setup-client-side-sketch-segments4', - onDone: 'Awaiting origin', - input: ({ context: { sketchDetails, selectionRanges } }) => ({ - sketchDetails, - selectionRanges, - }), - }, - }, - - 'adding draft circle': { - invoke: { - src: 'set-up-draft-circle', - id: 'set-up-draft-circle', - onDone: { - target: 'Awaiting Radius', - actions: 'update sketchDetails', - }, - onError: 'Awaiting origin', - input: ({ context: { sketchDetails }, event }) => { - if (event.type !== 'Add circle origin') - return { - sketchDetails, - data: [0, 0], - } - return { - sketchDetails, - data: event.data, - } - }, - }, + always: '#Modeling.Sketch.SketchIdle', }, }, initial: 'Awaiting origin', - }, - - 'Change Tool': { - states: { - 'splitting sketch pipe': { - invoke: { - src: 'split-sketch-pipe-if-needed', - id: 'split-sketch-pipe-if-needed', - onDone: { - target: 'setup sketch for tool', - actions: 'update sketchDetails', - }, - onError: '#Modeling.Sketch.SketchIdle', - input: ({ context: { sketchDetails } }) => ({ - sketchDetails, - }), - }, - }, - - 'setup sketch for tool': { - invoke: { - src: 'setup-client-side-sketch-segments', - id: 'setup-client-side-sketch-segments3', - onDone: '#Modeling.Sketch.Change Tool ifs', - onError: '#Modeling.Sketch.SketchIdle', - input: ({ context: { sketchDetails, selectionRanges } }) => ({ - sketchDetails, - selectionRanges, - }), - }, - }, - }, - - initial: 'splitting sketch pipe', - entry: ['assign tool in context', 'reset selections'], + entry: 'listen for circle origin', }, }, @@ -2674,12 +2437,10 @@ export const modelingMachine = setup({ invoke: { src: 'animate-to-sketch', id: 'animate-to-sketch', - input: ({ context }) => ({ selectionRanges: context.selectionRanges, sketchDetails: context.sketchDetails, }), - onDone: { target: 'Sketch', actions: [ @@ -2688,8 +2449,6 @@ export const modelingMachine = setup({ 'enter sketching mode', ], }, - - onError: 'idle', }, }, @@ -2778,40 +2537,34 @@ export function isEditingExistingSketch({ }): boolean { // should check that the variable declaration is a pipeExpression // and that the pipeExpression contains a "startProfileAt" callExpression - if (!sketchDetails?.sketchEntryNodePath) return false + if (!sketchDetails?.sketchPathToNode) return false const variableDeclaration = getNodeFromPath( kclManager.ast, - sketchDetails.sketchEntryNodePath, + sketchDetails.sketchPathToNode, 'VariableDeclarator' ) - if (variableDeclaration instanceof Error) return false + if (err(variableDeclaration)) return false if (variableDeclaration.node.type !== 'VariableDeclarator') return false - const maybePipeExpression = variableDeclaration.node.init - if ( - maybePipeExpression.type === 'CallExpression' && - (maybePipeExpression.callee.name === 'startProfileAt' || - maybePipeExpression.callee.name === 'circle') - ) - return true - if (maybePipeExpression.type !== 'PipeExpression') return false - const hasStartProfileAt = maybePipeExpression.body.some( + const pipeExpression = variableDeclaration.node.init + if (pipeExpression.type !== 'PipeExpression') return false + const hasStartProfileAt = pipeExpression.body.some( (item) => item.type === 'CallExpression' && item.callee.name === 'startProfileAt' ) - const hasCircle = maybePipeExpression.body.some( + const hasCircle = pipeExpression.body.some( (item) => item.type === 'CallExpression' && item.callee.name === 'circle' ) - return (hasStartProfileAt && maybePipeExpression.body.length > 1) || hasCircle + return (hasStartProfileAt && pipeExpression.body.length > 2) || hasCircle } export function pipeHasCircle({ sketchDetails, }: { sketchDetails: SketchDetails | null }): boolean { - if (!sketchDetails?.sketchEntryNodePath) return false + if (!sketchDetails?.sketchPathToNode) return false const variableDeclaration = getNodeFromPath( kclManager.ast, - sketchDetails.sketchEntryNodePath, + sketchDetails.sketchPathToNode, 'VariableDeclarator' ) if (err(variableDeclaration)) return false @@ -2823,3 +2576,41 @@ export function pipeHasCircle({ ) return hasCircle } + +export function canRectangleOrCircleTool({ + sketchDetails, +}: { + sketchDetails: SketchDetails | null +}): boolean { + const node = getNodeFromPath( + kclManager.ast, + sketchDetails?.sketchPathToNode || [], + 'VariableDeclaration' + ) + // This should not be returning false, and it should be caught + // but we need to simulate old behavior to move on. + if (err(node)) return false + return node.node?.declaration.init.type !== 'PipeExpression' +} + +/** If the sketch contains `close` or `circle` stdlib functions it must be closed */ +export function isClosedSketch({ + sketchDetails, +}: { + sketchDetails: SketchDetails | null +}): boolean { + const node = getNodeFromPath( + kclManager.ast, + sketchDetails?.sketchPathToNode || [], + 'VariableDeclaration' + ) + // This should not be returning false, and it should be caught + // but we need to simulate old behavior to move on. + if (err(node)) return false + if (node.node?.declaration.init.type !== 'PipeExpression') return false + return node.node.declaration.init.body.some( + (node) => + node.type === 'CallExpression' && + (node.callee.name === 'close' || node.callee.name === 'circle') + ) +}