diff --git a/e2e/playwright/basic-sketch.spec.ts b/e2e/playwright/basic-sketch.spec.ts index 4e024cc3e..cd3fad58f 100644 --- a/e2e/playwright/basic-sketch.spec.ts +++ b/e2e/playwright/basic-sketch.spec.ts @@ -57,23 +57,26 @@ async function doBasicSketch(page: Page, openPanes: string[]) { const startXPx = 600 await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) if (openPanes.includes('code')) { - await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ') - |> startProfileAt(${commonPoints.startAt}, %)`) + await expect(u.codeLocator).toContainText( + `sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${commonPoints.startAt}, sketch001)` + ) } await page.waitForTimeout(500) await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10) await page.waitForTimeout(500) if (openPanes.includes('code')) { - await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ') - |> startProfileAt(${commonPoints.startAt}, %) + await expect(u.codeLocator) + .toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${commonPoints.startAt}, sketch001) |> xLine(${commonPoints.num1}, %)`) } await page.waitForTimeout(500) await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20) if (openPanes.includes('code')) { - await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ') - |> startProfileAt(${commonPoints.startAt}, %) + await expect(u.codeLocator) + .toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${ + commonPoints.startAt + }, sketch001) |> xLine(${commonPoints.num1}, %) |> yLine(${commonPoints.num1 + 0.01}, %)`) } else { @@ -82,8 +85,10 @@ async function doBasicSketch(page: Page, openPanes: string[]) { await page.waitForTimeout(200) await page.mouse.click(startXPx, 500 - PUR * 20) if (openPanes.includes('code')) { - await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ') - |> startProfileAt(${commonPoints.startAt}, %) + await expect(u.codeLocator) + .toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${ + commonPoints.startAt + }, sketch001) |> xLine(${commonPoints.num1}, %) |> yLine(${commonPoints.num1 + 0.01}, %) |> xLine(${commonPoints.num2 * -1}, %)`) @@ -140,8 +145,10 @@ async function doBasicSketch(page: Page, openPanes: string[]) { // Open the code pane. await u.openKclCodePanel() - await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ') - |> startProfileAt(${commonPoints.startAt}, %) + await expect(u.codeLocator) + .toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${ + commonPoints.startAt + }, sketch001) |> xLine(${commonPoints.num1}, %, $seg01) |> yLine(${commonPoints.num1 + 0.01}, %) |> xLine(-segLen(seg01), %)`) 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 fab014015..4eb77be19 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,8 +44,7 @@ test.describe('Can create sketches on all planes and their back sides', () => { }, } - const code = `sketch001 = startSketchOn('${plane}') - |> startProfileAt([0.9, -1.22], %)` + const code = `sketch001 = startSketchOn('${plane}')profile001 = startProfileAt([0.9, -1.22], sketch001)` await u.openDebugPanel() diff --git a/e2e/playwright/fixtures/sceneFixture.ts b/e2e/playwright/fixtures/sceneFixture.ts index fff1d36e3..59426cb48 100644 --- a/e2e/playwright/fixtures/sceneFixture.ts +++ b/e2e/playwright/fixtures/sceneFixture.ts @@ -11,6 +11,7 @@ import { type mouseParams = { pixelDiff?: number + shouldDbClick?: boolean } type mouseDragToParams = mouseParams & { fromPoint: { x: number; y: number } @@ -75,11 +76,16 @@ export class SceneFixture { if (clickParams?.pixelDiff) { return doAndWaitForImageDiff( this.page, - () => this.page.mouse.click(x, y), + () => + clickParams?.shouldDbClick + ? this.page.mouse.dblclick(x, y) + : this.page.mouse.click(x, y), clickParams.pixelDiff ) } - return this.page.mouse.click(x, y) + return clickParams?.shouldDbClick + ? this.page.mouse.dblclick(x, y) + : this.page.mouse.click(x, y) }, (moveParams?: mouseParams) => { if (moveParams?.pixelDiff) { @@ -210,7 +216,7 @@ export class SceneFixture { } expectPixelColor = async ( - colour: [number, number, number], + colour: [number, number, number] | [number, number, number][], coords: { x: number; y: number }, diff: number ) => { @@ -231,22 +237,36 @@ export class SceneFixture { } } +function isColourArray( + colour: [number, number, number] | [number, number, number][] +): colour is [number, number, number][] { + return Array.isArray(colour[0]) +} + export async function expectPixelColor( page: Page, - colour: [number, number, number], + colour: [number, number, number] | [number, number, number][], coords: { x: number; y: number }, diff: number ) { let finalValue = colour await expect - .poll(async () => { - const pixel = (await getPixelRGBs(page)(coords, 1))[0] - if (!pixel) return null - finalValue = pixel - return pixel.every( - (channel, index) => Math.abs(channel - colour[index]) < diff - ) - }) + .poll( + async () => { + const pixel = (await getPixelRGBs(page)(coords, 1))[0] + if (!pixel) return null + finalValue = pixel + if (!isColourArray(colour)) { + return pixel.every( + (channel, index) => Math.abs(channel - colour[index]) < diff + ) + } + return colour.some((c) => + c.every((channel, index) => Math.abs(pixel[index] - channel) < diff) + ) + }, + { timeout: 10_000 } + ) .toBeTruthy() .catch((cause) => { throw new Error( diff --git a/e2e/playwright/fixtures/toolbarFixture.ts b/e2e/playwright/fixtures/toolbarFixture.ts index 43cf577e1..484f26e92 100644 --- a/e2e/playwright/fixtures/toolbarFixture.ts +++ b/e2e/playwright/fixtures/toolbarFixture.ts @@ -11,7 +11,10 @@ export class ToolbarFixture { offsetPlaneButton!: Locator startSketchBtn!: Locator lineBtn!: Locator + tangentialArcBtn!: Locator + circleBtn!: Locator rectangleBtn!: Locator + lengthConstraintBtn!: Locator exitSketchBtn!: Locator editSketchBtn!: Locator fileTreeBtn!: Locator @@ -33,7 +36,10 @@ export class ToolbarFixture { this.offsetPlaneButton = page.getByTestId('plane-offset') this.startSketchBtn = page.getByTestId('sketch') this.lineBtn = page.getByTestId('line') + this.tangentialArcBtn = page.getByTestId('tangential-arc') + this.circleBtn = page.getByTestId('circle-center') this.rectangleBtn = page.getByTestId('corner-rectangle') + this.lengthConstraintBtn = page.getByTestId('constraint-length') this.exitSketchBtn = page.getByTestId('sketch-exit') this.editSketchBtn = page.getByText('Edit Sketch') this.fileTreeBtn = page.locator('[id="files-button-holder"]') @@ -91,4 +97,13 @@ export class ToolbarFixture { await expect(this.exeIndicator).toBeVisible({ timeout: 15_000 }) } } + selectCenterRectangle = async () => { + await this.page + .getByRole('button', { name: 'caret down Corner rectangle:' }) + .click() + await expect( + this.page.getByTestId('dropdown-center-rectangle') + ).toBeVisible() + await this.page.getByTestId('dropdown-center-rectangle').click() + } } diff --git a/e2e/playwright/onboarding-tests.spec.ts b/e2e/playwright/onboarding-tests.spec.ts index 9c1652be6..c722f120a 100644 --- a/e2e/playwright/onboarding-tests.spec.ts +++ b/e2e/playwright/onboarding-tests.spec.ts @@ -425,7 +425,7 @@ test.describe('Onboarding tests', () => { }) }) -test.fixme( +test( '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 6073ad6b6..7ee02695d 100644 --- a/e2e/playwright/point-click.spec.ts +++ b/e2e/playwright/point-click.spec.ts @@ -135,7 +135,9 @@ test.describe('verify sketch on chamfer works', () => { pixelDiff: 50, }) await rectangle2ndClick() - await editor.expectEditor.toContain(afterRectangle2ndClickSnippet) + await editor.expectEditor.toContain(afterRectangle2ndClickSnippet, { + shouldNormalise: true, + }) }) await test.step('Clean up so that `_sketchOnAChamfer` util can be called again', async () => { @@ -177,18 +179,13 @@ test.describe('verify sketch on chamfer works', () => { afterChamferSelectSnippet: 'sketch002 = startSketchOn(extrude001, seg03)', - afterRectangle1stClickSnippet: 'startProfileAt([205.96, 254.59], %)', - afterRectangle2ndClickSnippet: `angledLine([0, 11.39], %, $rectangleSegmentA002) - |> angledLine([ - segAng(rectangleSegmentA002) - 90, - 105.26 - ], %, $rectangleSegmentB001) - |> angledLine([ - segAng(rectangleSegmentA002), - -segLen(rectangleSegmentA002) - ], %, $rectangleSegmentC001) - |> lineTo([profileStartX(%), profileStartY(%)], %) - |> close(%)`, + afterRectangle1stClickSnippet: + 'startProfileAt([205.96, 254.59], sketch002)', + afterRectangle2ndClickSnippet: `angledLine([0,11.39],%,$rectangleSegmentA002) + |>angledLine([segAng(rectangleSegmentA002)-90,105.26],%) + |>angledLine([segAng(rectangleSegmentA002),-segLen(rectangleSegmentA002)],%) + |>lineTo([profileStartX(%),profileStartY(%)],%) + |>close(%)`, }) await sketchOnAChamfer({ @@ -209,19 +206,15 @@ test.describe('verify sketch on chamfer works', () => { afterChamferSelectSnippet: 'sketch003 = startSketchOn(extrude001, seg04)', - afterRectangle1stClickSnippet: 'startProfileAt([-209.64, 255.28], %)', - afterRectangle2ndClickSnippet: `angledLine([0, 11.56], %, $rectangleSegmentA003) - |> angledLine([ - segAng(rectangleSegmentA003) - 90, - 106.84 - ], %, $rectangleSegmentB002) - |> angledLine([ - segAng(rectangleSegmentA003), - -segLen(rectangleSegmentA003) - ], %, $rectangleSegmentC002) - |> lineTo([profileStartX(%), profileStartY(%)], %) - |> close(%)`, + afterRectangle1stClickSnippet: + 'startProfileAt([-209.64, 255.28], sketch003)', + afterRectangle2ndClickSnippet: `angledLine([0,11.56],%,$rectangleSegmentA003) + |>angledLine([segAng(rectangleSegmentA003)-90,106.84],%) + |>angledLine([segAng(rectangleSegmentA003),-segLen(rectangleSegmentA003)],%) + |>lineTo([profileStartX(%),profileStartY(%)],%) + |>close(%)`, }) + await sketchOnAChamfer({ clickCoords: { x: 677, y: 87 }, cameraPos: { x: -6200, y: 1500, z: 6200 }, @@ -234,19 +227,14 @@ test.describe('verify sketch on chamfer works', () => { ] }, %)`, afterChamferSelectSnippet: - 'sketch003 = startSketchOn(extrude001, seg04)', - afterRectangle1stClickSnippet: 'startProfileAt([-209.64, 255.28], %)', - afterRectangle2ndClickSnippet: `angledLine([0, 11.56], %, $rectangleSegmentA003) - |> angledLine([ - segAng(rectangleSegmentA003) - 90, - 106.84 - ], %, $rectangleSegmentB002) - |> angledLine([ - segAng(rectangleSegmentA003), - -segLen(rectangleSegmentA003) - ], %, $rectangleSegmentC002) - |> lineTo([profileStartX(%), profileStartY(%)], %) - |> close(%)`, + 'sketch004 = startSketchOn(extrude001, seg05)', + afterRectangle1stClickSnippet: + 'startProfileAt([82.57, 322.96], sketch004)', + afterRectangle2ndClickSnippet: `angledLine([0,11.16],%,$rectangleSegmentA004) + |>angledLine([segAng(rectangleSegmentA004)-90,103.07],%) + |>angledLine([segAng(rectangleSegmentA004),-segLen(rectangleSegmentA004)],%) + |>lineTo([profileStartX(%),profileStartY(%)],%)| + >close(%)`, }) /// last one await sketchOnAChamfer({ @@ -259,104 +247,97 @@ test.describe('verify sketch on chamfer works', () => { }, %)`, afterChamferSelectSnippet: 'sketch005 = startSketchOn(extrude001, seg06)', - afterRectangle1stClickSnippet: 'startProfileAt([-23.43, 19.69], %)', - afterRectangle2ndClickSnippet: `angledLine([0, 9.1], %, $rectangleSegmentA005) - - |> angledLine([ - segAng(rectangleSegmentA005) - 90, - 84.07 - ], %, $rectangleSegmentB004) - |> angledLine([ - segAng(rectangleSegmentA005), - -segLen(rectangleSegmentA005) - ], %, $rectangleSegmentC004) - |> lineTo([profileStartX(%), profileStartY(%)], %) - |> close(%)`, + afterRectangle1stClickSnippet: + 'startProfileAt([-23.43, 19.69], sketch005)', + afterRectangle2ndClickSnippet: `angledLine([0,9.1],%,$rectangleSegmentA005) + |>angledLine([segAng(rectangleSegmentA005)-90,84.07],%) + |>angledLine([segAng(rectangleSegmentA005),-segLen(rectangleSegmentA005)],%) + |>lineTo([profileStartX(%),profileStartY(%)],%) + |>close(%)`, }) await test.step('verify at the end of the test that final code is what is expected', async () => { await editor.expectEditor.toContain( `sketch001 = startSketchOn('XZ') - - |> startProfileAt([75.8, 317.2], %) // [$startCapTag, $EndCapTag] - |> angledLine([0, 268.43], %, $rectangleSegmentA001) - |> angledLine([ - segAng(rectangleSegmentA001) - 90, - 217.26 - ], %, $seg01) - |> angledLine([ - segAng(rectangleSegmentA001), - -segLen(rectangleSegmentA001) - ], %, $yo) - |> lineTo([profileStartX(%), profileStartY(%)], %, $seg02) - |> close(%) - extrude001 = extrude(100, sketch001) - |> chamfer({ - length = 30, - tags = [getOppositeEdge(seg01)] - }, %, $seg03) - |> chamfer({ length = 30, tags = [seg01] }, %, $seg04) - |> chamfer({ - length = 30, - tags = [getNextAdjacentEdge(seg02)] - }, %, $seg05) - |> chamfer({ - length = 30, - tags = [getNextAdjacentEdge(yo)] - }, %, $seg06) - sketch005 = startSketchOn(extrude001, seg06) - |> startProfileAt([-23.43, 19.69], %) - |> angledLine([0, 9.1], %, $rectangleSegmentA005) - |> angledLine([ - segAng(rectangleSegmentA005) - 90, - 84.07 - ], %, $rectangleSegmentB004) - |> angledLine([ - segAng(rectangleSegmentA005), - -segLen(rectangleSegmentA005) - ], %, $rectangleSegmentC004) - |> lineTo([profileStartX(%), profileStartY(%)], %) - |> close(%) - sketch004 = startSketchOn(extrude001, seg05) - |> startProfileAt([82.57, 322.96], %) - |> angledLine([0, 11.16], %, $rectangleSegmentA004) - |> angledLine([ - segAng(rectangleSegmentA004) - 90, - 103.07 - ], %, $rectangleSegmentB003) - |> angledLine([ - segAng(rectangleSegmentA004), - -segLen(rectangleSegmentA004) - ], %, $rectangleSegmentC003) - |> lineTo([profileStartX(%), profileStartY(%)], %) - |> close(%) - sketch003 = startSketchOn(extrude001, seg04) - |> startProfileAt([-209.64, 255.28], %) - |> angledLine([0, 11.56], %, $rectangleSegmentA003) - |> angledLine([ - segAng(rectangleSegmentA003) - 90, - 106.84 - ], %, $rectangleSegmentB002) - |> angledLine([ - segAng(rectangleSegmentA003), - -segLen(rectangleSegmentA003) - ], %, $rectangleSegmentC002) - |> lineTo([profileStartX(%), profileStartY(%)], %) - |> close(%) - sketch002 = startSketchOn(extrude001, seg03) - |> startProfileAt([205.96, 254.59], %) - |> angledLine([0, 11.39], %, $rectangleSegmentA002) - |> angledLine([ - segAng(rectangleSegmentA002) - 90, - 105.26 - ], %, $rectangleSegmentB001) - |> angledLine([ - segAng(rectangleSegmentA002), - -segLen(rectangleSegmentA002) - ], %, $rectangleSegmentC001) - |> lineTo([profileStartX(%), profileStartY(%)], %) - |> close(%) - `, + |> startProfileAt([75.8, 317.2], %) // [$startCapTag, $EndCapTag] + |> angledLine([0, 268.43], %, $rectangleSegmentA001) + |> angledLine([ + segAng(rectangleSegmentA001) - 90, + 217.26 + ], %, $seg01) + |> angledLine([ + segAng(rectangleSegmentA001), + -segLen(rectangleSegmentA001) + ], %, $yo) + |> lineTo([profileStartX(%), profileStartY(%)], %, $seg02) + |> close(%) +extrude001 = extrude(100, sketch001) + |> chamfer({ + length = 30, + tags = [getOppositeEdge(seg01)] + }, %, $seg03) + |> chamfer({ length = 30, tags = [seg01] }, %, $seg04) + |> chamfer({ + length = 30, + tags = [getNextAdjacentEdge(seg02)] + }, %, $seg05) + |> chamfer({ + length = 30, + tags = [getNextAdjacentEdge(yo)] + }, %, $seg06) +sketch005 = startSketchOn(extrude001, seg06) +profile004 = startProfileAt([-23.43, 19.69], sketch005) + |> angledLine([0, 9.1], %, $rectangleSegmentA005) + |> angledLine([ + segAng(rectangleSegmentA005) - 90, + 84.07 + ], %) + |> angledLine([ + segAng(rectangleSegmentA005), + -segLen(rectangleSegmentA005) + ], %) + |> lineTo([profileStartX(%), profileStartY(%)], %) + |> close(%) +sketch004 = startSketchOn(extrude001, seg05) +profile003 = startProfileAt([82.57, 322.96], sketch004) + |> angledLine([0, 11.16], %, $rectangleSegmentA004) + |> angledLine([ + segAng(rectangleSegmentA004) - 90, + 103.07 + ], %) + |> angledLine([ + segAng(rectangleSegmentA004), + -segLen(rectangleSegmentA004) + ], %) + |> lineTo([profileStartX(%), profileStartY(%)], %) + |> close(%) +sketch003 = startSketchOn(extrude001, seg04) +profile002 = startProfileAt([-209.64, 255.28], sketch003) + |> angledLine([0, 11.56], %, $rectangleSegmentA003) + |> angledLine([ + segAng(rectangleSegmentA003) - 90, + 106.84 + ], %) + |> angledLine([ + segAng(rectangleSegmentA003), + -segLen(rectangleSegmentA003) + ], %) + |> lineTo([profileStartX(%), profileStartY(%)], %) + |> close(%) +sketch002 = startSketchOn(extrude001, seg03) +profile001 = startProfileAt([205.96, 254.59], sketch002) + |> angledLine([0, 11.39], %, $rectangleSegmentA002) + |> angledLine([ + segAng(rectangleSegmentA002) - 90, + 105.26 + ], %) + |> angledLine([ + segAng(rectangleSegmentA002), + -segLen(rectangleSegmentA002) + ], %) + |> lineTo([profileStartX(%), profileStartY(%)], %) + |> close(%) +`, { shouldNormalise: true } ) }) @@ -392,18 +373,13 @@ test.describe('verify sketch on chamfer works', () => { beforeChamferSnippetEnd: '}, extrude001)', afterChamferSelectSnippet: 'sketch002 = startSketchOn(extrude001, seg03)', - afterRectangle1stClickSnippet: 'startProfileAt([205.96, 254.59], %)', - afterRectangle2ndClickSnippet: `angledLine([0, 11.39], %, $rectangleSegmentA002) - |> angledLine([ - segAng(rectangleSegmentA002) - 90, - 105.26 - ], %, $rectangleSegmentB001) - |> angledLine([ - segAng(rectangleSegmentA002), - -segLen(rectangleSegmentA002) - ], %, $rectangleSegmentC001) - |> lineTo([profileStartX(%), profileStartY(%)], %) - |> close(%)`, + afterRectangle1stClickSnippet: + 'startProfileAt([205.96, 254.59], sketch002)', + afterRectangle2ndClickSnippet: `angledLine([0,11.39],%,$rectangleSegmentA002) + |>angledLine([segAng(rectangleSegmentA002)-90,105.26],%) + |>angledLine([segAng(rectangleSegmentA002),-segLen(rectangleSegmentA002)],%) + |>lineTo([profileStartX(%),profileStartY(%)],%) + |>close(%)`, }) await editor.expectEditor.toContain( `sketch001 = startSketchOn('XZ') @@ -433,16 +409,16 @@ chamf = chamfer({ ] }, %) sketch002 = startSketchOn(extrude001, seg03) - |> startProfileAt([205.96, 254.59], %) +profile001 = startProfileAt([205.96, 254.59], sketch002) |> angledLine([0, 11.39], %, $rectangleSegmentA002) |> angledLine([ segAng(rectangleSegmentA002) - 90, 105.26 - ], %, $rectangleSegmentB001) + ], %) |> angledLine([ segAng(rectangleSegmentA002), -segLen(rectangleSegmentA002) - ], %, $rectangleSegmentC001) + ], %) |> lineTo([profileStartX(%), profileStartY(%)], %) |> close(%) `, @@ -504,10 +480,10 @@ test(`Verify axis, origin, and horizontal snapping`, async ({ const expectedCodeSnippets = { sketchOnXzPlane: `sketch001 = startSketchOn('XZ')`, - pointAtOrigin: `startProfileAt([${originSloppy.kcl[0]}, ${originSloppy.kcl[1]}], %)`, + pointAtOrigin: `startProfileAt([${originSloppy.kcl[0]}, ${originSloppy.kcl[1]}], sketch001)`, segmentOnXAxis: `xLine(${xAxisSloppy.kcl[0]}, %)`, - afterSegmentDraggedOffYAxis: `startProfileAt([${offYAxis.kcl[0]}, ${offYAxis.kcl[1]}], %)`, - afterSegmentDraggedOnYAxis: `startProfileAt([${yAxisSloppy.kcl[0]}, ${yAxisSloppy.kcl[1]}], %)`, + afterSegmentDraggedOffYAxis: `startProfileAt([${offYAxis.kcl[0]}, ${offYAxis.kcl[1]}], sketch001)`, + afterSegmentDraggedOnYAxis: `startProfileAt([${yAxisSloppy.kcl[0]}, ${yAxisSloppy.kcl[1]}], sketch001)`, } await app.initialise() diff --git a/e2e/playwright/sketch-tests.spec.ts b/e2e/playwright/sketch-tests.spec.ts index e96ad60a5..6c16fbb78 100644 --- a/e2e/playwright/sketch-tests.spec.ts +++ b/e2e/playwright/sketch-tests.spec.ts @@ -7,6 +7,7 @@ import { PERSIST_MODELING_CONTEXT, setup, tearDown, + TEST_COLORS, } from './test-utils' import { uuidv4, roundOff } from 'lib/utils' @@ -114,9 +115,9 @@ test.describe('Sketch tests', () => { localStorage.setItem( 'persistCode', `sketch001 = startSketchOn('XZ') - |> startProfileAt([4.61, -14.01], %) - |> xLine(12.73, %) - |> tangentialArcTo([24.95, -5.38], %)` + |> startProfileAt([2.61, -4.01], %) + |> xLine(8.73, %) + |> tangentialArcTo([8.33, -1.31], %)` ) }) @@ -126,7 +127,7 @@ test.describe('Sketch tests', () => { await expect(async () => { await page.mouse.click(700, 200) - await page.getByText('tangentialArcTo([24.95, -5.38], %)').click() + await page.getByText('tangentialArcTo([8.33, -1.31], %)').click() await expect( page.getByRole('button', { name: 'Edit Sketch' }) ).toBeEnabled({ timeout: 1000 }) @@ -135,7 +136,7 @@ test.describe('Sketch tests', () => { await page.waitForTimeout(600) // wait for animation - await page.getByText('tangentialArcTo([24.95, -5.38], %)').click() + await page.getByText('tangentialArcTo([8.33, -1.31], %)').click() await page.keyboard.press('End') await page.keyboard.down('Shift') await page.keyboard.press('ArrowUp') @@ -149,17 +150,21 @@ 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(`sketch001 = startSketchOn('XZ') - |> startProfileAt([12.34, -12.34], %) + .toBe(`sketch002 = startSketchOn('XZ') +sketch001 = startProfileAt([12.34, -12.34], sketch002) |> yLine(12.34, %) `) - }).toPass({ timeout: 40_000, intervals: [1_000] }) + }).toPass({ timeout: 5_000, intervals: [1_000] }) }) test('Can exit selection of face', async ({ page }) => { // Load the app with the code panes @@ -669,7 +674,7 @@ test.describe('Sketch tests', () => { await page.waitForTimeout(500) // TODO detect animation ending, or disable animation await click00r(0, 0) - codeStr += ` |> startProfileAt(${toSU([0, 0])}, %)` + codeStr += `profile001 = startProfileAt(${toSU([0, 0])}, sketch001)` await expect(u.codeLocator).toHaveText(codeStr) await click00r(50, 0) @@ -705,7 +710,7 @@ test.describe('Sketch tests', () => { await u.closeDebugPanel() await click00r(30, 0) - codeStr += ` |> startProfileAt([2.03, 0], %)` + codeStr += `profile002 = startProfileAt([2.03, 0], sketch002)` await expect(u.codeLocator).toHaveText(codeStr) // TODO: I couldn't use `toSU` here because of some rounding error causing @@ -742,7 +747,9 @@ test.describe('Sketch tests', () => { await u.openDebugPanel() const code = `sketch001 = startSketchOn('-XZ') - |> startProfileAt([${roundOff(scale * 69.6)}, ${roundOff(scale * 34.8)}], %) +profile001 = startProfileAt([${roundOff(scale * 69.6)}, ${roundOff( + scale * 34.8 + )}], sketch001) |> xLine(${roundOff(scale * 139.19)}, %) |> yLine(-${roundOff(scale * 139.2)}, %) |> lineTo([profileStartX(%), profileStartY(%)], %) @@ -808,11 +815,17 @@ test.describe('Sketch tests', () => { await expect(page.locator('.cm-content')).not.toHaveText(prevContent) prevContent = await page.locator('.cm-content').innerText() - await expect(page.locator('.cm-content')).toHaveText(code) - // Assert the tool was unequipped + 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.getByRole('button', { name: 'line Line', exact: true }) - ).not.toHaveAttribute('aria-pressed', 'true') + ).toHaveAttribute('aria-pressed', 'true') // exit sketch await u.openAndClearDebugPanel() @@ -1130,11 +1143,17 @@ 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(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ') - |> startProfileAt([11.8, 9.09], %) - |> line([3.39, -3.39], %) -`) - + 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 page.addInitScript(async () => { localStorage.setItem( 'persistCode', @@ -1332,7 +1351,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: [number, number, number] = [255, 255, 255] + const arrowHeadWhite = TEST_COLORS.WHITE const backgroundGray: [number, number, number] = [28, 28, 28] const verifyArrowHeadColor = async (c: [number, number, number]) => scene.expectPixelColor(c, arrowHeadLocation, 15) @@ -1419,3 +1438,890 @@ 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 4b2e1b8f4..e7f94b670 100644 --- a/e2e/playwright/snapshot-tests.spec.ts +++ b/e2e/playwright/snapshot-tests.spec.ts @@ -446,8 +446,7 @@ test( const startXPx = 600 await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) - code += ` - |> startProfileAt([7.19, -9.7], %)` + code += `profile001 = startProfileAt([7.19, -9.7], sketch001)` await expect(page.locator('.cm-content')).toHaveText(code) await page.waitForTimeout(100) @@ -469,6 +468,10 @@ test( .getByRole('button', { name: 'arc Tangential Arc', exact: true }) .click() + // click to continue profile + await page.mouse.move(813, 392, { steps: 10 }) + await page.waitForTimeout(100) + await page.mouse.move(startXPx + PUR * 30, 500 - PUR * 20, { steps: 10 }) await page.waitForTimeout(1000) @@ -591,8 +594,7 @@ test( mask: [page.getByTestId('model-state-indicator')], }) await expect(page.locator('.cm-content')).toHaveText( - `sketch001 = startSketchOn('XZ') - |> circle({ center = [14.44, -2.44], radius = 1 }, %)` + `sketch001 = startSketchOn('XZ')profile001 = circle({ center = [14.44, -2.44], radius = 1 }, sketch001)` ) } ) @@ -636,8 +638,7 @@ test.describe( const startXPx = 600 await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) - code += ` - |> startProfileAt([7.19, -9.7], %)` + code += `profile001 = startProfileAt([7.19, -9.7], sketch001)` await expect(u.codeLocator).toHaveText(code) await page.waitForTimeout(100) @@ -655,6 +656,10 @@ test.describe( .click() await page.waitForTimeout(100) + // click to continue profile + await page.mouse.click(813, 392) + await page.waitForTimeout(100) + await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20) code += ` @@ -741,8 +746,7 @@ test.describe( const startXPx = 600 await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) - code += ` - |> startProfileAt([182.59, -246.32], %)` + code += `profile001 = startProfileAt([182.59, -246.32], sketch001)` await expect(u.codeLocator).toHaveText(code) await page.waitForTimeout(100) @@ -760,6 +764,10 @@ test.describe( .click() await page.waitForTimeout(100) + // click to continue profile + await page.mouse.click(813, 392) + await page.waitForTimeout(100) + await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20) code += ` 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 45c6a4241..dc257dad5 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 0571ae984..be428bf08 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-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 d0f165e5c..5cd6926bf 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 8561d872a..31d1b085e 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 0100b1691..10a4bf4b8 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 3877bfc48..ac2bc65b0 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 f9c30400a..8deadee79 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 4787f484d..88524a55b 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 8af8484e9..850b72cc1 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 96422ab7a..0de2002d3 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 8fdf3768c..0efde30de 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 38d1cb522..9a7fd6623 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 fe490db6f..7fa2b6b7c 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 b0f06ad60..b3dcfe635 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 974555be9..7cec6f2e8 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 5f235e51c..c01783eb2 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/test-network-and-connection-issues.spec.ts b/e2e/playwright/test-network-and-connection-issues.spec.ts index e38d08c5a..e201f4ce4 100644 --- a/e2e/playwright/test-network-and-connection-issues.spec.ts +++ b/e2e/playwright/test-network-and-connection-issues.spec.ts @@ -1,6 +1,8 @@ import { test, expect } from '@playwright/test' import { commonPoints, getUtils, setup, tearDown } from './test-utils' +import { uuidv4 } from 'lib/utils' +import { EngineCommand } from 'lang/std/artifactGraph' test.beforeEach(async ({ context, page }, testInfo) => { await setup(context, page, testInfo) @@ -130,17 +132,16 @@ test.describe('Test network and connection issues', () => { const startXPx = 600 await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) - await expect(page.locator('.cm-content')) - .toHaveText(`sketch001 = startSketchOn('XZ') - |> startProfileAt(${commonPoints.startAt}, %)`) + await expect(page.locator('.cm-content')).toHaveText( + `sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${commonPoints.startAt}, sketch001)` + ) await page.waitForTimeout(100) await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10) await page.waitForTimeout(100) await expect(page.locator('.cm-content')) - .toHaveText(`sketch001 = startSketchOn('XZ') - |> startProfileAt(${commonPoints.startAt}, %) + .toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${commonPoints.startAt}, sketch001) |> xLine(${commonPoints.num1}, %)`) // Expect the network to be up @@ -188,7 +189,9 @@ test.describe('Test network and connection issues', () => { await page.mouse.click(100, 100) // select a line - await page.getByText(`startProfileAt(${commonPoints.startAt}, %)`).click() + await page + .getByText(`startProfileAt(${commonPoints.startAt}, sketch001)`) + .click() // enter sketch again await u.doAndWaitForCmd( @@ -202,11 +205,36 @@ test.describe('Test network and connection issues', () => { await page.waitForTimeout(150) + const camCommand: EngineCommand = { + type: 'modeling_cmd_req', + cmd_id: uuidv4(), + cmd: { + type: 'default_camera_look_at', + center: { x: 109, y: 0, z: -152 }, + vantage: { x: 115, y: -505, z: -152 }, + up: { x: 0, y: 0, z: 1 }, + }, + } + const updateCamCommand: EngineCommand = { + type: 'modeling_cmd_req', + cmd_id: uuidv4(), + cmd: { + type: 'default_camera_get_settings', + }, + } + await u.sendCustomCmd(camCommand) + await page.waitForTimeout(100) + await u.sendCustomCmd(updateCamCommand) + await page.waitForTimeout(100) + + // click to continue profile + await page.mouse.click(1007, 400) + await page.waitForTimeout(100) // Ensure we can continue sketching await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20) await expect.poll(u.normalisedEditorCode) .toBe(`sketch001 = startSketchOn('XZ') - |> startProfileAt([12.34, -12.34], %) +profile001 = startProfileAt([12.34, -12.34], sketch001) |> xLine(12.34, %) |> line([-12.34, 12.34], %) @@ -216,7 +244,7 @@ test.describe('Test network and connection issues', () => { await expect.poll(u.normalisedEditorCode) .toBe(`sketch001 = startSketchOn('XZ') - |> startProfileAt([12.34, -12.34], %) +profile001 = startProfileAt([12.34, -12.34], sketch001) |> xLine(12.34, %) |> line([-12.34, 12.34], %) |> xLine(-12.34, %) diff --git a/e2e/playwright/testing-constraints.spec.ts b/e2e/playwright/testing-constraints.spec.ts index 026568781..f02c0515b 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 ef984ce50..fb4880e6c 100644 --- a/e2e/playwright/testing-selections.spec.ts +++ b/e2e/playwright/testing-selections.spec.ts @@ -77,30 +77,31 @@ test.describe('Testing selections', () => { const startXPx = 600 await u.closeDebugPanel() await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) - await expect(page.locator('.cm-content')) - .toHaveText(`sketch001 = startSketchOn('XZ') - |> startProfileAt(${commonPoints.startAt}, %)`) + await expect(page.locator('.cm-content')).toHaveText( + `sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${commonPoints.startAt}, sketch001)` + ) await page.waitForTimeout(100) await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10) await expect(page.locator('.cm-content')) - .toHaveText(`sketch001 = startSketchOn('XZ') - |> startProfileAt(${commonPoints.startAt}, %) + .toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${commonPoints.startAt}, sketch001) |> xLine(${commonPoints.num1}, %)`) await page.waitForTimeout(100) await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20) await expect(page.locator('.cm-content')) - .toHaveText(`sketch001 = startSketchOn('XZ') - |> startProfileAt(${commonPoints.startAt}, %) + .toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${ + commonPoints.startAt + }, sketch001) |> xLine(${commonPoints.num1}, %) |> yLine(${commonPoints.num1 + 0.01}, %)`) await page.waitForTimeout(100) await page.mouse.click(startXPx, 500 - PUR * 20) await expect(page.locator('.cm-content')) - .toHaveText(`sketch001 = startSketchOn('XZ') - |> startProfileAt(${commonPoints.startAt}, %) + .toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${ + commonPoints.startAt + }, sketch001) |> xLine(${commonPoints.num1}, %) |> yLine(${commonPoints.num1 + 0.01}, %) |> xLine(${commonPoints.num2 * -1}, %)`) @@ -330,6 +331,28 @@ part009 = startSketchOn('XY') |> angledLineToX({ angle = 60, to = pipeLargeDia }, %) |> close(%) rev = revolve({ axis = 'y' }, part009) +sketch006 = startSketchOn('XY') +profile001 = circle({ + center = [42.91, -70.42], + radius = 17.96 +}, sketch006) +profile002 = startProfileAt([86.92, -63.81], sketch006) + |> angledLine([0, 63.81], %, $rectangleSegmentA001) + |> angledLine([ + segAng(rectangleSegmentA001) - 90, + 17.05 + ], %) + |> angledLine([ + segAng(rectangleSegmentA001), + -segLen(rectangleSegmentA001) + ], %) + |> lineTo([profileStartX(%), profileStartY(%)], %) + |> close(%) +profile003 = startProfileAt([40.16, -120.48], sketch006) + |> line([26.95, 24.21], %) + |> line([20.91, -28.61], %) + |> line([32.46, 18.71], %) + ` ) }, KCL_DEFAULT_LENGTH) @@ -362,9 +385,10 @@ rev = revolve({ axis = 'y' }, part009) }) await page.waitForTimeout(100) - const revolve = { x: 646, y: 248 } + const revolve = { x: 635, y: 253 } const parentExtrude = { x: 915, y: 133 } const solid2d = { x: 770, y: 167 } + const individualProfile = { x: 694, y: 432 } // DELETE REVOLVE await page.mouse.click(revolve.x, revolve.y) @@ -430,6 +454,20 @@ rev = revolve({ axis = 'y' }, part009) await u.expectCmdLog('[data-message-type="execution-done"]', 10_000) await page.waitForTimeout(200) await expect(u.codeLocator).not.toContainText(`sketch005 = startSketchOn({`) + + // Delete a single profile + await page.mouse.click(individualProfile.x, individualProfile.y) + await page.waitForTimeout(100) + const codeToBeDeletedSnippet = + 'profile003 = startProfileAt([40.16, -120.48], sketch006)' + await expect(page.locator('.cm-activeLine')).toHaveText( + ' |> line([20.91, -28.61], %)' + ) + await u.clearCommandLogs() + await page.keyboard.press('Backspace') + await u.expectCmdLog('[data-message-type="execution-done"]', 10_000) + await page.waitForTimeout(200) + await expect(u.codeLocator).not.toContainText(codeToBeDeletedSnippet) }) test("Deleting solid that the AST mod can't handle results in a toast message", async ({ page, @@ -1258,12 +1296,15 @@ extrude001 = extrude(50, sketch001) await page.waitForTimeout(600) + const firstClickCoords = { x: 650, y: 200 } as const // Place a point because the line tool will exit if no points are pressed - await page.mouse.click(650, 200) + await page.mouse.click(firstClickCoords.x, firstClickCoords.y) await page.waitForTimeout(600) // Code before exiting the tool - let previousCodeContent = await page.locator('.cm-content').innerText() + let previousCodeContent = ( + await page.locator('.cm-content').innerText() + ).replace(/\s+/g, '') // deselect the line tool by clicking it await page.getByRole('button', { name: 'line Line', exact: true }).click() @@ -1275,14 +1316,23 @@ extrude001 = extrude(50, sketch001) await page.mouse.click(750, 200) await page.waitForTimeout(100) - // expect no change - await expect(page.locator('.cm-content')).toHaveText(previousCodeContent) + await expect + .poll(async () => { + let str = await page.locator('.cm-content').innerText() + str = str.replace(/\s+/g, '') + return str + }) + .toBe(previousCodeContent) // select line tool again await page.getByRole('button', { name: 'line Line', exact: true }).click() await u.closeDebugPanel() + // Click to continue profile + await page.mouse.click(firstClickCoords.x, firstClickCoords.y) + await page.waitForTimeout(100) + // line tool should work as expected again await page.mouse.click(700, 200) await expect(page.locator('.cm-content')).not.toHaveText( diff --git a/e2e/playwright/various.spec.ts b/e2e/playwright/various.spec.ts index e2ce53138..93551bd6b 100644 --- a/e2e/playwright/various.spec.ts +++ b/e2e/playwright/various.spec.ts @@ -224,8 +224,13 @@ test('First escape in tool pops you out of tool, second exits sketch mode', asyn // Draw a line await page.mouse.move(700, 200, { steps: 5 }) await page.mouse.click(700, 200) - await page.mouse.move(800, 250, { steps: 5 }) - await page.mouse.click(800, 250) + + const secondMousePosition = { x: 800, y: 250 } + + await page.mouse.move(secondMousePosition.x, secondMousePosition.y, { + steps: 5, + }) + await page.mouse.click(secondMousePosition.x, secondMousePosition.y) // Unequip line tool await page.keyboard.press('Escape') // Make sure we didn't pop out of sketch mode. @@ -234,9 +239,17 @@ test('First escape in tool pops you out of tool, second exits sketch mode', asyn // Equip arc tool await page.keyboard.press('a') await expect(arcButton).toHaveAttribute('aria-pressed', 'true') + + // click in the same position again to continue the profile + await page.mouse.move(secondMousePosition.x, secondMousePosition.y, { + steps: 5, + }) + await page.mouse.click(secondMousePosition.x, secondMousePosition.y) + await page.mouse.move(1000, 100, { steps: 5 }) await page.mouse.click(1000, 100) await page.keyboard.press('Escape') + await expect(arcButton).toHaveAttribute('aria-pressed', 'false') await page.keyboard.press('l') await expect(lineButton).toHaveAttribute('aria-pressed', 'true') @@ -537,9 +550,9 @@ test('Sketch on face', async ({ page }) => { await expect.poll(u.normalisedEditorCode).toContain( u.normalisedCode(`sketch002 = startSketchOn(extrude001, seg01) - |> startProfileAt([-12.94, 6.6], %) - |> line([2.45, -0.2], %) - |> line([-2.6, -1.25], %) +profile001 = startProfileAt([-12.88, 6.66], sketch002) + |> line([2.71, -0.22], %) + |> line([-2.87, -1.38], %) |> lineTo([profileStartX(%), profileStartY(%)], %) |> close(%)`) ) @@ -554,8 +567,7 @@ test('Sketch on face', async ({ page }) => { await page.getByText('startProfileAt([-12').click() await expect(page.getByRole('button', { name: 'Edit Sketch' })).toBeVisible() await page.getByRole('button', { name: 'Edit Sketch' }).click() - await page.waitForTimeout(400) - await page.waitForTimeout(150) + await page.waitForTimeout(500) await page.setViewportSize({ width: 1200, height: 1200 }) await u.openAndClearDebugPanel() await u.updateCamPosition([452, -152, 1166]) diff --git a/src/Toolbar.tsx b/src/Toolbar.tsx index e6e680721..6b66be341 100644 --- a/src/Toolbar.tsx +++ b/src/Toolbar.tsx @@ -6,7 +6,6 @@ import { useCommandsContext } from 'hooks/useCommandsContext' import { useNetworkContext } from 'hooks/useNetworkContext' import { NetworkHealthState } from 'hooks/useNetworkStatus' import { ActionButton } from 'components/ActionButton' -import { isSingleCursorInPipe } from 'lang/queryAst' import { useKclContext } from 'lang/KclProvider' import { ActionButtonDropdown } from 'components/ActionButtonDropdown' import { useHotkeys } from 'react-hotkeys-hook' @@ -22,6 +21,7 @@ import { } from 'lib/toolbar' import { isDesktop } from 'lib/isDesktop' import { openExternalBrowserIfDesktop } from 'lib/openWindow' +import { isCursorInFunctionDefinition } from 'lang/queryAst' export function Toolbar({ className = '', @@ -38,7 +38,12 @@ export function Toolbar({ '!border-transparent hover:!border-chalkboard-20 dark:enabled:hover:!border-primary pressed:!border-primary ui-open:!border-primary' const sketchPathId = useMemo(() => { - if (!isSingleCursorInPipe(context.selectionRanges, kclManager.ast)) + if ( + isCursorInFunctionDefinition( + kclManager.ast, + context.selectionRanges.graphSelections[0] + ) + ) return false return isCursorInSketchCommandRange( engineCommandManager.artifactGraph, diff --git a/src/clientSideScene/ClientSideSceneComp.tsx b/src/clientSideScene/ClientSideSceneComp.tsx index b8031e3f8..bbf47c586 100644 --- a/src/clientSideScene/ClientSideSceneComp.tsx +++ b/src/clientSideScene/ClientSideSceneComp.tsx @@ -433,6 +433,8 @@ 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 d9560bd2f..30cb5ac12 100644 --- a/src/clientSideScene/sceneEntities.ts +++ b/src/clientSideScene/sceneEntities.ts @@ -43,11 +43,9 @@ import { ProgramMemory, recast, Sketch, - Solid, VariableDeclaration, VariableDeclarator, sketchFromKclValue, - sketchFromKclValueOptional, defaultSourceRange, sourceRangeFromRust, resultIsOk, @@ -75,20 +73,29 @@ 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 } from 'machines/modelingMachine' +import { + SegmentOverlayPayload, + SketchDetails, + SketchDetailsUpdate, + SketchTool, +} from 'machines/modelingMachine' import { EngineCommandManager } from 'lang/std/engineConnection' import { getRectangleCallExpressions, @@ -96,12 +103,13 @@ import { updateCenterRectangleSketch, } from 'lib/rectangleTool' import { getThemeColorForThreeJs, Themes } from 'lib/theme' -import { err, Reason, reportRejection, trap } from 'lib/trap' +import { err, 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' @@ -333,6 +341,9 @@ 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) @@ -346,9 +357,17 @@ export class SceneEntities { setupNoPointsListener({ sketchDetails, afterClick, + currentTool, }: { sketchDetails: SketchDetails - afterClick: (args: OnClickCallbackArgs) => void + currentTool: SketchTool + afterClick: ( + args: OnClickCallbackArgs, + updatedPaths: { + sketchNodePaths: PathToNode[] + sketchEntryNodePath: PathToNode + } + ) => void }) { // TODO: Consolidate shared logic between this and setupSketch // Which should just fire when the sketch mode is entered, @@ -388,14 +407,31 @@ export class SceneEntities { sceneObject.object.name === X_AXIS || sceneObject.object.name === Y_AXIS ) - if (!axisIntersection) return + + 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 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 { + } else if (axisIntersection?.object.name === X_AXIS) { 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() @@ -431,7 +467,25 @@ export class SceneEntities { if (interaction !== 'none') return if (args.mouseEvent.which !== 1) return const { intersectionPoint } = args - if (!intersectionPoint?.twoD || !sketchDetails?.sketchPathToNode) return + 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 + } // Snap to either or both axes // if the click intersects their meshes @@ -447,27 +501,34 @@ export class SceneEntities { y: xAxisIntersection ? 0 : intersectionPoint.twoD.y, } - const addStartProfileAtRes = addStartProfileAt( + const inserted = insertNewStartProfileAt( kclManager.ast, - sketchDetails.sketchPathToNode, - [snappedClickPoint.x, snappedClickPoint.y] + sketchDetails.sketchEntryNodePath, + sketchDetails.sketchNodePaths, + sketchDetails.planeNodePath, + [snappedClickPoint.x, snappedClickPoint.y], + 'end' ) - if (trap(addStartProfileAtRes)) return - const { modifiedAst } = addStartProfileAtRes + if (trap(inserted)) return + const { modifiedAst } = inserted await kclManager.updateAst(modifiedAst, false) this.scene.remove(draftPointGroup) // Now perform the caller-specified action - afterClick(args) + afterClick(args, { + sketchNodePaths: inserted.updatedSketchNodePaths, + sketchEntryNodePath: inserted.updatedEntryNodePath, + }) }, }) } async setupSketch({ - sketchPathToNode, + sketchEntryNodePath, + sketchNodePaths, forward, up, position, @@ -475,7 +536,8 @@ export class SceneEntities { draftExpressionsIndices, selectionRanges, }: { - sketchPathToNode: PathToNode + sketchEntryNodePath: PathToNode + sketchNodePaths: PathToNode[] maybeModdedAst: Node draftExpressionsIndices?: { start: number; end: number } forward: [number, number, number] @@ -485,11 +547,12 @@ export class SceneEntities { }): Promise<{ truncatedAst: Node programMemoryOverride: ProgramMemory - sketch: Sketch variableDeclarationName: string }> { + this.createIntersectionPlane() + const prepared = this.prepareTruncatedMemoryAndAst( - sketchPathToNode || [], + sketchNodePaths, maybeModdedAst ) if (err(prepared)) return Promise.reject(prepared) @@ -503,139 +566,144 @@ export class SceneEntities { programMemoryOverride, }) const programMemory = execState.memory - const sketch = sketchFromPathToNode({ - pathToNode: sketchPathToNode, + const sketchesInfo = getSketchesInfo({ + sketchNodePaths, 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: sketchPathToNode, + pathToNode: sketchEntryNodePath, } 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)[] = [] - sketch.paths.forEach((segment, index) => { - let segPathToNode = getNodePathFromSourceRange( + + for (const sketchInfo of sketchesInfo) { + const { sketch } = sketchInfo + const segPathToNode = getNodePathFromSourceRange( maybeModdedAst, - sourceRangeFromRust(segment.__geoMeta.sourceRange) + sourceRangeFromRust(sketch.start.__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 + 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 } - const isDraftSegment = - draftExpressionsIndices && - index <= draftExpressionsIndices.end && - index >= draftExpressionsIndices.start - const isSelected = selectionRanges?.graphSelections.some((selection) => - isOverlap( - selection?.codeRef?.range, + sketch.paths.forEach((segment, index) => { + const isLastInProfile = + index === sketch.paths.length - 1 && segment.type !== 'Circle' + let segPathToNode = getNodePathFromSourceRange( + maybeModdedAst, sourceRangeFromRust(segment.__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) + ) + ) - let seg: Group - const _node1 = getNodeFromPath( - maybeModdedAst, - segPathToNode, - 'CallExpression' - ) - if (err(_node1)) return - const callExpName = _node1.node?.callee?.name + 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, + 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 }) - 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), @@ -656,22 +724,25 @@ export class SceneEntities { return { truncatedAst, programMemoryOverride, - sketch, variableDeclarationName, } } updateAstAndRejigSketch = async ( - sketchPathToNode: PathToNode, - modifiedAst: Node, + sketchEntryNodePath: PathToNode, + sketchNodePaths: PathToNode[], + planeNodePath: PathToNode, + modifiedAst: Node | Error, 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) - await this.tearDownSketch({ removeAxis: false }) + this.tearDownSketch({ removeAxis: false }) sceneInfra.resetMouseListeners() await this.setupSketch({ - sketchPathToNode, + sketchEntryNodePath, + sketchNodePaths, forward, up, position: origin, @@ -681,12 +752,16 @@ export class SceneEntities { forward, up, position: origin, - pathToNode: sketchPathToNode, + sketchEntryNodePath, + sketchNodePaths, + planeNodePath, }) return nextAst } setupDraftSegment = async ( - sketchPathToNode: PathToNode, + sketchEntryNodePath: PathToNode, + sketchNodePaths: PathToNode[], + planeNodePath: PathToNode, forward: [number, number, number], up: [number, number, number], origin: [number, number, number], @@ -697,7 +772,7 @@ export class SceneEntities { const _node1 = getNodeFromPath( _ast, - sketchPathToNode || [], + sketchEntryNodePath || [], 'VariableDeclaration' ) if (trap(_node1)) return Promise.reject(_node1) @@ -720,7 +795,7 @@ export class SceneEntities { from: lastSeg.to, }, fnName: segmentName, - pathToNode: sketchPathToNode, + pathToNode: sketchEntryNodePath, }) if (trap(mod)) return Promise.reject(mod) const pResult = parse(recast(mod.modifiedAst)) @@ -729,18 +804,18 @@ export class SceneEntities { const draftExpressionsIndices = { start: index, end: index } - if (shouldTearDown) await this.tearDownSketch({ removeAxis: false }) + if (shouldTearDown) this.tearDownSketch({ removeAxis: false }) sceneInfra.resetMouseListeners() - const { truncatedAst, programMemoryOverride, sketch } = - await this.setupSketch({ - sketchPathToNode, - forward, - up, - position: origin, - maybeModdedAst: modifiedAst, - draftExpressionsIndices, - }) + const { truncatedAst, programMemoryOverride } = await this.setupSketch({ + sketchEntryNodePath, + sketchNodePaths, + forward, + up, + position: origin, + maybeModdedAst: modifiedAst, + draftExpressionsIndices, + }) sceneInfra.setCallbacks({ onClick: async (args) => { if (!args) return @@ -757,7 +832,15 @@ export class SceneEntities { .map(({ object }) => getParentGroup(object, [PROFILE_START])) .find((a) => a?.name === PROFILE_START) - let modifiedAst + 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')) // Snapping logic for the profile start handle if (intersectsProfileStart) { @@ -765,7 +848,7 @@ export class SceneEntities { modifiedAst = addCallExpressionsToPipe({ node: kclManager.ast, programMemory: kclManager.programMemory, - pathToNode: sketchPathToNode, + pathToNode: sketchEntryNodePath, expressions: [ createCallExpressionStdLib( lastSegment.type === 'TangentialArcTo' @@ -789,7 +872,7 @@ export class SceneEntities { modifiedAst = addCloseToPipe({ node: modifiedAst, programMemory: kclManager.programMemory, - pathToNode: sketchPathToNode, + pathToNode: sketchEntryNodePath, }) if (trap(modifiedAst)) return Promise.reject(modifiedAst) } else if (intersection2d) { @@ -823,7 +906,11 @@ 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') { + if ( + (lastSegment.type === 'TangentialArcTo' && + segmentName !== 'line') || + segmentName === 'tangentialArcTo' + ) { resolvedFunctionName = 'tangentialArcTo' } else if (isHorizontal) { // If the angle between is 0 or 180 degrees (+/- the snapping angle), make the line an xLine @@ -845,7 +932,7 @@ export class SceneEntities { to: [snappedPoint.x, snappedPoint.y], }, fnName: resolvedFunctionName, - pathToNode: sketchPathToNode, + pathToNode: sketchEntryNodePath, }) if (trap(tmp)) return Promise.reject(tmp) modifiedAst = tmp.modifiedAst @@ -858,10 +945,12 @@ export class SceneEntities { await kclManager.executeAstMock(modifiedAst) if (intersectsProfileStart) { - sceneInfra.modelingSend({ type: 'CancelSketch' }) + sceneInfra.modelingSend({ type: 'Close sketch' }) } else { await this.setupDraftSegment( - sketchPathToNode, + sketchEntryNodePath, + sketchNodePaths, + planeNodePath, forward, up, origin, @@ -872,11 +961,23 @@ 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.values(this.activeSegments).slice(-1)[0], + object, intersects: args.intersects, - sketchPathToNode, + sketchNodePaths, + sketchEntryNodePath, + planeNodePath, draftInfo: { truncatedAst, programMemoryOverride, @@ -887,41 +988,82 @@ export class SceneEntities { }) } setupDraftRectangle = async ( - sketchPathToNode: PathToNode, + sketchEntryNodePath: PathToNode, + sketchNodePaths: PathToNode[], + planeNodePath: 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 _node1 = getNodeFromPath( + const varDec = getNodeFromPath( _ast, - sketchPathToNode || [], - 'VariableDeclaration' + planeNodePath, + 'VariableDeclarator' ) - if (trap(_node1)) return Promise.reject(_node1) - const variableDeclarationName = _node1.node?.declaration.id?.name || '' - const startSketchOn = _node1.node?.declaration - const startSketchOnInit = startSketchOn?.init - const tags: [string, string, string] = [ - findUniqueName(_ast, 'rectangleSegmentA'), - findUniqueName(_ast, 'rectangleSegmentB'), - findUniqueName(_ast, 'rectangleSegmentC'), - ] + if (err(varDec)) return varDec + if (varDec.node.type !== 'VariableDeclarator') return new Error('not a var') - startSketchOn.init = createPipeExpression([ - startSketchOnInit, - ...getRectangleCallExpressions(rectangleOrigin, tags), - ]) + 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), + ]) + ) + + const insertIndex = getInsertIndex(sketchNodePaths, planeNodePath, 'end') + + _ast.body.splice(insertIndex, 0, newDeclaration) + const { updatedEntryNodePath, updatedSketchNodePaths } = + updateSketchNodePathsWithInsertIndex({ + insertIndex, + insertType: 'end', + sketchNodePaths, + }) 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({ - sketchPathToNode, + sketchEntryNodePath: updatedEntryNodePath, + sketchNodePaths: updatedSketchNodePaths, forward, up, position: sketchOrigin, @@ -932,12 +1074,17 @@ export class SceneEntities { sceneInfra.setCallbacks({ onMove: async (args) => { // Update the width and height of the draft rectangle - const pathToNodeTwo = structuredClone(sketchPathToNode) - pathToNodeTwo[1][0] = 0 + + const nodePathWithCorrectedIndexForTruncatedAst = + structuredClone(updatedEntryNodePath) + nodePathWithCorrectedIndexForTruncatedAst[1][0] = + Number(nodePathWithCorrectedIndexForTruncatedAst[1][0]) - + Number(planeNodePath[1][0]) - + 1 const _node = getNodeFromPath( truncatedAst, - pathToNodeTwo || [], + nodePathWithCorrectedIndexForTruncatedAst, 'VariableDeclaration' ) if (trap(_node)) return Promise.reject(_node) @@ -947,7 +1094,7 @@ export class SceneEntities { const y = (args.intersectionPoint.twoD.y || 0) - rectangleOrigin[1] if (sketchInit.type === 'PipeExpression') { - updateRectangleSketch(sketchInit, x, y, tags[0]) + updateRectangleSketch(sketchInit, x, y, tag) } const { execState } = await executeAst({ @@ -958,17 +1105,23 @@ export class SceneEntities { }) const programMemory = execState.memory this.sceneProgramMemory = programMemory - const sketch = sketchFromKclValue( - programMemory.get(variableDeclarationName), - variableDeclarationName - ) + const sketch = sketchFromKclValue(programMemory.get(varName), varName) if (err(sketch)) return Promise.reject(sketch) const sgPaths = sketch.paths const orthoFactor = orthoScale(sceneInfra.camControls.camera) - this.updateSegment(sketch.start, 0, 0, _ast, orthoFactor, sketch) + const varDecIndex = Number(updatedEntryNodePath[1][0]) + + this.updateSegment( + sketch.start, + 0, + varDecIndex, + _ast, + orthoFactor, + sketch + ) sgPaths.forEach((seg, index) => - this.updateSegment(seg, index, 0, _ast, orthoFactor, sketch) + this.updateSegment(seg, index, varDecIndex, _ast, orthoFactor, sketch) ) }, onClick: async (args) => { @@ -986,7 +1139,7 @@ export class SceneEntities { const _node = getNodeFromPath( _ast, - sketchPathToNode || [], + updatedEntryNodePath, 'VariableDeclaration' ) if (trap(_node)) return @@ -996,7 +1149,7 @@ export class SceneEntities { return } - updateRectangleSketch(sketchInit, x, y, tags[0]) + updateRectangleSketch(sketchInit, x, y, tag) const newCode = recast(_ast) const pResult = parse(newCode) @@ -1006,77 +1159,92 @@ 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) - 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) - ) + sceneInfra.modelingSend({ type: 'Finish rectangle' }) }, }) + return { updatedEntryNodePath, updatedSketchNodePaths } } setupDraftCenterRectangle = async ( - sketchPathToNode: PathToNode, + sketchEntryNodePath: PathToNode, + sketchNodePaths: PathToNode[], + planeNodePath: 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 _node1 = getNodeFromPath( + + const varDec = getNodeFromPath( _ast, - sketchPathToNode || [], + 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), + ]) + ) + 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 + + // 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(_node1)) return Promise.reject(_node1) - // startSketchOn already exists - const variableDeclarationName = _node1.node?.declaration.id?.name || '' - const startSketchOn = _node1.node?.declaration - const startSketchOnInit = startSketchOn?.init - - const tags: [string, string, string] = [ - findUniqueName(_ast, 'rectangleSegmentA'), - findUniqueName(_ast, 'rectangleSegmentB'), - findUniqueName(_ast, 'rectangleSegmentC'), - ] - - startSketchOn.init = createPipeExpression([ - startSketchOnInit, - ...getRectangleCallExpressions(rectangleOrigin, tags), + 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 pResult = parse(recast(_ast)) - if (trap(pResult) || !resultIsOk(pResult)) return Promise.reject(pResult) - _ast = pResult.program + const code = recast(_ast) + __recastAst = parse(code) + if (trap(__recastAst) || !resultIsOk(__recastAst)) + return Promise.reject(__recastAst) + _ast = __recastAst.program const { programMemoryOverride, truncatedAst } = await this.setupSketch({ - sketchPathToNode, + sketchEntryNodePath: updatedEntryNodePath, + sketchNodePaths: updatedSketchNodePaths, forward, up, position: sketchOrigin, @@ -1087,12 +1255,17 @@ export class SceneEntities { sceneInfra.setCallbacks({ onMove: async (args) => { // Update the width and height of the draft rectangle - const pathToNodeTwo = structuredClone(sketchPathToNode) - pathToNodeTwo[1][0] = 0 + + const nodePathWithCorrectedIndexForTruncatedAst = + structuredClone(updatedEntryNodePath) + nodePathWithCorrectedIndexForTruncatedAst[1][0] = + Number(nodePathWithCorrectedIndexForTruncatedAst[1][0]) - + Number(planeNodePath[1][0]) - + 1 const _node = getNodeFromPath( truncatedAst, - pathToNodeTwo || [], + nodePathWithCorrectedIndexForTruncatedAst, 'VariableDeclaration' ) if (trap(_node)) return Promise.reject(_node) @@ -1106,7 +1279,7 @@ export class SceneEntities { sketchInit, x, y, - tags[0], + tag, rectangleOrigin[0], rectangleOrigin[1] ) @@ -1120,17 +1293,23 @@ export class SceneEntities { }) const programMemory = execState.memory this.sceneProgramMemory = programMemory - const sketch = sketchFromKclValue( - programMemory.get(variableDeclarationName), - variableDeclarationName - ) + const sketch = sketchFromKclValue(programMemory.get(varName), varName) if (err(sketch)) return Promise.reject(sketch) const sgPaths = sketch.paths const orthoFactor = orthoScale(sceneInfra.camControls.camera) - this.updateSegment(sketch.start, 0, 0, _ast, orthoFactor, sketch) + const varDecIndex = Number(updatedEntryNodePath[1][0]) + + this.updateSegment( + sketch.start, + 0, + varDecIndex, + _ast, + orthoFactor, + sketch + ) sgPaths.forEach((seg, index) => - this.updateSegment(seg, index, 0, _ast, orthoFactor, sketch) + this.updateSegment(seg, index, varDecIndex, _ast, orthoFactor, sketch) ) }, onClick: async (args) => { @@ -1148,7 +1327,7 @@ export class SceneEntities { const _node = getNodeFromPath( _ast, - sketchPathToNode || [], + updatedEntryNodePath, 'VariableDeclaration' ) if (trap(_node)) return @@ -1159,7 +1338,7 @@ export class SceneEntities { sketchInit, x, y, - tags[0], + tag, rectangleOrigin[0], rectangleOrigin[1] ) @@ -1171,62 +1350,41 @@ 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) - 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) - ) + sceneInfra.modelingSend({ type: 'Finish center rectangle' }) } }, }) + return { updatedEntryNodePath, updatedSketchNodePaths } } setupDraftCircle = async ( - sketchPathToNode: PathToNode, + sketchEntryNodePath: PathToNode, + sketchNodePaths: PathToNode[], + planeNodePath: 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 _node1 = getNodeFromPath( + const varDec = getNodeFromPath( _ast, - sketchPathToNode || [], - 'VariableDeclaration' + planeNodePath, + 'VariableDeclarator' ) - if (trap(_node1)) return Promise.reject(_node1) - const variableDeclarationName = _node1.node?.declaration.id?.name || '' - const startSketchOn = _node1.node?.declaration - const startSketchOnInit = startSketchOn?.init - startSketchOn.init = createPipeExpression([ - startSketchOnInit, + 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, createCallExpressionStdLib('circle', [ createObjectExpression({ center: createArrayExpression([ @@ -1235,9 +1393,19 @@ export class SceneEntities { ]), radius: createLiteral(1), }), - createPipeSubstitution(), - ]), - ]) + 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, + }) const pResult = parse(recast(_ast)) if (trap(pResult) || !resultIsOk(pResult)) return Promise.reject(pResult) @@ -1247,7 +1415,8 @@ export class SceneEntities { await kclManager.executeAstMock(_ast) const { programMemoryOverride, truncatedAst } = await this.setupSketch({ - sketchPathToNode, + sketchEntryNodePath: updatedEntryNodePath, + sketchNodePaths: updatedSketchNodePaths, forward, up, position: sketchOrigin, @@ -1257,12 +1426,15 @@ export class SceneEntities { sceneInfra.setCallbacks({ onMove: async (args) => { - const pathToNodeTwo = structuredClone(sketchPathToNode) - pathToNodeTwo[1][0] = 0 - + const nodePathWithCorrectedIndexForTruncatedAst = + structuredClone(updatedEntryNodePath) + nodePathWithCorrectedIndexForTruncatedAst[1][0] = + Number(nodePathWithCorrectedIndexForTruncatedAst[1][0]) - + Number(planeNodePath[1][0]) - + 1 const _node = getNodeFromPath( truncatedAst, - pathToNodeTwo || [], + nodePathWithCorrectedIndexForTruncatedAst, 'VariableDeclaration' ) let modded = structuredClone(truncatedAst) @@ -1272,17 +1444,13 @@ export class SceneEntities { const x = (args.intersectionPoint.twoD.x || 0) - circleCenter[0] const y = (args.intersectionPoint.twoD.y || 0) - circleCenter[1] - if (sketchInit.type === 'PipeExpression') { + if (sketchInit.type === 'CallExpression') { const moddedResult = changeSketchArguments( modded, kclManager.programMemory, { type: 'path', - pathToNode: [ - ..._node.deepPath, - ['body', 'PipeExpression'], - [1, 'index'], - ], + pathToNode: nodePathWithCorrectedIndexForTruncatedAst, }, { type: 'arc-segment', @@ -1303,17 +1471,23 @@ export class SceneEntities { }) const programMemory = execState.memory this.sceneProgramMemory = programMemory - const sketch = sketchFromKclValue( - programMemory.get(variableDeclarationName), - variableDeclarationName - ) + const sketch = sketchFromKclValue(programMemory.get(varName), varName) if (err(sketch)) return const sgPaths = sketch.paths const orthoFactor = orthoScale(sceneInfra.camControls.camera) - this.updateSegment(sketch.start, 0, 0, _ast, orthoFactor, sketch) + const varDecIndex = Number(updatedEntryNodePath[1][0]) + + this.updateSegment( + sketch.start, + 0, + varDecIndex, + _ast, + orthoFactor, + sketch + ) sgPaths.forEach((seg, index) => - this.updateSegment(seg, index, 0, _ast, orthoFactor, sketch) + this.updateSegment(seg, index, varDecIndex, _ast, orthoFactor, sketch) ) }, onClick: async (args) => { @@ -1331,24 +1505,20 @@ export class SceneEntities { const _node = getNodeFromPath( _ast, - sketchPathToNode || [], + updatedEntryNodePath || [], 'VariableDeclaration' ) if (trap(_node)) return const sketchInit = _node.node?.declaration.init let modded = structuredClone(_ast) - if (sketchInit.type === 'PipeExpression') { + if (sketchInit.type === 'CallExpression') { const moddedResult = changeSketchArguments( modded, kclManager.programMemory, { type: 'path', - pathToNode: [ - ..._node.deepPath, - ['body', 'PipeExpression'], - [1, 'index'], - ], + pathToNode: updatedEntryNodePath, }, { type: 'arc-segment', @@ -1369,20 +1539,25 @@ export class SceneEntities { // Update the primary AST and unequip the rectangle tool await kclManager.executeAstMock(_ast) - sceneInfra.modelingSend({ type: 'Finish circle' }) - await codeManager.updateEditorWithAstAndWriteToFile(_ast) + + sceneInfra.modelingSend({ type: 'Finish circle' }) } }, }) + return { updatedEntryNodePath, updatedSketchNodePaths } } setupSketchIdleCallbacks = ({ - pathToNode, + sketchEntryNodePath, + sketchNodePaths, + planeNodePath, up, forward, position, }: { - pathToNode: PathToNode + sketchEntryNodePath: PathToNode + sketchNodePaths: PathToNode[] + planeNodePath: PathToNode forward: [number, number, number] up: [number, number, number] position?: [number, number, number] @@ -1391,10 +1566,11 @@ export class SceneEntities { sceneInfra.setCallbacks({ onDragEnd: async () => { if (addingNewSegmentStatus !== 'nothing') { - await this.tearDownSketch({ removeAxis: false }) + this.tearDownSketch({ removeAxis: false }) // eslint-disable-next-line @typescript-eslint/no-floating-promises this.setupSketch({ - sketchPathToNode: pathToNode, + sketchEntryNodePath, + sketchNodePaths, maybeModdedAst: kclManager.ast, up, forward, @@ -1402,7 +1578,9 @@ export class SceneEntities { }) // setting up the callbacks again resets value in closures this.setupSketchIdleCallbacks({ - pathToNode, + sketchEntryNodePath, + sketchNodePaths, + planeNodePath, up, forward, position, @@ -1459,10 +1637,11 @@ export class SceneEntities { if (trap(mod)) return await kclManager.executeAstMock(mod.modifiedAst) - await this.tearDownSketch({ removeAxis: false }) + this.tearDownSketch({ removeAxis: false }) // eslint-disable-next-line @typescript-eslint/no-floating-promises this.setupSketch({ - sketchPathToNode: pathToNode, + sketchEntryNodePath: pathToNode, + sketchNodePaths, maybeModdedAst: kclManager.ast, up, forward, @@ -1473,7 +1652,9 @@ export class SceneEntities { const pathToNodeForNewSegment = pathToNode.slice(0, pathToNodeIndex) pathToNodeForNewSegment.push([pipeIndex - 2, 'index']) this.onDragSegment({ - sketchPathToNode: pathToNodeForNewSegment, + sketchNodePaths, + sketchEntryNodePath: pathToNodeForNewSegment, + planeNodePath, object: selected, intersection2d: intersectionPoint.twoD, intersects, @@ -1485,8 +1666,10 @@ export class SceneEntities { this.onDragSegment({ object: selected, intersection2d: intersectionPoint.twoD, + planeNodePath, intersects, - sketchPathToNode: pathToNode, + sketchNodePaths, + sketchEntryNodePath, }) }, onMove: () => {}, @@ -1515,12 +1698,12 @@ export class SceneEntities { }) } prepareTruncatedMemoryAndAst = ( - sketchPathToNode: PathToNode, + sketchNodePaths: PathToNode[], ast?: Node, draftSegment?: DraftSegment ) => prepareTruncatedMemoryAndAst( - sketchPathToNode, + sketchNodePaths, ast || kclManager.ast, kclManager.lastSuccessfulProgramMemory, draftSegment @@ -1528,13 +1711,17 @@ export class SceneEntities { onDragSegment({ object, intersection2d: _intersection2d, - sketchPathToNode, + sketchEntryNodePath, + sketchNodePaths, + planeNodePath, draftInfo, intersects, }: { object: any intersection2d: Vector2 - sketchPathToNode: PathToNode + sketchEntryNodePath: PathToNode + sketchNodePaths: PathToNode[] + planeNodePath: PathToNode intersects: Intersection>[] draftInfo?: { truncatedAst: Node @@ -1577,9 +1764,6 @@ export class SceneEntities { ) return } - if (draftInfo) { - pathToNode[1][0] = 0 - } const from: [number, number] = [ group.userData.from[0], @@ -1588,9 +1772,15 @@ 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, - pathToNode, + draftInfo ? nodePathWithCorrectedIndexForTruncatedAst : pathToNode, 'CallExpression' ) if (trap(_node)) return @@ -1668,10 +1858,9 @@ export class SceneEntities { modifiedAst = modded.modifiedAst const info = draftInfo ? draftInfo - : this.prepareTruncatedMemoryAndAst(pathToNode || []) + : this.prepareTruncatedMemoryAndAst(sketchNodePaths || [], modifiedAst) if (trap(info, { suppress: true })) return - const { truncatedAst, programMemoryOverride, variableDeclarationName } = - info + const { truncatedAst, programMemoryOverride } = info ;(async () => { const code = recast(modifiedAst) if (trap(code)) return @@ -1687,42 +1876,43 @@ 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]) - 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 + 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( - group, - index, + sketch.start, + 0, varDecIndex, modifiedAst, orthoFactor, sketch ) - ) + + callBacks.push( + ...sgPaths.map((group, index) => + this.updateSegment( + group, + index, + varDecIndex, + modifiedAst, + orthoFactor, + sketch + ) + ) + ) + } sceneInfra.overlayCallbacks(callBacks) })().catch(reportRejection) } @@ -1757,7 +1947,6 @@ 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 @@ -1828,12 +2017,7 @@ export class SceneEntities { removeSketchGrid() { if (this.axisGroup) this.scene.remove(this.axisGroup) } - private _tearDownSketch( - callDepth = 0, - resolve: (val: unknown) => void, - reject: () => void, - { removeAxis = true }: { removeAxis?: boolean } - ) { + tearDownSketch({ removeAxis = true }: { removeAxis?: boolean }) { // Remove all draft groups this.draftPointGroups.forEach((draftPointGroup) => { this.scene.remove(draftPointGroup) @@ -1842,7 +2026,6 @@ 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 @@ -1853,36 +2036,9 @@ 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 { @@ -2036,7 +2192,7 @@ export type DefaultPlaneStr = 'XY' | 'XZ' | 'YZ' | '-XY' | '-XZ' | '-YZ' // calculations/pure-functions/easy to test so no excuse not to function prepareTruncatedMemoryAndAst( - sketchPathToNode: PathToNode, + sketchNodePaths: PathToNode[], ast: Node, programMemory: ProgramMemory, draftSegment?: DraftSegment @@ -2044,15 +2200,19 @@ function prepareTruncatedMemoryAndAst( | { truncatedAst: Node programMemoryOverride: ProgramMemory + // can I remove the below? variableDeclarationName: string } | Error { - const bodyIndex = Number(sketchPathToNode?.[1]?.[0]) || 0 + const bodyStartIndex = Number(sketchNodePaths?.[0]?.[1]?.[0]) || 0 + const bodyEndIndex = + Number(sketchNodePaths[sketchNodePaths.length - 1]?.[1]?.[0]) || + ast.body.length const _ast = structuredClone(ast) const _node = getNodeFromPath>( _ast, - sketchPathToNode || [], + sketchNodePaths[0] || [], 'VariableDeclaration' ) if (err(_node)) return _node @@ -2081,7 +2241,7 @@ function prepareTruncatedMemoryAndAst( ]) } ;( - (_ast.body[bodyIndex] as VariableDeclaration).declaration + (_ast.body[bodyStartIndex] as VariableDeclaration).declaration .init as PipeExpression ).body.push(newSegment) // update source ranges to section we just added. @@ -2092,17 +2252,17 @@ function prepareTruncatedMemoryAndAst( const updatedSrcRangeAst = pResult.program const lastPipeItem = ( - (updatedSrcRangeAst.body[bodyIndex] as VariableDeclaration).declaration - .init as PipeExpression + (updatedSrcRangeAst.body[bodyStartIndex] as VariableDeclaration) + .declaration.init as PipeExpression ).body.slice(-1)[0] ;( - (_ast.body[bodyIndex] as VariableDeclaration).declaration + (_ast.body[bodyStartIndex] as VariableDeclaration).declaration .init as PipeExpression ).body.slice(-1)[0].start = lastPipeItem.start _ast.end = lastPipeItem.end - const varDec = _ast.body[bodyIndex] as Node + const varDec = _ast.body[bodyStartIndex] as Node varDec.end = lastPipeItem.end const declarator = varDec.declaration declarator.end = lastPipeItem.end @@ -2112,7 +2272,7 @@ function prepareTruncatedMemoryAndAst( } const truncatedAst: Node = { ..._ast, - body: [structuredClone(_ast.body[bodyIndex])], + body: structuredClone(_ast.body.slice(bodyStartIndex, bodyEndIndex + 1)), } // Grab all the TagDeclarators and TagIdentifiers from memory. @@ -2136,7 +2296,7 @@ function prepareTruncatedMemoryAndAst( }) if (err(programMemoryOverride)) return programMemoryOverride - for (let i = 0; i < bodyIndex; i++) { + for (let i = 0; i < bodyStartIndex; i++) { const node = _ast.body[i] if (node.type !== 'VariableDeclaration') { continue @@ -2232,13 +2392,16 @@ export function getSketchQuaternion( return getQuaternionFromZAxis(massageFormats(zAxis)) } export async function getSketchOrientationDetails( - sketchPathToNode: PathToNode + sketchEntryNodePath: PathToNode ): Promise<{ quat: Quaternion - sketchDetails: SketchDetails & { faceId?: string } + sketchDetails: Omit< + SketchDetails & { faceId?: string }, + 'sketchNodePaths' | 'sketchEntryNodePath' | 'planeNodePath' + > }> { const sketch = sketchFromPathToNode({ - pathToNode: sketchPathToNode, + pathToNode: sketchEntryNodePath, ast: kclManager.ast, programMemory: kclManager.programMemory, }) @@ -2250,7 +2413,6 @@ 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], @@ -2272,7 +2434,6 @@ 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], @@ -2351,3 +2512,35 @@ 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 f2e47e09a..c68fc58fb 100644 --- a/src/clientSideScene/segments.ts +++ b/src/clientSideScene/segments.ts @@ -691,19 +691,21 @@ export function createProfileStartHandle({ scale = 1, theme, isSelected, + size = 12, ...rest }: { from: Coords2d scale?: number theme: Themes isSelected?: boolean + size?: number } & ( | { isDraft: true } | { isDraft: false; id: string; pathToNode: PathToNode } )) { const group = new Group() - const geometry = new BoxGeometry(12, 12, 12) // in pixels scaled later + const geometry = new BoxGeometry(size, size, size) // in pixels scaled later const baseColor = getThemeColorForThreeJs(theme) const color = isSelected ? 0x0000ff : baseColor const body = new MeshBasicMaterial({ color }) diff --git a/src/components/ModelingMachineProvider.tsx b/src/components/ModelingMachineProvider.tsx index faf2c0ac9..b9b99bed5 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, - updatePathToNodeFromMap, + updateSketchDetailsNodePaths, } from 'lang/util' import { kclManager, @@ -71,14 +71,24 @@ import { replaceValueAtNodePath, sketchOnExtrudedFace, sketchOnOffsetPlane, + splitPipedProfile, startSketchOnDefault, } from 'lang/modifyAst' -import { PathToNode, Program, parse, recast, resultIsOk } from 'lang/wasm' +import { + PathToNode, + Program, + VariableDeclaration, + parse, + recast, + resultIsOk, +} from 'lang/wasm' import { doesSceneHaveExtrudedSketch, doesSceneHaveSweepableSketch, - getNodePathFromSourceRange, - isSingleCursorInPipe, + doesSketchPipeNeedSplitting, + getNodeFromPath, + isCursorInFunctionDefinition, + traverse, } from 'lang/queryAst' import { exportFromEngine } from 'lib/exportFromEngine' import { Models } from '@kittycad/lib/dist/types/src' @@ -86,7 +96,7 @@ import toast from 'react-hot-toast' import { EditorSelection, Transaction } from '@codemirror/state' import { useLoaderData, useNavigate, useSearchParams } from 'react-router-dom' import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls' -import { err, reportRejection, trap } from 'lib/trap' +import { err, reportRejection, trap, reject } from 'lib/trap' import { useCommandsContext } from 'hooks/useCommandsContext' import { modelingMachineEvent } from 'editor/manager' import { hasValidEdgeTreatmentSelection } from 'lang/modifyAst/addEdgeTreatment' @@ -100,6 +110,10 @@ import { useFileContext } from 'hooks/useFileContext' import { uuidv4 } from 'lib/utils' import { IndexLoaderData } from 'lib/types' import { Node } from 'wasm-lib/kcl/bindings/Node' +import { + getPathsFromArtifact, + getPlaneFromArtifact, +} from 'lang/std/artifactGraph' type MachineContext = { state: StateFrom @@ -290,7 +304,7 @@ export const ModelingMachineProvider = ({ return { sketchDetails: { ...sketchDetails, - sketchPathToNode: event.data, + sketchEntryNodePath: event.data, }, } }), @@ -413,9 +427,17 @@ export const ModelingMachineProvider = ({ selectionRanges: setSelections.selection, sketchDetails: { ...sketchDetails, - sketchPathToNode: - setSelections.updatedPathToNode || - sketchDetails?.sketchPathToNode || + sketchEntryNodePath: + setSelections.updatedSketchEntryNodePath || + sketchDetails?.sketchEntryNodePath || + [], + sketchNodePaths: + setSelections.updatedSketchNodePaths || + sketchDetails?.sketchNodePaths || + [], + planeNodePath: + setSelections.updatedPlaneNodePath || + sketchDetails?.planeNodePath || [], }, } @@ -647,7 +669,12 @@ export const ModelingMachineProvider = ({ 'Selection is on face': ({ context: { selectionRanges }, event }) => { if (event.type !== 'Enter sketch') return false if (event.data?.forceNewSketch) return false - if (!isSingleCursorInPipe(selectionRanges, kclManager.ast)) + if ( + isCursorInFunctionDefinition( + kclManager.ast, + selectionRanges.graphSelections[0] + ) + ) return false return !!isCursorInSketchCommandRange( engineCommandManager.artifactGraph, @@ -678,10 +705,32 @@ export const ModelingMachineProvider = ({ // this assumes no changes have been made to the sketch besides what we did when entering the sketch // i.e. doesn't account for user's adding code themselves, maybe we need store a flag userEditedSinceSketchMode? const newAst = structuredClone(kclManager.ast) - const varDecIndex = sketchDetails.sketchPathToNode[1][0] + const varDecIndex = sketchDetails.planeNodePath[1][0] + + const varDec = getNodeFromPath( + newAst, + sketchDetails.planeNodePath, + 'VariableDeclaration' + ) + if (err(varDec)) return reject(new Error('No varDec')) + const variableName = varDec.node.declaration.id.name + let isIdentifierUsed = false + traverse(newAst, { + enter: (node) => { + if ( + node.type === 'Identifier' && + node.name === variableName + ) { + isIdentifierUsed = true + } + }, + }) + if (isIdentifierUsed) return + // remove body item at varDecIndex newAst.body = newAst.body.filter((_, i) => i !== varDecIndex) await kclManager.executeAstMock(newAst) + await codeManager.updateEditorWithAstAndWriteToFile(newAst) } sceneInfra.setCallbacks({ onClick: () => {}, @@ -691,7 +740,7 @@ export const ModelingMachineProvider = ({ } ), 'animate-to-face': fromPromise(async ({ input }) => { - if (!input) return undefined + if (!input) return null if (input.type === 'extrudeFace' || input.type === 'offsetPlane') { const sketched = input.type === 'extrudeFace' @@ -718,7 +767,9 @@ export const ModelingMachineProvider = ({ await letEngineAnimateAndSyncCamAfter(engineCommandManager, id) sceneInfra.camControls.syncDirection = 'clientToEngine' return { - sketchPathToNode: pathToNewSketchNode, + sketchEntryNodePath: [], + planeNodePath: pathToNewSketchNode, + sketchNodePaths: [], zAxis: input.zAxis, yAxis: input.yAxis, origin: input.position, @@ -738,7 +789,9 @@ export const ModelingMachineProvider = ({ ) return { - sketchPathToNode: pathToNode, + sketchEntryNodePath: [], + planeNodePath: pathToNode, + sketchNodePaths: [], zAxis: input.zAxis, yAxis: input.yAxis, origin: [0, 0, 0], @@ -746,12 +799,14 @@ export const ModelingMachineProvider = ({ }), 'animate-to-sketch': fromPromise( async ({ input: { selectionRanges } }) => { - const sourceRange = - selectionRanges.graphSelections[0]?.codeRef?.range - const sketchPathToNode = getNodePathFromSourceRange( - kclManager.ast, - sourceRange + const sketchPathToNode = + selectionRanges.graphSelections[0]?.codeRef?.pathToNode + const plane = getPlaneFromArtifact( + selectionRanges.graphSelections[0].artifact, + engineCommandManager.artifactGraph ) + if (err(plane)) return Promise.reject(plane) + const info = await getSketchOrientationDetails( sketchPathToNode || [] ) @@ -759,8 +814,17 @@ export const ModelingMachineProvider = ({ engineCommandManager, info?.sketchDetails?.faceId || '' ) - return { + const sketchPaths = getPathsFromArtifact({ + artifact: selectionRanges.graphSelections[0].artifact, sketchPathToNode: sketchPathToNode || [], + }) + if (err(sketchPaths)) return Promise.reject(sketchPaths) + if (!plane.codeRef) + return Promise.reject(new Error('No plane codeRef')) + return { + sketchEntryNodePath: sketchPathToNode || [], + sketchNodePaths: sketchPaths, + planeNodePath: plane.codeRef.pathToNode, zAxis: info.sketchDetails.zAxis || null, yAxis: info.sketchDetails.yAxis || null, origin: info.sketchDetails.origin.map( @@ -772,7 +836,7 @@ export const ModelingMachineProvider = ({ 'Get horizontal info': fromPromise( async ({ input: { selectionRanges, sketchDetails } }) => { - const { modifiedAst, pathToNodeMap } = + const { modifiedAst, pathToNodeMap, exprInsertIndex } = await applyConstraintHorzVertDistance({ constraint: 'setHorzDistance', selectionRanges, @@ -784,13 +848,23 @@ export const ModelingMachineProvider = ({ if (!sketchDetails) return Promise.reject(new Error('No sketch details')) - const updatedPathToNode = updatePathToNodeFromMap( - sketchDetails.sketchPathToNode, - pathToNodeMap - ) + + const { + updatedSketchEntryNodePath, + updatedSketchNodePaths, + updatedPlaneNodePath, + } = updateSketchDetailsNodePaths({ + sketchEntryNodePath: sketchDetails.sketchEntryNodePath, + sketchNodePaths: sketchDetails.sketchNodePaths, + planeNodePath: sketchDetails.planeNodePath, + exprInsertIndex, + }) + const updatedAst = await sceneEntitiesManager.updateAstAndRejigSketch( - updatedPathToNode, + updatedSketchEntryNodePath, + updatedSketchNodePaths, + updatedPlaneNodePath, _modifiedAst, sketchDetails.zAxis, sketchDetails.yAxis, @@ -811,13 +885,15 @@ export const ModelingMachineProvider = ({ return { selectionType: 'completeSelection', selection, - updatedPathToNode, + updatedSketchEntryNodePath, + updatedSketchNodePaths, + updatedPlaneNodePath, } } ), 'Get vertical info': fromPromise( async ({ input: { selectionRanges, sketchDetails } }) => { - const { modifiedAst, pathToNodeMap } = + const { modifiedAst, pathToNodeMap, exprInsertIndex } = await applyConstraintHorzVertDistance({ constraint: 'setVertDistance', selectionRanges, @@ -828,13 +904,23 @@ export const ModelingMachineProvider = ({ const _modifiedAst = pResult.program if (!sketchDetails) return Promise.reject(new Error('No sketch details')) - const updatedPathToNode = updatePathToNodeFromMap( - sketchDetails.sketchPathToNode, - pathToNodeMap - ) + + const { + updatedSketchEntryNodePath, + updatedSketchNodePaths, + updatedPlaneNodePath, + } = updateSketchDetailsNodePaths({ + sketchEntryNodePath: sketchDetails.sketchEntryNodePath, + sketchNodePaths: sketchDetails.sketchNodePaths, + planeNodePath: sketchDetails.planeNodePath, + exprInsertIndex, + }) + const updatedAst = await sceneEntitiesManager.updateAstAndRejigSketch( - updatedPathToNode, + updatedSketchEntryNodePath, + updatedSketchNodePaths, + updatedPlaneNodePath, _modifiedAst, sketchDetails.zAxis, sketchDetails.yAxis, @@ -855,7 +941,9 @@ export const ModelingMachineProvider = ({ return { selectionType: 'completeSelection', selection, - updatedPathToNode, + updatedSketchEntryNodePath, + updatedSketchNodePaths, + updatedPlaneNodePath, } } ), @@ -865,14 +953,15 @@ export const ModelingMachineProvider = ({ selectionRanges, }) if (err(info)) return Promise.reject(info) - const { modifiedAst, pathToNodeMap } = await (info.enabled - ? applyConstraintAngleBetween({ - selectionRanges, - }) - : applyConstraintAngleLength({ - selectionRanges, - angleOrLength: 'setAngle', - })) + const { modifiedAst, pathToNodeMap, exprInsertIndex } = + await (info.enabled + ? applyConstraintAngleBetween({ + selectionRanges, + }) + : applyConstraintAngleLength({ + selectionRanges, + angleOrLength: 'setAngle', + })) const pResult = parse(recast(modifiedAst)) if (trap(pResult) || !resultIsOk(pResult)) return Promise.reject(new Error('Unexpected compilation error')) @@ -881,13 +970,23 @@ export const ModelingMachineProvider = ({ if (!sketchDetails) return Promise.reject(new Error('No sketch details')) - const updatedPathToNode = updatePathToNodeFromMap( - sketchDetails.sketchPathToNode, - pathToNodeMap - ) + + const { + updatedSketchEntryNodePath, + updatedSketchNodePaths, + updatedPlaneNodePath, + } = updateSketchDetailsNodePaths({ + sketchEntryNodePath: sketchDetails.sketchEntryNodePath, + sketchNodePaths: sketchDetails.sketchNodePaths, + planeNodePath: sketchDetails.planeNodePath, + exprInsertIndex, + }) + const updatedAst = await sceneEntitiesManager.updateAstAndRejigSketch( - updatedPathToNode, + updatedSketchEntryNodePath, + updatedSketchNodePaths, + updatedPlaneNodePath, _modifiedAst, sketchDetails.zAxis, sketchDetails.yAxis, @@ -908,7 +1007,9 @@ export const ModelingMachineProvider = ({ return { selectionType: 'completeSelection', selection, - updatedPathToNode, + updatedSketchEntryNodePath, + updatedSketchNodePaths, + updatedPlaneNodePath, } } ), @@ -923,20 +1024,30 @@ export const ModelingMachineProvider = ({ length: lengthValue, }) if (err(constraintResult)) return Promise.reject(constraintResult) - const { modifiedAst, pathToNodeMap } = constraintResult + const { modifiedAst, pathToNodeMap, exprInsertIndex } = + constraintResult const pResult = parse(recast(modifiedAst)) if (trap(pResult) || !resultIsOk(pResult)) return Promise.reject(new Error('Unexpected compilation error')) const _modifiedAst = pResult.program if (!sketchDetails) return Promise.reject(new Error('No sketch details')) - const updatedPathToNode = updatePathToNodeFromMap( - sketchDetails.sketchPathToNode, - pathToNodeMap - ) + + const { + updatedSketchEntryNodePath, + updatedSketchNodePaths, + updatedPlaneNodePath, + } = updateSketchDetailsNodePaths({ + sketchEntryNodePath: sketchDetails.sketchEntryNodePath, + sketchNodePaths: sketchDetails.sketchNodePaths, + planeNodePath: sketchDetails.planeNodePath, + exprInsertIndex, + }) const updatedAst = await sceneEntitiesManager.updateAstAndRejigSketch( - updatedPathToNode, + updatedSketchEntryNodePath, + updatedSketchNodePaths, + updatedPlaneNodePath, _modifiedAst, sketchDetails.zAxis, sketchDetails.yAxis, @@ -957,13 +1068,15 @@ export const ModelingMachineProvider = ({ return { selectionType: 'completeSelection', selection, - updatedPathToNode, + updatedSketchEntryNodePath, + updatedSketchNodePaths, + updatedPlaneNodePath, } } ), 'Get perpendicular distance info': fromPromise( async ({ input: { selectionRanges, sketchDetails } }) => { - const { modifiedAst, pathToNodeMap } = + const { modifiedAst, pathToNodeMap, exprInsertIndex } = await applyConstraintIntersect({ selectionRanges, }) @@ -973,13 +1086,22 @@ export const ModelingMachineProvider = ({ const _modifiedAst = pResult.program if (!sketchDetails) return Promise.reject(new Error('No sketch details')) - const updatedPathToNode = updatePathToNodeFromMap( - sketchDetails.sketchPathToNode, - pathToNodeMap - ) + + const { + updatedSketchEntryNodePath, + updatedSketchNodePaths, + updatedPlaneNodePath, + } = updateSketchDetailsNodePaths({ + sketchEntryNodePath: sketchDetails.sketchEntryNodePath, + sketchNodePaths: sketchDetails.sketchNodePaths, + planeNodePath: sketchDetails.planeNodePath, + exprInsertIndex, + }) const updatedAst = await sceneEntitiesManager.updateAstAndRejigSketch( - updatedPathToNode, + updatedSketchEntryNodePath, + updatedSketchNodePaths, + updatedPlaneNodePath, _modifiedAst, sketchDetails.zAxis, sketchDetails.yAxis, @@ -1000,13 +1122,15 @@ export const ModelingMachineProvider = ({ return { selectionType: 'completeSelection', selection, - updatedPathToNode, + updatedSketchEntryNodePath, + updatedSketchNodePaths, + updatedPlaneNodePath, } } ), 'Get ABS X info': fromPromise( async ({ input: { selectionRanges, sketchDetails } }) => { - const { modifiedAst, pathToNodeMap } = + const { modifiedAst, pathToNodeMap, exprInsertIndex } = await applyConstraintAbsDistance({ constraint: 'xAbs', selectionRanges, @@ -1017,13 +1141,22 @@ export const ModelingMachineProvider = ({ const _modifiedAst = pResult.program if (!sketchDetails) return Promise.reject(new Error('No sketch details')) - const updatedPathToNode = updatePathToNodeFromMap( - sketchDetails.sketchPathToNode, - pathToNodeMap - ) + + const { + updatedSketchEntryNodePath, + updatedSketchNodePaths, + updatedPlaneNodePath, + } = updateSketchDetailsNodePaths({ + sketchEntryNodePath: sketchDetails.sketchEntryNodePath, + sketchNodePaths: sketchDetails.sketchNodePaths, + planeNodePath: sketchDetails.planeNodePath, + exprInsertIndex, + }) const updatedAst = await sceneEntitiesManager.updateAstAndRejigSketch( - updatedPathToNode, + updatedSketchEntryNodePath, + updatedSketchNodePaths, + updatedPlaneNodePath, _modifiedAst, sketchDetails.zAxis, sketchDetails.yAxis, @@ -1044,13 +1177,15 @@ export const ModelingMachineProvider = ({ return { selectionType: 'completeSelection', selection, - updatedPathToNode, + updatedSketchEntryNodePath, + updatedSketchNodePaths, + updatedPlaneNodePath, } } ), 'Get ABS Y info': fromPromise( async ({ input: { selectionRanges, sketchDetails } }) => { - const { modifiedAst, pathToNodeMap } = + const { modifiedAst, pathToNodeMap, exprInsertIndex } = await applyConstraintAbsDistance({ constraint: 'yAbs', selectionRanges, @@ -1061,13 +1196,22 @@ export const ModelingMachineProvider = ({ const _modifiedAst = pResult.program if (!sketchDetails) return Promise.reject(new Error('No sketch details')) - const updatedPathToNode = updatePathToNodeFromMap( - sketchDetails.sketchPathToNode, - pathToNodeMap - ) + + const { + updatedSketchEntryNodePath, + updatedSketchNodePaths, + updatedPlaneNodePath, + } = updateSketchDetailsNodePaths({ + sketchEntryNodePath: sketchDetails.sketchEntryNodePath, + sketchNodePaths: sketchDetails.sketchNodePaths, + planeNodePath: sketchDetails.planeNodePath, + exprInsertIndex, + }) const updatedAst = await sceneEntitiesManager.updateAstAndRejigSketch( - updatedPathToNode, + updatedSketchEntryNodePath, + updatedSketchNodePaths, + updatedPlaneNodePath, _modifiedAst, sketchDetails.zAxis, sketchDetails.yAxis, @@ -1088,7 +1232,9 @@ export const ModelingMachineProvider = ({ return { selectionType: 'completeSelection', selection, - updatedPathToNode, + updatedSketchEntryNodePath, + updatedSketchNodePaths, + updatedPlaneNodePath, } } ), @@ -1108,9 +1254,11 @@ export const ModelingMachineProvider = ({ let result: { modifiedAst: Node pathToReplaced: PathToNode | null + exprInsertIndex: number } = { modifiedAst: parsed, pathToReplaced: null, + exprInsertIndex: -1, } // If the user provided a constant name, // we need to insert the named constant @@ -1140,6 +1288,7 @@ export const ModelingMachineProvider = ({ result = { modifiedAst: parseResultAfterInsertion.program, pathToReplaced: astAfterReplacement.pathToReplaced, + exprInsertIndex: astAfterReplacement.exprInsertIndex, } } else if ('valueText' in data.namedValue) { // If they didn't provide a constant name, @@ -1170,10 +1319,22 @@ export const ModelingMachineProvider = ({ parsed = parsed as Node if (!result.pathToReplaced) return Promise.reject(new Error('No path to replaced node')) + const { + updatedSketchEntryNodePath, + updatedSketchNodePaths, + updatedPlaneNodePath, + } = updateSketchDetailsNodePaths({ + sketchEntryNodePath: sketchDetails.sketchEntryNodePath, + sketchNodePaths: sketchDetails.sketchNodePaths, + planeNodePath: sketchDetails.planeNodePath, + exprInsertIndex: result.exprInsertIndex, + }) const updatedAst = await sceneEntitiesManager.updateAstAndRejigSketch( - result.pathToReplaced || [], + updatedSketchEntryNodePath, + updatedSketchNodePaths, + updatedPlaneNodePath, parsed, sketchDetails.zAxis, sketchDetails.yAxis, @@ -1194,7 +1355,140 @@ export const ModelingMachineProvider = ({ return { selectionType: 'completeSelection', selection, - updatedPathToNode: result.pathToReplaced, + updatedSketchEntryNodePath, + updatedSketchNodePaths, + updatedPlaneNodePath, + } + } + ), + 'set-up-draft-circle': fromPromise( + async ({ input: { sketchDetails, data } }) => { + if (!sketchDetails || !data) + return reject('No sketch details or data') + await sceneEntitiesManager.tearDownSketch({ removeAxis: false }) + + const result = await sceneEntitiesManager.setupDraftCircle( + sketchDetails.sketchEntryNodePath, + sketchDetails.sketchNodePaths, + sketchDetails.planeNodePath, + sketchDetails.zAxis, + sketchDetails.yAxis, + sketchDetails.origin, + data + ) + if (err(result)) return reject(result) + await codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast) + + return result + } + ), + 'set-up-draft-rectangle': fromPromise( + async ({ input: { sketchDetails, data } }) => { + if (!sketchDetails || !data) + return reject('No sketch details or data') + await sceneEntitiesManager.tearDownSketch({ removeAxis: false }) + + const result = await sceneEntitiesManager.setupDraftRectangle( + sketchDetails.sketchEntryNodePath, + sketchDetails.sketchNodePaths, + sketchDetails.planeNodePath, + sketchDetails.zAxis, + sketchDetails.yAxis, + sketchDetails.origin, + data + ) + if (err(result)) return reject(result) + await codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast) + + return result + } + ), + 'set-up-draft-center-rectangle': fromPromise( + async ({ input: { sketchDetails, data } }) => { + if (!sketchDetails || !data) + return reject('No sketch details or data') + await sceneEntitiesManager.tearDownSketch({ removeAxis: false }) + const result = await sceneEntitiesManager.setupDraftCenterRectangle( + sketchDetails.sketchEntryNodePath, + sketchDetails.sketchNodePaths, + sketchDetails.planeNodePath, + sketchDetails.zAxis, + sketchDetails.yAxis, + sketchDetails.origin, + data + ) + if (err(result)) return reject(result) + await codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast) + + return result + } + ), + 'setup-client-side-sketch-segments': fromPromise( + async ({ input: { sketchDetails, selectionRanges } }) => { + if (!sketchDetails) return + if (!sketchDetails.sketchEntryNodePath.length) return + if (Object.keys(sceneEntitiesManager.activeSegments).length > 0) { + sceneEntitiesManager.tearDownSketch({ removeAxis: false }) + } + sceneInfra.resetMouseListeners() + await sceneEntitiesManager.setupSketch({ + sketchEntryNodePath: sketchDetails?.sketchEntryNodePath || [], + sketchNodePaths: sketchDetails.sketchNodePaths, + forward: sketchDetails.zAxis, + up: sketchDetails.yAxis, + position: sketchDetails.origin, + maybeModdedAst: kclManager.ast, + selectionRanges, + }) + sceneInfra.resetMouseListeners() + + sceneEntitiesManager.setupSketchIdleCallbacks({ + sketchEntryNodePath: sketchDetails?.sketchEntryNodePath || [], + forward: sketchDetails.zAxis, + up: sketchDetails.yAxis, + position: sketchDetails.origin, + sketchNodePaths: sketchDetails.sketchNodePaths, + planeNodePath: sketchDetails.planeNodePath, + }) + return undefined + } + ), + 'split-sketch-pipe-if-needed': fromPromise( + async ({ input: { sketchDetails } }) => { + if (!sketchDetails) return reject('No sketch details') + const existingSketchInfoNoOp = { + updatedEntryNodePath: sketchDetails.sketchEntryNodePath, + updatedSketchNodePaths: sketchDetails.sketchNodePaths, + updatedPlaneNodePath: sketchDetails.planeNodePath, + } as const + if ( + !sketchDetails.sketchNodePaths.length && + sketchDetails.planeNodePath.length + ) { + // new sketch, no profiles yet + return existingSketchInfoNoOp + } + const doesNeedSplitting = doesSketchPipeNeedSplitting( + kclManager.ast, + sketchDetails.sketchEntryNodePath + ) + if (err(doesNeedSplitting)) return reject(doesNeedSplitting) + if (!doesNeedSplitting) return existingSketchInfoNoOp + + const splitResult = splitPipedProfile( + kclManager.ast, + sketchDetails.sketchEntryNodePath + ) + if (err(splitResult)) return reject(splitResult) + + await kclManager.executeAstMock(splitResult.modifiedAst) + await codeManager.updateEditorWithAstAndWriteToFile( + splitResult.modifiedAst + ) + return { + updatedEntryNodePath: splitResult.pathToProfile, + updatedSketchNodePaths: [splitResult.pathToProfile], + updatedPlaneNodePath: sketchDetails.planeNodePath, } } ), diff --git a/src/components/Spinner.tsx b/src/components/Spinner.tsx index dea0c20fd..63a5b356f 100644 --- a/src/components/Spinner.tsx +++ b/src/components/Spinner.tsx @@ -2,7 +2,12 @@ import { SVGProps } from 'react' export const Spinner = (props: SVGProps) => { return ( - + pathToNodeMap: PathToNodeMap + exprInsertIndex: number }> { const info = intersectInfo({ selectionRanges, @@ -174,6 +175,7 @@ export async function applyConstraintIntersect({ return { modifiedAst, pathToNodeMap, + exprInsertIndex: -1, } } // transform again but forcing certain values @@ -192,6 +194,7 @@ export async function applyConstraintIntersect({ const { modifiedAst: _modifiedAst, pathToNodeMap: _pathToNodeMap } = transform2 + let exprInsertIndex = -1 if (variableName) { const newBody = [..._modifiedAst.body] newBody.splice( @@ -204,9 +207,11 @@ export async function applyConstraintIntersect({ const index = pathToNode.findIndex((a) => a[0] === 'body') + 1 pathToNode[index][0] = Number(pathToNode[index][0]) + 1 }) + exprInsertIndex = newVariableInsertIndex } return { modifiedAst: _modifiedAst, pathToNodeMap: _pathToNodeMap, + exprInsertIndex, } } diff --git a/src/components/Toolbar/RemoveConstrainingValues.tsx b/src/components/Toolbar/RemoveConstrainingValues.tsx index e4fe8eb15..0100a6baf 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 (err(tmp)) return tmp + if (tmp instanceof Error) 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 f1b9652d6..03f8838bd 100644 --- a/src/components/Toolbar/SetAbsDistance.tsx +++ b/src/components/Toolbar/SetAbsDistance.tsx @@ -93,6 +93,7 @@ export async function applyConstraintAbsDistance({ }): Promise<{ modifiedAst: Program pathToNodeMap: PathToNodeMap + exprInsertIndex: number }> { const info = absDistanceInfo({ selectionRanges, @@ -132,6 +133,7 @@ export async function applyConstraintAbsDistance({ if (err(transform2)) return Promise.reject(transform2) const { modifiedAst: _modifiedAst, pathToNodeMap } = transform2 + let exprInsertIndex = -1 if (variableName) { const newBody = [..._modifiedAst.body] newBody.splice( @@ -144,8 +146,9 @@ export async function applyConstraintAbsDistance({ const index = pathToNode.findIndex((a) => a[0] === 'body') + 1 pathToNode[index][0] = Number(pathToNode[index][0]) + 1 }) + exprInsertIndex = newVariableInsertIndex } - return { modifiedAst: _modifiedAst, pathToNodeMap } + return { modifiedAst: _modifiedAst, pathToNodeMap, exprInsertIndex } } export function applyConstraintAxisAlign({ diff --git a/src/components/Toolbar/SetAngleBetween.tsx b/src/components/Toolbar/SetAngleBetween.tsx index 14a0fe72a..dc91d39c3 100644 --- a/src/components/Toolbar/SetAngleBetween.tsx +++ b/src/components/Toolbar/SetAngleBetween.tsx @@ -86,6 +86,7 @@ export async function applyConstraintAngleBetween({ }): Promise<{ modifiedAst: Program pathToNodeMap: PathToNodeMap + exprInsertIndex: number }> { const info = angleBetweenInfo({ selectionRanges }) if (err(info)) return Promise.reject(info) @@ -122,6 +123,7 @@ export async function applyConstraintAngleBetween({ return { modifiedAst, pathToNodeMap, + exprInsertIndex: -1, } } @@ -141,6 +143,7 @@ export async function applyConstraintAngleBetween({ const { modifiedAst: _modifiedAst, pathToNodeMap: _pathToNodeMap } = transformed2 + let exprInsertIndex = -1 if (variableName) { const newBody = [..._modifiedAst.body] newBody.splice( @@ -153,9 +156,11 @@ export async function applyConstraintAngleBetween({ const index = pathToNode.findIndex((a) => a[0] === 'body') + 1 pathToNode[index][0] = Number(pathToNode[index][0]) + 1 }) + exprInsertIndex = newVariableInsertIndex } return { modifiedAst: _modifiedAst, pathToNodeMap: _pathToNodeMap, + exprInsertIndex, } } diff --git a/src/components/Toolbar/SetHorzVertDistance.tsx b/src/components/Toolbar/SetHorzVertDistance.tsx index 172ebfa79..b96d9fc38 100644 --- a/src/components/Toolbar/SetHorzVertDistance.tsx +++ b/src/components/Toolbar/SetHorzVertDistance.tsx @@ -87,15 +87,13 @@ export function horzVertDistanceInfo({ export async function applyConstraintHorzVertDistance({ selectionRanges, constraint, - // TODO align will always be false (covered by synconous applyConstraintHorzVertAlign), remove it - isAlign = false, }: { selectionRanges: Selections constraint: 'setHorzDistance' | 'setVertDistance' - isAlign?: false }): Promise<{ modifiedAst: Program pathToNodeMap: PathToNodeMap + exprInsertIndex: number }> { const info = horzVertDistanceInfo({ selectionRanges: selectionRanges, @@ -133,13 +131,12 @@ export async function applyConstraintHorzVertDistance({ return { modifiedAst, pathToNodeMap, + exprInsertIndex: -1, } } else { if (!isExprBinaryPart(valueNode)) return Promise.reject('Invalid valueNode, is not a BinaryPart') - let finalValue = isAlign - ? createLiteral(0) - : removeDoubleNegatives(valueNode, sign, variableName) + let finalValue = removeDoubleNegatives(valueNode, sign, variableName) // transform again but forcing certain values const transformed = transformSecondarySketchLinesTagFirst({ ast: kclManager.ast, @@ -152,6 +149,7 @@ export async function applyConstraintHorzVertDistance({ if (err(transformed)) return Promise.reject(transformed) const { modifiedAst: _modifiedAst, pathToNodeMap } = transformed + let exprInsertIndex = -1 if (variableName) { const newBody = [..._modifiedAst.body] newBody.splice( @@ -164,10 +162,12 @@ export async function applyConstraintHorzVertDistance({ const index = pathToNode.findIndex((a) => a[0] === 'body') + 1 pathToNode[index][0] = Number(pathToNode[index][0]) + 1 }) + exprInsertIndex = newVariableInsertIndex } return { modifiedAst: _modifiedAst, pathToNodeMap, + exprInsertIndex, } } } diff --git a/src/components/Toolbar/setAngleLength.tsx b/src/components/Toolbar/setAngleLength.tsx index 5453ef684..6ea63e53b 100644 --- a/src/components/Toolbar/setAngleLength.tsx +++ b/src/components/Toolbar/setAngleLength.tsx @@ -70,10 +70,14 @@ export async function applyConstraintLength({ }: { length: KclCommandValue selectionRanges: Selections -}) { +}): Promise<{ + modifiedAst: Program + pathToNodeMap: PathToNodeMap + exprInsertIndex: number +}> { const ast = kclManager.ast const angleLength = angleLengthInfo({ selectionRanges }) - if (err(angleLength)) return angleLength + if (err(angleLength)) return Promise.reject(angleLength) const { transforms } = angleLength let distanceExpression: Expr = length.valueAst @@ -94,7 +98,7 @@ export async function applyConstraintLength({ } if (!isExprBinaryPart(distanceExpression)) { - return new Error('Invalid valueNode, is not a BinaryPart') + return Promise.reject('Invalid valueNode, is not a BinaryPart') } const retval = transformAstSketchLines({ @@ -112,6 +116,12 @@ export async function applyConstraintLength({ return { modifiedAst: _modifiedAst, pathToNodeMap, + exprInsertIndex: + 'variableName' in length && + length.variableName && + length.insertIndex !== undefined + ? length.insertIndex + : -1, } } @@ -124,6 +134,7 @@ export async function applyConstraintAngleLength({ }): Promise<{ modifiedAst: Program pathToNodeMap: PathToNodeMap + exprInsertIndex: number }> { const angleLength = angleLengthInfo({ selectionRanges, angleOrLength }) if (err(angleLength)) return Promise.reject(angleLength) @@ -208,5 +219,6 @@ export async function applyConstraintAngleLength({ return { modifiedAst: _modifiedAst, pathToNodeMap, + exprInsertIndex: variableName ? newVariableInsertIndex : -1, } } diff --git a/src/lang/KclSingleton.ts b/src/lang/KclSingleton.ts index 1688d4cbe..e1222ccae 100644 --- a/src/lang/KclSingleton.ts +++ b/src/lang/KclSingleton.ts @@ -359,10 +359,8 @@ export class KclManager { // updateArtifactGraph relies on updated executeState/programMemory await this.engineCommandManager.updateArtifactGraph(this.ast) this._executeCallback() - if (!isInterrupted) { + if (!isInterrupted) sceneInfra.modelingSend({ type: 'code edit during sketch' }) - } - this.engineCommandManager.addCommandLog({ type: 'execution-done', data: null, @@ -404,6 +402,7 @@ export class KclManager { this._logs = logs this.addDiagnostics(kclErrorsToDiagnostics(errors)) + this._execState = execState this._programMemory = execState.memory if (!errors.length) { @@ -415,7 +414,7 @@ export class KclManager { // problem this solves, but either way we should strive to remove it. Array.from(this.engineCommandManager.artifactGraph).forEach( ([commandId, artifact]) => { - if (!('codeRef' in artifact)) return + if (!('codeRef' in artifact && artifact.codeRef)) return const _node1 = getNodeFromPath>( this.ast, artifact.codeRef.pathToNode, diff --git a/src/lang/langHelpers.ts b/src/lang/langHelpers.ts index 64920e7ae..d8f12f1ad 100644 --- a/src/lang/langHelpers.ts +++ b/src/lang/langHelpers.ts @@ -1,6 +1,6 @@ import { Program, - _executor, + executor, ProgramMemory, kclLint, emptyExecState, @@ -64,10 +64,9 @@ export async function executeAst({ try { const execState = await (programMemoryOverride ? enginelessExecutor(ast, programMemoryOverride) - : _executor(ast, engineCommandManager)) + : executor(ast, engineCommandManager)) await engineCommandManager.waitForAllCommands() - return { logs: [], errors: [], diff --git a/src/lang/modifyAst.test.ts b/src/lang/modifyAst.test.ts index 261e8875c..aa067e9dd 100644 --- a/src/lang/modifyAst.test.ts +++ b/src/lang/modifyAst.test.ts @@ -16,6 +16,7 @@ import { deleteSegmentFromPipeExpression, removeSingleConstraintInfo, deleteFromSelection, + splitPipedProfile, } from './modifyAst' import { enginelessExecutor } from '../lib/testHelpers' import { findUsesOfTagInPipe, getNodePathFromSourceRange } from './queryAst' @@ -918,3 +919,63 @@ sketch002 = startSketchOn({ } ) }) + +describe('Testing splitPipedProfile', () => { + it('should split the pipe expression correctly', () => { + const codeBefore = `part001 = startSketchOn('XZ') + |> startProfileAt([1, 2], %) + |> line([3, 4], %) + |> line([5, 6], %) + |> close(%) +extrude001 = extrude(5, part001) + ` + + const expectedCodeAfter = `sketch001 = startSketchOn('XZ') +part001 = startProfileAt([1, 2], sketch001) + |> line([3, 4], %) + |> line([5, 6], %) + |> close(%) +extrude001 = extrude(5, part001) + ` + + const ast = assertParse(codeBefore) + + const codeOfInterest = `startSketchOn('XZ')` + const range: [number, number, boolean] = [ + codeBefore.indexOf(codeOfInterest), + codeBefore.indexOf(codeOfInterest) + codeOfInterest.length, + true, + ] + const pathToPipe = getNodePathFromSourceRange(ast, range) + + const result = splitPipedProfile(ast, pathToPipe) + + if (err(result)) throw result + + const newCode = recast(result.modifiedAst) + if (err(newCode)) throw newCode + expect(newCode.trim()).toBe(expectedCodeAfter.trim()) + }) + it('should return error for already split pipe', () => { + const codeBefore = `sketch001 = startSketchOn('XZ') +part001 = startProfileAt([1, 2], sketch001) + |> line([3, 4], %) + |> line([5, 6], %) + |> close(%) +extrude001 = extrude(5, part001) + ` + + const ast = assertParse(codeBefore) + + const codeOfInterest = `startProfileAt([1, 2], sketch001)` + const range: [number, number, boolean] = [ + codeBefore.indexOf(codeOfInterest), + codeBefore.indexOf(codeOfInterest) + codeOfInterest.length, + true, + ] + const pathToPipe = getNodePathFromSourceRange(ast, range) + + const result = splitPipedProfile(ast, pathToPipe) + expect(result instanceof Error).toBe(true) + }) +}) diff --git a/src/lang/modifyAst.ts b/src/lang/modifyAst.ts index 6a1ae6345..e770073cf 100644 --- a/src/lang/modifyAst.ts +++ b/src/lang/modifyAst.ts @@ -29,6 +29,8 @@ import { getNodePathFromSourceRange, isNodeSafeToReplace, traverse, + getBodyIndex, + isCallExprWithName, } from './queryAst' import { addTagForSketchOnFace, getConstraintInfo } from './std/sketch' import { @@ -46,6 +48,7 @@ import { Models } from '@kittycad/lib' import { ExtrudeFacePlane } from 'machines/modelingMachine' import { Node } from 'wasm-lib/kcl/bindings/Node' import { KclExpressionWithVariable } from 'lib/commandTypes' +import { Artifact, getPathsFromArtifact } from './std/artifactGraph' export function startSketchOnDefault( node: Node, @@ -78,41 +81,54 @@ export function startSketchOnDefault( } } -export function addStartProfileAt( +export function insertNewStartProfileAt( node: Node, - pathToNode: PathToNode, - at: [number, number] -): { modifiedAst: Node; pathToNode: PathToNode } | Error { - const _node1 = getNodeFromPath( + sketchEntryNodePath: PathToNode, + sketchNodePaths: PathToNode[], + planeNodePath: PathToNode, + at: [number, number], + insertType: 'start' | 'end' = 'end' +): + | { + modifiedAst: Node + updatedSketchNodePaths: PathToNode[] + updatedEntryNodePath: PathToNode + } + | Error { + const varDec = getNodeFromPath( node, - pathToNode, - 'VariableDeclaration' + planeNodePath, + 'VariableDeclarator' ) - if (err(_node1)) return _node1 - const variableDeclaration = _node1.node - if (variableDeclaration.type !== 'VariableDeclaration') { - return new Error('variableDeclaration.init.type !== PipeExpression') - } - const _node = { ...node } - const init = variableDeclaration.declaration.init - const startProfileAt = createCallExpressionStdLib('startProfileAt', [ - createArrayExpression([ - createLiteral(roundOff(at[0])), - createLiteral(roundOff(at[1])), - ]), - createPipeSubstitution(), - ]) - if (init.type === 'PipeExpression') { - init.body.splice(1, 0, startProfileAt) - } else { - variableDeclaration.declaration.init = createPipeExpression([ - init, - startProfileAt, + if (err(varDec)) return varDec + if (varDec.node.type !== 'VariableDeclarator') return new Error('not a var') + + const newExpression = createVariableDeclaration( + findUniqueName(node, 'profile'), + createCallExpressionStdLib('startProfileAt', [ + createArrayExpression([ + createLiteral(roundOff(at[0])), + createLiteral(roundOff(at[1])), + ]), + createIdentifier(varDec.node.id.name), ]) - } + ) + const insertIndex = getInsertIndex(sketchNodePaths, planeNodePath, insertType) + + const _node = structuredClone(node) + // TODO the rest of this function will not be robust to work for sketches defined within a function declaration + _node.body.splice(insertIndex, 0, newExpression) + + const { updatedEntryNodePath, updatedSketchNodePaths } = + updateSketchNodePathsWithInsertIndex({ + insertIndex, + insertType, + sketchNodePaths, + }) return { modifiedAst: _node, - pathToNode, + updatedSketchNodePaths, + updatedEntryNodePath, } } @@ -253,7 +269,7 @@ export function mutateObjExpProp( export function extrudeSketch( node: Node, pathToNode: PathToNode, - shouldPipe = false, + artifact?: Artifact, distance: Expr = createLiteral(4) ): | { @@ -262,10 +278,14 @@ export function extrudeSketch( pathToExtrudeArg: PathToNode } | Error { + const orderedSketchNodePaths = getPathsFromArtifact({ + artifact: artifact, + sketchPathToNode: pathToNode, + }) + if (err(orderedSketchNodePaths)) return orderedSketchNodePaths const _node = structuredClone(node) const _node1 = getNodeFromPath(_node, pathToNode) if (err(_node1)) return _node1 - const { node: sketchExpression } = _node1 // determine if sketchExpression is in a pipeExpression or not const _node2 = getNodeFromPath( @@ -274,9 +294,6 @@ export function extrudeSketch( 'PipeExpression' ) if (err(_node2)) return _node2 - const { node: pipeExpression } = _node2 - - const isInPipeExpression = pipeExpression.type === 'PipeExpression' const _node3 = getNodeFromPath( _node, @@ -284,49 +301,23 @@ export function extrudeSketch( 'VariableDeclarator' ) if (err(_node3)) return _node3 - const { node: variableDeclarator, shallowPath: pathToDecleration } = _node3 + const { node: variableDeclarator } = _node3 const extrudeCall = createCallExpressionStdLib('extrude', [ distance, - shouldPipe - ? createPipeSubstitution() - : createIdentifier(variableDeclarator.id.name), + createIdentifier(variableDeclarator.id.name), ]) - if (shouldPipe) { - const pipeChain = createPipeExpression( - isInPipeExpression - ? [...pipeExpression.body, extrudeCall] - : [sketchExpression as any, extrudeCall] - ) - - variableDeclarator.init = pipeChain - const pathToExtrudeArg: PathToNode = [ - ...pathToDecleration, - ['init', 'VariableDeclarator'], - ['body', ''], - [pipeChain.body.length - 1, 'index'], - ['arguments', 'CallExpression'], - [0, 'index'], - ] - - return { - modifiedAst: _node, - pathToNode, - pathToExtrudeArg, - } - } - // We're not creating a pipe expression, // but rather a separate constant for the extrusion const name = findUniqueName(node, KCL_DEFAULT_CONSTANT_PREFIXES.EXTRUDE) const VariableDeclaration = createVariableDeclaration(name, extrudeCall) - const sketchIndexInPathToNode = - pathToDecleration.findIndex((a) => a[0] === 'body') + 1 - const sketchIndexInBody = pathToDecleration[ - sketchIndexInPathToNode - ][0] as number + const lastSketchNodePath = + orderedSketchNodePaths[orderedSketchNodePaths.length - 1] + + console.log('lastSketchNodePath', lastSketchNodePath, orderedSketchNodePaths) + const sketchIndexInBody = Number(lastSketchNodePath[1][0]) _node.body.splice(sketchIndexInBody + 1, 0, VariableDeclaration) const pathToExtrudeArg: PathToNode = [ @@ -1295,7 +1286,8 @@ export async function deleteFromSelection( const pipeBody = varDec.node.init.body if ( pipeBody[0].type === 'CallExpression' && - pipeBody[0].callee.name === 'startSketchOn' + (pipeBody[0].callee.name === 'startSketchOn' || + pipeBody[0].callee.name === 'startProfileAt') ) { // remove varDec const varDecIndex = varDec.shallowPath[1][0] as number @@ -1310,3 +1302,149 @@ export async function deleteFromSelection( const nonCodeMetaEmpty = () => { return { nonCodeNodes: {}, startNodes: [], start: 0, end: 0 } } + +export function getInsertIndex( + sketchNodePaths: PathToNode[], + planeNodePath: PathToNode, + insertType: 'start' | 'end' +) { + let minIndex = 0 + let maxIndex = 0 + for (const path of sketchNodePaths) { + const index = Number(path[1][0]) + if (index < minIndex) minIndex = index + if (index > maxIndex) maxIndex = index + } + + const insertIndex = !sketchNodePaths.length + ? Number(planeNodePath[1][0]) + 1 + : insertType === 'start' + ? minIndex + : maxIndex + 1 + return insertIndex +} + +export function updateSketchNodePathsWithInsertIndex({ + insertIndex, + insertType, + sketchNodePaths, +}: { + insertIndex: number + insertType: 'start' | 'end' + sketchNodePaths: PathToNode[] +}): { + updatedEntryNodePath: PathToNode + updatedSketchNodePaths: PathToNode[] +} { + // TODO the rest of this function will not be robust to work for sketches defined within a function declaration + const newExpressionPathToNode: PathToNode = [ + ['body', ''], + [insertIndex, 'index'], + ['declaration', 'VariableDeclaration'], + ['init', 'VariableDeclarator'], + ] + let updatedSketchNodePaths = structuredClone(sketchNodePaths) + if (insertType === 'start') { + updatedSketchNodePaths = updatedSketchNodePaths.map((path) => { + path[1][0] = Number(path[1][0]) + 1 + return path + }) + updatedSketchNodePaths.unshift(newExpressionPathToNode) + } else { + updatedSketchNodePaths.push(newExpressionPathToNode) + } + return { + updatedSketchNodePaths, + updatedEntryNodePath: newExpressionPathToNode, + } +} + +/** + * + * Split the following pipe expression into + * ```ts + * part001 = startSketchOn('XZ') + |> startProfileAt([1, 2], %) + |> line([3, 4], %) + |> line([5, 6], %) + |> close(%) +extrude001 = extrude(5, part001) +``` +into +```ts +sketch001 = startSketchOn('XZ') +part001 = startProfileAt([1, 2], sketch001) + |> line([3, 4], %) + |> line([5, 6], %) + |> close(%) +extrude001 = extrude(5, part001) +``` +Notice that the `startSketchOn` is what gets the new variable name, this is so part001 still has the same data as before +making it safe for later code that uses part001 (the extrude in this example) + * + */ +export function splitPipedProfile( + ast: Program, + pathToPipe: PathToNode +): + | { + modifiedAst: Program + pathToProfile: PathToNode + pathToPlane: PathToNode + } + | Error { + const _ast = structuredClone(ast) + const varDec = getNodeFromPath( + _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 930599229..7f00e250f 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)) return false + if (!('codeRef' in a && a.codeRef)) return false return isOverlap(a.codeRef.range, segmentRange) }) return { diff --git a/src/lang/modifyAst/addRevolve.ts b/src/lang/modifyAst/addRevolve.ts index d9af1917a..769135f2c 100644 --- a/src/lang/modifyAst/addRevolve.ts +++ b/src/lang/modifyAst/addRevolve.ts @@ -5,7 +5,6 @@ import { PathToNode, Expr, CallExpression, - PipeExpression, VariableDeclarator, } from 'lang/wasm' import { Selections } from 'lib/selections' @@ -15,7 +14,6 @@ import { createCallExpressionStdLib, createObjectExpression, createIdentifier, - createPipeExpression, findUniqueName, createVariableDeclaration, } from 'lang/modifyAst' @@ -24,12 +22,13 @@ import { mutateAstWithTagForSketchSegment, getEdgeTagCall, } from 'lang/modifyAst/addEdgeTreatment' +import { Artifact, getPathsFromArtifact } from 'lang/std/artifactGraph' export function revolveSketch( ast: Node, pathToSketchNode: PathToNode, - shouldPipe = false, angle: Expr = createLiteral(4), - axis: Selections + axis: Selections, + artifact?: Artifact ): | { modifiedAst: Node @@ -37,6 +36,11 @@ export function revolveSketch( pathToRevolveArg: PathToNode } | Error { + const orderedSketchNodePaths = getPathsFromArtifact({ + artifact: artifact, + sketchPathToNode: pathToSketchNode, + }) + if (err(orderedSketchNodePaths)) return orderedSketchNodePaths const clonedAst = structuredClone(ast) const sketchNode = getNodeFromPath(clonedAst, pathToSketchNode) if (err(sketchNode)) return sketchNode @@ -67,29 +71,13 @@ export function revolveSketch( if (err(tagResult)) return tagResult const { tag } = tagResult - /* Original Code */ - const { node: sketchExpression } = sketchNode - - // determine if sketchExpression is in a pipeExpression or not - const sketchPipeExpressionNode = getNodeFromPath( - 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, - shallowPath: sketchPathToDecleration, - } = sketchVariableDeclaratorNode + const { node: sketchVariableDeclarator } = sketchVariableDeclaratorNode const axisSelection = axis?.graphSelections[0]?.artifact @@ -103,37 +91,13 @@ export function revolveSketch( createIdentifier(sketchVariableDeclarator.id.name), ]) - if (shouldPipe) { - const pipeChain = createPipeExpression( - isInPipeExpression - ? [...sketchPipeExpression.body, revolveCall] - : [sketchExpression as any, revolveCall] - ) - - sketchVariableDeclarator.init = pipeChain - const pathToRevolveArg: PathToNode = [ - ...sketchPathToDecleration, - ['init', 'VariableDeclarator'], - ['body', ''], - [pipeChain.body.length - 1, 'index'], - ['arguments', 'CallExpression'], - [0, 'index'], - ] - - return { - modifiedAst: clonedAst, - pathToSketchNode, - pathToRevolveArg, - } - } - // We're not creating a pipe expression, // but rather a separate constant for the extrusion const name = findUniqueName(clonedAst, KCL_DEFAULT_CONSTANT_PREFIXES.REVOLVE) const VariableDeclaration = createVariableDeclaration(name, revolveCall) - const sketchIndexInPathToNode = - sketchPathToDecleration.findIndex((a) => a[0] === 'body') + 1 - const sketchIndexInBody = sketchPathToDecleration[sketchIndexInPathToNode][0] + const lastSketchNodePath = + orderedSketchNodePaths[orderedSketchNodePaths.length - 1] + const sketchIndexInBody = Number(lastSketchNodePath[1][0]) if (typeof sketchIndexInBody !== 'number') return new Error('expected sketchIndexInBody to be a number') clonedAst.body.splice(sketchIndexInBody + 1, 0, VariableDeclaration) diff --git a/src/lang/queryAst.ts b/src/lang/queryAst.ts index 08b5032da..88587618a 100644 --- a/src/lang/queryAst.ts +++ b/src/lang/queryAst.ts @@ -33,6 +33,7 @@ import { err, Reason } from 'lib/trap' import { ImportStatement } from 'wasm-lib/kcl/bindings/ImportStatement' import { Node } from 'wasm-lib/kcl/bindings/Node' import { ArtifactGraph, codeRefFromRange } from './std/artifactGraph' +import { FunctionExpression } from 'wasm-lib/kcl/bindings/FunctionExpression' /** * Retrieves a node from a given path within a Program node structure, optionally stopping at a specified node type. @@ -597,7 +598,13 @@ export function findAllPreviousVariables( type ReplacerFn = ( _ast: Node, varName: string -) => { modifiedAst: Node; pathToReplaced: PathToNode } | Error +) => + | { + modifiedAst: Node + pathToReplaced: PathToNode + exprInsertIndex: number + } + | Error export function isNodeSafeToReplacePath( ast: Program, @@ -649,7 +656,7 @@ export function isNodeSafeToReplacePath( if (err(_nodeToReplace)) return _nodeToReplace const nodeToReplace = _nodeToReplace.node as any nodeToReplace[last[0]] = identifier - return { modifiedAst: _ast, pathToReplaced } + return { modifiedAst: _ast, pathToReplaced, exprInsertIndex: index } } const hasPipeSub = isTypeInValue(finVal as Expr, 'PipeSubstitution') @@ -768,8 +775,15 @@ export function isLinesParallelAndConstrained( if (err(_primarySegment)) return _primarySegment const primarySegment = _primarySegment.segment + const _varDec2 = getNodeFromPath(ast, secondaryPath, 'VariableDeclaration') + if (err(_varDec2)) return _varDec2 + const varDec2 = _varDec2.node + const varName2 = (varDec2 as VariableDeclaration)?.declaration.id?.name + const sg2 = sketchFromKclValue(programMemory?.get(varName2), varName2) + if (err(sg2)) return sg2 + const _segment = getSketchSegmentFromSourceRange( - sg, + sg2, secondaryLine?.codeRef?.range ) if (err(_segment)) return _segment @@ -1102,3 +1116,57 @@ export function getObjExprProperty( if (index === -1) return null return { expr: node.properties[index].value, index } } + +export function isCursorInFunctionDefinition( + ast: Node, + 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 d31574a2d..db2304b74 100644 --- a/src/lang/std/__snapshots__/artifactGraph.test.ts.snap +++ b/src/lang/std/__snapshots__/artifactGraph.test.ts.snap @@ -212,6 +212,7 @@ 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 2870fe42a..1c75c1a6e 100644 --- a/src/lang/std/artifactGraph.test.ts +++ b/src/lang/std/artifactGraph.test.ts @@ -22,6 +22,7 @@ import * as d3 from 'd3-force' import path from 'path' import pixelmatch from 'pixelmatch' import { PNG } from 'pngjs' +import { Node } from 'wasm-lib/kcl/bindings/Node' /* Note this is an integration test, these tests connect to our real dev server and make websocket commands. @@ -171,7 +172,7 @@ afterAll(() => { describe('testing createArtifactGraph', () => { describe('code with offset planes and a sketch:', () => { - let ast: Program + let ast: Node let theMap: ReturnType it('setup', () => { @@ -217,7 +218,7 @@ describe('testing createArtifactGraph', () => { }) }) describe('code with an extrusion, fillet and sketch of face:', () => { - let ast: Program + let ast: Node let theMap: ReturnType it('setup', () => { // putting this logic in here because describe blocks runs before beforeAll has finished @@ -312,7 +313,7 @@ describe('testing createArtifactGraph', () => { }) describe(`code with sketches but no extrusions or other 3D elements`, () => { - let ast: Program + let ast: Node let theMap: ReturnType it(`setup`, () => { // putting this logic in here because describe blocks runs before beforeAll has finished @@ -377,7 +378,7 @@ describe('testing createArtifactGraph', () => { describe('capture graph of sketchOnFaceOnFace...', () => { describe('code with an extrusion, fillet and sketch of face:', () => { - let ast: Program + let ast: Node let theMap: ReturnType it('setup', async () => { // putting this logic in here because describe blocks runs before beforeAll has finished @@ -399,7 +400,9 @@ describe('capture graph of sketchOnFaceOnFace...', () => { }) }) -function getCommands(codeKey: CodeKey): CacheShape[CodeKey] & { ast: Program } { +function getCommands( + codeKey: CodeKey +): CacheShape[CodeKey] & { ast: Node } { 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 d5a9e5098..9ecab816d 100644 --- a/src/lang/std/artifactGraph.ts +++ b/src/lang/std/artifactGraph.ts @@ -1,7 +1,19 @@ -import { PathToNode, Program, SourceRange } from 'lang/wasm' +import { + Expr, + PathToNode, + Program, + SourceRange, + VariableDeclaration, +} from 'lang/wasm' import { Models } from '@kittycad/lib' -import { getNodePathFromSourceRange } from 'lang/queryAst' +import { + getNodeFromPath, + getNodePathFromSourceRange, + traverse, +} from 'lang/queryAst' import { err } from 'lib/trap' +import { engineCommandManager, kclManager } from 'lib/singletons' +import { Node } from 'wasm-lib/kcl/bindings/Node' export type ArtifactId = string @@ -34,14 +46,14 @@ export interface PathArtifact extends BaseArtifact { codeRef: CodeRef } -interface solid2D extends BaseArtifact { +interface Solid2DArtifact extends BaseArtifact { type: 'solid2D' pathId: ArtifactId } export interface PathArtifactRich extends BaseArtifact { type: 'path' /** A path must always lie on a plane */ - plane: PlaneArtifact | WallArtifact + plane: PlaneArtifact | WallArtifact | CapArtifact /** A path must always contain 0 or more segments */ segments: Array /** A path may not result in a sweep artifact */ @@ -61,7 +73,7 @@ interface SegmentArtifactRich extends BaseArtifact { type: 'segment' path: PathArtifact surf: WallArtifact - edges: Array + edges: Array edgeCut?: EdgeCut codeRef: CodeRef } @@ -80,7 +92,7 @@ interface SweepArtifactRich extends BaseArtifact { subType: 'extrusion' | 'revolve' path: PathArtifact surfaces: Array - edges: Array + edges: Array codeRef: CodeRef } @@ -90,6 +102,9 @@ 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' @@ -97,9 +112,12 @@ 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 SweepEdge extends BaseArtifact { +interface SweepEdgeArtifact extends BaseArtifact { type: 'sweepEdge' segId: ArtifactId sweepId: ArtifactId @@ -129,10 +147,10 @@ export type Artifact = | SweepArtifact | WallArtifact | CapArtifact - | SweepEdge + | SweepEdgeArtifact | EdgeCut | EdgeCutEdge - | solid2D + | Solid2DArtifact export type ArtifactGraph = Map @@ -159,7 +177,7 @@ export function createArtifactGraph({ }: { orderedCommands: Array responseMap: ResponseMap - ast: Program + ast: Node }) { const myMap = new Map() @@ -238,7 +256,7 @@ export function getArtifactsToUpdate({ /** Passing in a getter because we don't wan this function to update the map directly */ getArtifact: (id: ArtifactId) => Artifact | undefined currentPlaneId: ArtifactId - ast: Program + ast: Node }): Array<{ id: ArtifactId artifact: Artifact @@ -274,6 +292,13 @@ export function getArtifactsToUpdate({ plane?.type === 'plane' ? plane?.codeRef : { range, pathToNode } const existingPlane = getArtifact(currentPlaneId) if (existingPlane?.type === 'wall') { + let existingPlaneCodeRef = existingPlane.codeRef + if (!existingPlaneCodeRef) { + const astWalkCodeRef = getWallOrCapPlaneCodeRef(ast, codeRef.pathToNode) + if (!err(astWalkCodeRef)) { + existingPlaneCodeRef = astWalkCodeRef + } + } return [ { id: currentPlaneId, @@ -284,6 +309,29 @@ export function getArtifactsToUpdate({ edgeCutEdgeIds: existingPlane.edgeCutEdgeIds, sweepId: existingPlane.sweepId, pathIds: existingPlane.pathIds, + codeRef: existingPlaneCodeRef, + }, + }, + ] + } else if (existingPlane?.type === 'cap') { + let existingPlaneCodeRef = existingPlane.codeRef + if (!existingPlaneCodeRef) { + const astWalkCodeRef = getWallOrCapPlaneCodeRef(ast, codeRef.pathToNode) + if (!err(astWalkCodeRef)) { + existingPlaneCodeRef = astWalkCodeRef + } + } + return [ + { + id: currentPlaneId, + artifact: { + type: 'cap', + subType: existingPlane.subType, + id: currentPlaneId, + edgeCutEdgeIds: existingPlane.edgeCutEdgeIds, + sweepId: existingPlane.sweepId, + pathIds: existingPlane.pathIds, + codeRef: existingPlaneCodeRef, }, }, ] @@ -328,6 +376,18 @@ export function getArtifactsToUpdate({ pathIds: [id], }, }) + } else if (plane?.type === 'cap') { + returnArr.push({ + id: currentPlaneId, + artifact: { + type: 'cap', + id: currentPlaneId, + subType: plane.subType, + edgeCutEdgeIds: plane.edgeCutEdgeIds, + sweepId: plane.sweepId, + pathIds: [id], + }, + }) } return returnArr } else if (cmd.type === 'extend_path' || cmd.type === 'close_path') { @@ -733,7 +793,7 @@ export function getCapCodeRef( } export function getSolid2dCodeRef( - solid2D: solid2D, + solid2D: Solid2DArtifact, artifactGraph: ArtifactGraph ): CodeRef | Error { const path = getArtifactOfTypes( @@ -757,7 +817,7 @@ export function getWallCodeRef( } export function getSweepEdgeCodeRef( - edge: SweepEdge, + edge: SweepEdgeArtifact, artifactGraph: ArtifactGraph ): CodeRef | Error { const seg = getArtifactOfTypes( @@ -871,3 +931,281 @@ export function codeRefFromRange(range: SourceRange, ast: Program): CodeRef { pathToNode: getNodePathFromSourceRange(ast, range), } } + +function getPlaneFromPath( + path: PathArtifact, + graph: ArtifactGraph +): PlaneArtifact | WallArtifact | CapArtifact | Error { + const plane = getArtifactOfTypes( + { key: path.planeId, types: ['plane', 'wall', 'cap'] }, + graph + ) + if (err(plane)) return plane + return plane +} + +function getPlaneFromSegment( + segment: SegmentArtifact, + graph: ArtifactGraph +): PlaneArtifact | WallArtifact | CapArtifact | Error { + const path = getArtifactOfTypes( + { key: segment.pathId, types: ['path'] }, + graph + ) + if (err(path)) return path + return getPlaneFromPath(path, graph) +} +function getPlaneFromSolid2D( + solid2D: Solid2DArtifact, + graph: ArtifactGraph +): PlaneArtifact | WallArtifact | CapArtifact | Error { + const path = getArtifactOfTypes( + { key: solid2D.pathId, types: ['path'] }, + graph + ) + if (err(path)) return path + return getPlaneFromPath(path, graph) +} +function getPlaneFromCap( + cap: CapArtifact, + graph: ArtifactGraph +): PlaneArtifact | WallArtifact | CapArtifact | Error { + const sweep = getArtifactOfTypes( + { key: cap.sweepId, types: ['sweep'] }, + graph + ) + if (err(sweep)) return sweep + const path = getArtifactOfTypes({ key: sweep.pathId, types: ['path'] }, graph) + if (err(path)) return path + return getPlaneFromPath(path, graph) +} +function getPlaneFromWall( + wall: WallArtifact, + graph: ArtifactGraph +): PlaneArtifact | WallArtifact | CapArtifact | Error { + const sweep = getArtifactOfTypes( + { key: wall.sweepId, types: ['sweep'] }, + graph + ) + if (err(sweep)) return sweep + const path = getArtifactOfTypes({ key: sweep.pathId, types: ['path'] }, graph) + if (err(path)) return path + return getPlaneFromPath(path, graph) +} +function getPlaneFromSweepEdge(edge: SweepEdgeArtifact, graph: ArtifactGraph) { + const sweep = getArtifactOfTypes( + { key: edge.sweepId, types: ['sweep'] }, + graph + ) + if (err(sweep)) return sweep + const path = getArtifactOfTypes({ key: sweep.pathId, types: ['path'] }, graph) + if (err(path)) return path + return getPlaneFromPath(path, graph) +} + +export function getPlaneFromArtifact( + artifact: Artifact | undefined, + graph: ArtifactGraph +): PlaneArtifact | WallArtifact | CapArtifact | Error { + if (!artifact) return new Error(`Artifact is undefined`) + if (artifact.type === 'plane') return artifact + if (artifact.type === 'path') return getPlaneFromPath(artifact, graph) + if (artifact.type === 'segment') return getPlaneFromSegment(artifact, graph) + if (artifact.type === 'solid2D') return getPlaneFromSolid2D(artifact, graph) + if (artifact.type === 'cap') return getPlaneFromCap(artifact, graph) + if (artifact.type === 'wall') return getPlaneFromWall(artifact, graph) + if (artifact.type === 'sweepEdge') + return getPlaneFromSweepEdge(artifact, graph) + return new Error(`Artifact type ${artifact.type} does not have a plane`) +} + +const isExprSafe = (index: number): boolean => { + const expr = kclManager.ast.body?.[index] + if (!expr) { + return false + } + if (expr.type === 'ImportStatement' || expr.type === 'ReturnStatement') { + return false + } + if (expr.type === 'VariableDeclaration') { + const init = expr.declaration?.init + if (!init) return false + if (init.type === 'CallExpression') { + return false + } + if (init.type === 'BinaryExpression' && isNodeSafe(init)) { + return true + } + if (init.type === 'Literal' || init.type === 'MemberExpression') { + return true + } + } + return false +} + +const onlyConsecutivePaths = ( + orderedNodePaths: PathToNode[], + originalPath: PathToNode +): PathToNode[] => { + const originalIndex = Number( + orderedNodePaths.find( + (path) => path[1][0] === originalPath[1][0] + )?.[1]?.[0] || 0 + ) + + const minIndex = Number(orderedNodePaths[0][1][0]) + const maxIndex = Number(orderedNodePaths[orderedNodePaths.length - 1][1][0]) + const pathIndexMap: any = {} + orderedNodePaths.forEach((path) => { + const bodyIndex = Number(path[1][0]) + pathIndexMap[bodyIndex] = path + }) + const safePaths: PathToNode[] = [] + + // traverse expressions in either direction from the profile selected + // when the user entered sketch mode + for (let i = originalIndex; i <= maxIndex; i++) { + if (pathIndexMap[i]) { + safePaths.push(pathIndexMap[i]) + } else if (!isExprSafe(i)) { + break + } + } + for (let i = originalIndex - 1; i >= minIndex; i--) { + if (pathIndexMap[i]) { + safePaths.unshift(pathIndexMap[i]) + } else if (!isExprSafe(i)) { + break + } + } + return safePaths +} + +export function getPathsFromPlaneArtifact(planeArtifact: PlaneArtifact) { + const nodePaths: PathToNode[] = [] + for (const pathId of planeArtifact.pathIds) { + const path = engineCommandManager.artifactGraph.get(pathId) + if (!path) continue + if ('codeRef' in path && path.codeRef) { + // TODO should figure out why upstream the path is bad + const isNodePathBad = path.codeRef.pathToNode.length < 2 + nodePaths.push( + isNodePathBad + ? getNodePathFromSourceRange(kclManager.ast, path.codeRef.range) + : path.codeRef.pathToNode + ) + } + } + return onlyConsecutivePaths(nodePaths, nodePaths[0]) +} + +export function getPathsFromArtifact({ + sketchPathToNode, + artifact, +}: { + sketchPathToNode: PathToNode + artifact?: Artifact +}): PathToNode[] | Error { + const plane = getPlaneFromArtifact( + artifact, + engineCommandManager.artifactGraph + ) + if (err(plane)) return plane + const paths = getArtifactsOfTypes( + { keys: plane.pathIds, types: ['path'] }, + engineCommandManager.artifactGraph + ) + let nodePaths = [...paths.values()] + .map((path) => path.codeRef.pathToNode) + .sort((a, b) => Number(a[1][0]) - Number(b[1][0])) + return onlyConsecutivePaths(nodePaths, sketchPathToNode) +} + +function isNodeSafe(node: Expr): boolean { + if (node.type === 'Literal' || node.type === 'MemberExpression') { + return true + } + if (node.type === 'BinaryExpression') { + return isNodeSafe(node.left) && isNodeSafe(node.right) + } + return false +} + +/** {@deprecated} this information should come from the ArtifactGraph not digging around in the AST */ +function getWallOrCapPlaneCodeRef( + ast: Node, + 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 1bec224d5..f24510872 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 1f7418f10..a59131744 100644 --- a/src/lang/std/engineConnection.ts +++ b/src/lang/std/engineConnection.ts @@ -37,6 +37,7 @@ import { KclManager } from 'lang/KclSingleton' import { reportRejection } from 'lib/trap' import { markOnce } from 'lib/performance' import { MachineManager } from 'components/MachineManagerProvider' +import { Node } from 'wasm-lib/kcl/bindings/Node' // TODO(paultag): This ought to be tweakable. const pingIntervalMs = 5_000 @@ -2115,7 +2116,7 @@ export class EngineCommandManager extends EventTarget { Object.values(this.pendingCommands).map((a) => a.promise) ) } - updateArtifactGraph(ast: Program) { + updateArtifactGraph(ast: Node) { this.artifactGraph = createArtifactGraph({ orderedCommands: this.orderedCommands, responseMap: this.responseMap, @@ -2213,7 +2214,11 @@ export class EngineCommandManager extends EventTarget { commandTypeToTarget: string ): string | undefined { for (const [artifactId, artifact] of this.artifactGraph) { - if ('codeRef' in artifact && isOverlap(range, artifact.codeRef.range)) { + if ( + 'codeRef' in artifact && + artifact.codeRef && + isOverlap(range, artifact.codeRef.range) + ) { if (commandTypeToTarget === artifact.type) return artifactId } } diff --git a/src/lang/std/sketch.ts b/src/lang/std/sketch.ts index 947db85c7..a4933a8ae 100644 --- a/src/lang/std/sketch.ts +++ b/src/lang/std/sketch.ts @@ -297,14 +297,20 @@ export const lineTo: SketchLineHelper = { add: ({ node, pathToNode, segmentInput, replaceExistingCallback }) => { if (segmentInput.type !== 'straight-segment') return STRAIGHT_SEGMENT_ERR const to = segmentInput.to - const _node = { ...node } + const _node = structuredClone(node) const nodeMeta = getNodeFromPath( _node, pathToNode, 'PipeExpression' ) if (err(nodeMeta)) return nodeMeta - const { node: pipe } = nodeMeta + const varDec = getNodeFromPath( + _node, + pathToNode, + 'VariableDeclaration' + ) + if (err(varDec)) return varDec + const dec = varDec.node.declaration const newVals: [Expr, Expr] = [ createLiteral(roundOff(to[0], 2)), @@ -333,14 +339,20 @@ export const lineTo: SketchLineHelper = { ]) if (err(result)) return result const { callExp, valueUsedInTransform } = result - pipe.body[callIndex] = callExp + if (dec.init.type === 'PipeExpression') { + dec.init.body[callIndex] = callExp + } else { + dec.init = callExp + } return { modifiedAst: _node, pathToNode, valueUsedInTransform: valueUsedInTransform, } + } else if (dec.init.type === 'PipeExpression') { + dec.init.body = [...dec.init.body, newLine] } else { - pipe.body = [...pipe.body, newLine] + dec.init = createPipeExpression([dec.init, newLine]) } return { modifiedAst: _node, @@ -663,11 +675,11 @@ export const xLine: SketchLineHelper = { add: ({ node, pathToNode, segmentInput, replaceExistingCallback }) => { if (segmentInput.type !== 'straight-segment') return STRAIGHT_SEGMENT_ERR const { from, to } = segmentInput - const _node = { ...node } + const _node = structuredClone(node) const getNode = getNodeFromPathCurry(_node, pathToNode) - const _node1 = getNode('PipeExpression') - if (err(_node1)) return _node1 - const { node: pipe } = _node1 + const varDec = getNode('VariableDeclaration') + if (err(varDec)) return varDec + const dec = varDec.node.declaration const newVal = createLiteral(roundOff(to[0] - from[0], 2)) @@ -682,7 +694,11 @@ export const xLine: SketchLineHelper = { ]) if (err(result)) return result const { callExp, valueUsedInTransform } = result - pipe.body[callIndex] = callExp + if (dec.init.type === 'PipeExpression') { + dec.init.body[callIndex] = callExp + } else { + dec.init = callExp + } return { modifiedAst: _node, pathToNode, @@ -694,7 +710,11 @@ export const xLine: SketchLineHelper = { newVal, createPipeSubstitution(), ]) - pipe.body = [...pipe.body, newLine] + if (dec.init.type === 'PipeExpression') { + dec.init.body = [...dec.init.body, newLine] + } else { + dec.init = createPipeExpression([dec.init, newLine]) + } return { modifiedAst: _node, pathToNode } }, updateArgs: ({ node, pathToNode, input }) => { @@ -731,11 +751,11 @@ export const yLine: SketchLineHelper = { add: ({ node, pathToNode, segmentInput, replaceExistingCallback }) => { if (segmentInput.type !== 'straight-segment') return STRAIGHT_SEGMENT_ERR const { from, to } = segmentInput - const _node = { ...node } + const _node = structuredClone(node) const getNode = getNodeFromPathCurry(_node, pathToNode) - const _node1 = getNode('PipeExpression') - if (err(_node1)) return _node1 - const { node: pipe } = _node1 + const varDec = getNode('VariableDeclaration') + if (err(varDec)) return varDec + const dec = varDec.node.declaration const newVal = createLiteral(roundOff(to[1] - from[1], 2)) if (replaceExistingCallback) { const { index: callIndex } = splitPathAtPipeExpression(pathToNode) @@ -748,7 +768,11 @@ export const yLine: SketchLineHelper = { ]) if (err(result)) return result const { callExp, valueUsedInTransform } = result - pipe.body[callIndex] = callExp + if (dec.init.type === 'PipeExpression') { + dec.init.body[callIndex] = callExp + } else { + dec.init = callExp + } return { modifiedAst: _node, pathToNode, @@ -760,7 +784,11 @@ export const yLine: SketchLineHelper = { newVal, createPipeSubstitution(), ]) - pipe.body = [...pipe.body, newLine] + if (dec.init.type === 'PipeExpression') { + dec.init.body = [...dec.init.body, newLine] + } else { + dec.init = createPipeExpression([dec.init, newLine]) + } return { modifiedAst: _node, pathToNode } }, updateArgs: ({ node, pathToNode, input }) => { @@ -2145,8 +2173,6 @@ function addTagToChamfer( if (err(variableDec)) return variableDec const isPipeExpression = pipeExpr.node.type === 'PipeExpression' - console.log('pipeExpr', pipeExpr, variableDec) - // const callExpr = isPipeExpression ? pipeExpr.node.body[pipeIndex] : variableDec.node.init const callExpr = isPipeExpression ? pipeExpr.node.body[pipeIndex] : variableDec.node.init @@ -2227,7 +2253,6 @@ function addTagToChamfer( if (isPipeExpression) { pipeExpr.node.body.splice(pipeIndex, 0, newExpressionToInsert) } else { - console.log('yo', createPipeExpression([newExpressionToInsert, callExpr])) callExpr.arguments[1] = createPipeSubstitution() variableDec.node.init = createPipeExpression([ newExpressionToInsert, diff --git a/src/lang/util.ts b/src/lang/util.ts index f7147fd04..9bb2d4737 100644 --- a/src/lang/util.ts +++ b/src/lang/util.ts @@ -9,20 +9,47 @@ import { import { ArtifactGraph, filterArtifacts } from 'lang/std/artifactGraph' import { isOverlap } from 'lib/utils' -export function updatePathToNodeFromMap( - oldPath: PathToNode, - pathToNodeMap: { [key: number]: PathToNode } +/** + * Updates pathToNode body indices to account for the insertion of an expression + * PathToNode expression is after the insertion index, that the body index is incremented + * Negative insertion index means no insertion + */ +export function updatePathToNodePostExprInjection( + pathToNode: PathToNode, + exprInsertIndex: number ): PathToNode { - const updatedPathToNode = structuredClone(oldPath) - let max = 0 - Object.values(pathToNodeMap).forEach((path) => { - const index = Number(path[1][0]) - if (index > max) { - max = index - } - }) - updatedPathToNode[1][0] = max - return updatedPathToNode + if (exprInsertIndex < 0) return pathToNode + const bodyIndex = Number(pathToNode[1][0]) + if (bodyIndex < exprInsertIndex) return pathToNode + const clone = structuredClone(pathToNode) + clone[1][0] = bodyIndex + 1 + return clone +} + +export function updateSketchDetailsNodePaths({ + sketchEntryNodePath, + sketchNodePaths, + planeNodePath, + exprInsertIndex, +}: { + sketchEntryNodePath: PathToNode + sketchNodePaths: Array + planeNodePath: PathToNode + exprInsertIndex: number +}) { + return { + updatedSketchEntryNodePath: updatePathToNodePostExprInjection( + sketchEntryNodePath, + exprInsertIndex + ), + updatedSketchNodePaths: sketchNodePaths.map((path) => + updatePathToNodePostExprInjection(path, exprInsertIndex) + ), + updatedPlaneNodePath: updatePathToNodePostExprInjection( + planeNodePath, + exprInsertIndex + ), + } } export function isCursorInSketchCommandRange( @@ -31,7 +58,7 @@ export function isCursorInSketchCommandRange( ): string | false { const overlappingEntries = filterArtifacts( { - types: ['segment', 'path'], + types: ['segment', 'path', 'plane'], predicate: (artifact) => { return selectionRanges.graphSelections.some( (selection) => diff --git a/src/lang/wasm.ts b/src/lang/wasm.ts index 3b1ed0de6..908030d07 100644 --- a/src/lang/wasm.ts +++ b/src/lang/wasm.ts @@ -501,26 +501,6 @@ export const executor = async ( if (programMemoryOverride !== null && err(programMemoryOverride)) return Promise.reject(programMemoryOverride) - // eslint-disable-next-line @typescript-eslint/no-floating-promises - engineCommandManager.startNewSession() - const _programMemory = await _executor( - node, - engineCommandManager, - programMemoryOverride - ) - await engineCommandManager.waitForAllCommands() - - return _programMemory -} - -export const _executor = async ( - node: Node, - 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 6b9fdc66f..6fb9ff029 100644 --- a/src/lib/rectangleTool.ts +++ b/src/lib/rectangleTool.ts @@ -29,7 +29,7 @@ import { */ export const getRectangleCallExpressions = ( rectangleOrigin: [number, number], - tags: [string, string, string] + tag: string ) => [ createCallExpressionStdLib('angledLine', [ createArrayExpression([ @@ -37,30 +37,28 @@ export const getRectangleCallExpressions = ( createLiteral(0), // This will be the width of the rectangle ]), createPipeSubstitution(), - createTagDeclarator(tags[0]), + createTagDeclarator(tag), ]), createCallExpressionStdLib('angledLine', [ createArrayExpression([ createBinaryExpression([ - createCallExpressionStdLib('segAng', [createIdentifier(tags[0])]), + createCallExpressionStdLib('segAng', [createIdentifier(tag)]), '+', createLiteral(90), ]), // 90 offset from the previous line createLiteral(0), // This will be the height of the rectangle ]), createPipeSubstitution(), - createTagDeclarator(tags[1]), ]), createCallExpressionStdLib('angledLine', [ createArrayExpression([ - createCallExpressionStdLib('segAng', [createIdentifier(tags[0])]), // same angle as the first line + createCallExpressionStdLib('segAng', [createIdentifier(tag)]), // same angle as the first line createUnaryExpression( - createCallExpressionStdLib('segLen', [createIdentifier(tags[0])]), + createCallExpressionStdLib('segLen', [createIdentifier(tag)]), '-' ), // negative height ]), createPipeSubstitution(), - createTagDeclarator(tags[2]), ]), createCallExpressionStdLib('lineTo', [ createArrayExpression([ @@ -85,12 +83,12 @@ export function updateRectangleSketch( y: number, tag: string ) { - ;((pipeExpression.body[2] as CallExpression) + ;((pipeExpression.body[1] as CallExpression) .arguments[0] as ArrayExpression) = createArrayExpression([ createLiteral(x >= 0 ? 0 : 180), createLiteral(Math.abs(x)), ]) - ;((pipeExpression.body[3] as CallExpression) + ;((pipeExpression.body[2] as CallExpression) .arguments[0] as ArrayExpression) = createArrayExpression([ createBinaryExpression([ createCallExpressionStdLib('segAng', [createIdentifier(tag)]), @@ -120,7 +118,7 @@ export function updateCenterRectangleSketch( let startY = originY - Math.abs(deltaY) // pipeExpression.body[1] is startProfileAt - let callExpression = pipeExpression.body[1] + let callExpression = pipeExpression.body[0] if (isCallExpression(callExpression)) { const arrayExpression = callExpression.arguments[0] if (isArrayExpression(arrayExpression)) { @@ -134,7 +132,7 @@ export function updateCenterRectangleSketch( const twoX = deltaX * 2 const twoY = deltaY * 2 - callExpression = pipeExpression.body[2] + callExpression = pipeExpression.body[1] if (isCallExpression(callExpression)) { const arrayExpression = callExpression.arguments[0] if (isArrayExpression(arrayExpression)) { @@ -148,7 +146,7 @@ export function updateCenterRectangleSketch( } } - callExpression = pipeExpression.body[3] + callExpression = pipeExpression.body[2] if (isCallExpression(callExpression)) { const arrayExpression = callExpression.arguments[0] if (isArrayExpression(arrayExpression)) { diff --git a/src/lib/selections.ts b/src/lib/selections.ts index d74da02dd..872b068af 100644 --- a/src/lib/selections.ts +++ b/src/lib/selections.ts @@ -280,18 +280,19 @@ export function getEventForSegmentSelection( } if (!id || !group) return null const artifact = engineCommandManager.artifactGraph.get(id) - const codeRefs = getCodeRefsByArtifactId( - id, - engineCommandManager.artifactGraph - ) - if (!artifact || !codeRefs) return null + if (!artifact) return null + const node = getNodeFromPath(kclManager.ast, group.userData.pathToNode) + if (err(node)) return null return { type: 'Set selection', data: { selectionType: 'singleCodeCursor', selection: { artifact, - codeRef: codeRefs[0], + codeRef: { + pathToNode: group?.userData?.pathToNode, + range: [node.node.start, node.node.end, true], + }, }, }, } @@ -675,8 +676,7 @@ export function getSelectionTypeDisplayText( const selectionsByType = getSelectionCountByType(selection) if (selectionsByType === 'none') return null - return selectionsByType - .entries() + return [...selectionsByType.entries()] .map( // Hack for showing "face" instead of "extrude-wall" in command bar text ([type, count]) => @@ -685,7 +685,6 @@ export function getSelectionTypeDisplayText( .replace('solid2D', 'face') .replace('segment', 'face')}${count > 1 ? 's' : ''}` ) - .toArray() .join(', ') } @@ -695,7 +694,7 @@ export function canSubmitSelectionArg( ) { return ( selectionsByType !== 'none' && - selectionsByType.entries().every(([type, count]) => { + [...selectionsByType.entries()].every(([type, count]) => { const foundIndex = argument.selectionTypes.findIndex((s) => s === type) return ( foundIndex !== -1 && @@ -718,7 +717,7 @@ export function codeToIdSelections( // TODO #868: loops over all artifacts will become inefficient at a large scale const overlappingEntries = Array.from(engineCommandManager.artifactGraph) .map(([id, artifact]) => { - if (!('codeRef' in artifact)) return null + if (!('codeRef' in artifact && artifact.codeRef)) return null return isOverlap(artifact.codeRef.range, selection.range) ? { artifact, @@ -961,7 +960,6 @@ export function updateSelections( JSON.stringify(pathToNode) ) { artifact = a - console.log('found artifact', a) break } } diff --git a/src/lib/testHelpers.ts b/src/lib/testHelpers.ts index 7453086ae..720bceec2 100644 --- a/src/lib/testHelpers.ts +++ b/src/lib/testHelpers.ts @@ -1,20 +1,16 @@ import { Program, ProgramMemory, - _executor, + executor, SourceRange, ExecState, } from '../lang/wasm' -import { - EngineCommandManager, - EngineCommandManagerEvents, -} from 'lang/std/engineConnection' +import { EngineCommandManager } from 'lang/std/engineConnection' import { EngineCommand } from 'lang/std/artifactGraph' import { Models } from '@kittycad/lib' import { v4 as uuidv4 } from 'uuid' import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes' -import { err, reportRejection } from 'lib/trap' -import { toSync } from './utils' +import { err } from 'lib/trap' import { Node } from 'wasm-lib/kcl/bindings/Node' type WebSocketResponse = Models['WebSocketResponse_type'] @@ -94,36 +90,7 @@ export async function enginelessExecutor( }) as any as EngineCommandManager // eslint-disable-next-line @typescript-eslint/no-floating-promises mockEngineCommandManager.startNewSession() - const execState = await _executor(ast, mockEngineCommandManager, pmo) + const execState = await executor(ast, mockEngineCommandManager, pmo) await mockEngineCommandManager.waitForAllCommands() return execState } - -export async function executor( - ast: Node, - 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 e5512cf74..8d3061a2a 100644 --- a/src/lib/toolbar.ts +++ b/src/lib/toolbar.ts @@ -2,8 +2,6 @@ import { CustomIconName } from 'components/CustomIcon' import { DEV } from 'env' import { commandBarMachine } from 'machines/commandBarMachine' import { - canRectangleOrCircleTool, - isClosedSketch, isEditingExistingSketch, modelingMachine, pipeHasCircle, @@ -73,7 +71,7 @@ export const toolbarConfig: Record = { status: 'available', disabled: (state) => !state.matches('idle'), title: ({ sketchPathId }) => - `${sketchPathId ? 'Edit' : 'Start'} Sketch`, + sketchPathId ? 'Edit Sketch' : 'Start Sketch', showTitle: true, hotkey: 'S', description: 'Start drawing a 2D sketch', @@ -315,22 +313,14 @@ export const toolbarConfig: Record = { { id: 'line', onClick: ({ modelingState, modelingSend }) => { - if (modelingState.matches({ Sketch: { 'Line tool': 'No Points' } })) { - // Exit the sketch state if there are no points and they press ESC - modelingSend({ - type: 'Cancel', - }) - } else { - // Exit the tool if there are points and they press ESC - modelingSend({ - type: 'change tool', - data: { - tool: !modelingState.matches({ Sketch: 'Line tool' }) - ? 'line' - : 'none', - }, - }) - } + modelingSend({ + type: 'change tool', + data: { + tool: !modelingState.matches({ Sketch: 'Line tool' }) + ? 'line' + : 'none', + }, + }) }, icon: 'line', status: 'available', @@ -341,8 +331,7 @@ export const toolbarConfig: Record = { }) || state.matches({ Sketch: { 'Circle tool': 'Awaiting Radius' }, - }) || - isClosedSketch(state.context), + }), title: 'Line', hotkey: (state) => state.matches({ Sketch: 'Line tool' }) ? ['Esc', 'L'] : 'L', @@ -422,10 +411,7 @@ export const toolbarConfig: Record = { icon: 'circle', status: 'available', title: 'Center circle', - disabled: (state) => - state.matches('Sketch no face') || - (!canRectangleOrCircleTool(state.context) && - !state.matches({ Sketch: 'Circle tool' })), + disabled: (state) => state.matches('Sketch no face'), isActive: (state) => state.matches({ Sketch: 'Circle tool' }), hotkey: (state) => state.matches({ Sketch: 'Circle tool' }) ? ['Esc', 'C'] : 'C', @@ -464,10 +450,7 @@ export const toolbarConfig: Record = { }), icon: 'rectangle', status: 'available', - disabled: (state) => - state.matches('Sketch no face') || - (!canRectangleOrCircleTool(state.context) && - !state.matches({ Sketch: 'Rectangle tool' })), + disabled: (state) => state.matches('Sketch no face'), title: 'Corner rectangle', hotkey: (state) => state.matches({ Sketch: 'Rectangle tool' }) ? ['Esc', 'R'] : 'R', @@ -490,10 +473,7 @@ export const toolbarConfig: Record = { }), icon: 'arc', status: 'available', - disabled: (state) => - state.matches('Sketch no face') || - (!canRectangleOrCircleTool(state.context) && - !state.matches({ Sketch: 'Center Rectangle tool' })), + disabled: (state) => state.matches('Sketch no face'), title: 'Center rectangle', hotkey: (state) => state.matches({ Sketch: 'Center Rectangle tool' }) diff --git a/src/lib/trap.ts b/src/lib/trap.ts index ab4551a42..e62b7fcc8 100644 --- a/src/lib/trap.ts +++ b/src/lib/trap.ts @@ -97,3 +97,7 @@ 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 3bb313b9d..f8f14d78b 100644 --- a/src/machines/modelingMachine.ts +++ b/src/machines/modelingMachine.ts @@ -1,7 +1,6 @@ import { PathToNode, ProgramMemory, - VariableDeclaration, VariableDeclarator, parse, recast, @@ -82,6 +81,7 @@ 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,7 +101,9 @@ export type SetSelections = | { selectionType: 'completeSelection' selection: Selections - updatedPathToNode?: PathToNode + updatedSketchEntryNodePath?: PathToNode + updatedSketchNodePaths?: PathToNode[] + updatedPlaneNodePath?: PathToNode } | { selectionType: 'mirrorCodeMirrorSelections' @@ -126,12 +128,20 @@ export type MouseState = } export interface SketchDetails { - sketchPathToNode: PathToNode + sketchEntryNodePath: PathToNode + sketchNodePaths: PathToNode[] + planeNodePath: 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 @@ -240,7 +250,14 @@ export type ModelingMachineEvent = | { type: 'Toggle gui mode' } | { type: 'Cancel' } | { type: 'CancelSketch' } - | { type: 'Add start point' } + | { + type: 'Add start point' | 'Continue existing profile' + data: { + sketchNodePaths: PathToNode[] + sketchEntryNodePath: PathToNode + } + } + | { type: 'Close sketch' } | { type: 'Make segment horizontal' } | { type: 'Make segment vertical' } | { type: 'Constrain horizontal distance' } @@ -288,6 +305,14 @@ 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 } | { @@ -369,7 +394,9 @@ export const modelingMachineDefaultContext: ModelingMachineContext = { graphSelections: [], }, sketchDetails: { - sketchPathToNode: [], + sketchEntryNodePath: [], + planeNodePath: [], + sketchNodePaths: [], zAxis: [0, 0, 1], yAxis: [0, 1, 0], origin: [0, 0, 0], @@ -400,23 +427,6 @@ 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 } }) => { @@ -562,14 +572,12 @@ export const modelingMachine = setup({ currentTool === 'tangentialArc' && isEditingExistingSketch({ sketchDetails }), - '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 rectangle': ({ context: { currentTool } }) => + currentTool === 'rectangle', + 'next is center rectangle': ({ context: { currentTool } }) => + currentTool === 'center rectangle', + 'next is circle': ({ context: { currentTool } }) => + currentTool === 'circle', 'next is line': ({ context }) => context.currentTool === 'line', 'next is none': ({ context }) => context.currentTool === 'none', }, @@ -588,11 +596,11 @@ export const modelingMachine = setup({ 'enter modeling mode': assign({ currentMode: 'modeling' }), 'set sketchMetadata from pathToNode': assign( ({ context: { sketchDetails } }) => { - if (!sketchDetails?.sketchPathToNode || !sketchDetails) return {} + if (!sketchDetails?.sketchEntryNodePath || !sketchDetails) return {} return { sketchDetails: { ...sketchDetails, - sketchPathToNode: sketchDetails.sketchPathToNode, + sketchEntryNodePath: sketchDetails.sketchEntryNodePath, }, } } @@ -634,7 +642,19 @@ export const modelingMachine = setup({ ;(async () => { if (!event.data) return const { selection, distance } = event.data - let ast = kclManager.ast + 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) if ( 'variableName' in distance && distance.variableName && @@ -647,24 +667,13 @@ 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: [pathToExtrudeArg], + focusPath: [updatedPathToExtrudeArg], zoomToFit: true, zoomOnRangeAndType: { range: selection.graphSelections[0]?.codeRef.range, @@ -704,11 +713,11 @@ export const modelingMachine = setup({ const revolveSketchRes = revolveSketch( ast, pathToNode, - false, 'variableName' in angle ? angle.variableIdentifierAst : angle.valueAst, - axis + axis, + selection.graphSelections[0]?.artifact ) if (trap(revolveSketchRes)) return const { modifiedAst, pathToRevolveArg } = revolveSketchRes @@ -798,11 +807,12 @@ export const modelingMachine = setup({ if (!sketchDetails) return ;(async () => { if (Object.keys(sceneEntitiesManager.activeSegments).length > 0) { - await sceneEntitiesManager.tearDownSketch({ removeAxis: false }) + sceneEntitiesManager.tearDownSketch({ removeAxis: false }) } sceneInfra.resetMouseListeners() await sceneEntitiesManager.setupSketch({ - sketchPathToNode: sketchDetails?.sketchPathToNode || [], + sketchEntryNodePath: sketchDetails?.sketchEntryNodePath || [], + sketchNodePaths: sketchDetails.sketchNodePaths, forward: sketchDetails.zAxis, up: sketchDetails.yAxis, position: sketchDetails.origin, @@ -810,28 +820,33 @@ export const modelingMachine = setup({ selectionRanges, }) sceneInfra.resetMouseListeners() + sceneEntitiesManager.setupSketchIdleCallbacks({ - pathToNode: sketchDetails?.sketchPathToNode || [], + sketchEntryNodePath: sketchDetails?.sketchEntryNodePath || [], 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': ({ context: { sketchDetails } }) => { - if (!sketchDetails) return + 'set up draft line': assign(({ context: { sketchDetails }, event }) => { + if (!sketchDetails) return {} + if (event.type !== 'Add start point') return {} // eslint-disable-next-line @typescript-eslint/no-floating-promises sceneEntitiesManager .setupDraftSegment( - sketchDetails.sketchPathToNode, + event.data.sketchEntryNodePath || sketchDetails.sketchEntryNodePath, + event.data.sketchNodePaths || sketchDetails.sketchNodePaths, + sketchDetails.planeNodePath, sketchDetails.zAxis, sketchDetails.yAxis, sketchDetails.origin, @@ -840,14 +855,24 @@ export const modelingMachine = setup({ .then(() => { return codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast) }) - }, - 'set up draft arc': ({ context: { sketchDetails } }) => { - if (!sketchDetails) return + 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 {} // eslint-disable-next-line @typescript-eslint/no-floating-promises sceneEntitiesManager .setupDraftSegment( - sketchDetails.sketchPathToNode, + event.data.sketchEntryNodePath || sketchDetails.sketchEntryNodePath, + event.data.sketchNodePaths || sketchDetails.sketchNodePaths, + sketchDetails.planeNodePath, sketchDetails.zAxis, sketchDetails.yAxis, sketchDetails.origin, @@ -856,12 +881,32 @@ 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 - sceneEntitiesManager.setupNoPointsListener({ - sketchDetails, - afterClick: (args) => { + 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) => { const twoD = args.intersectionPoint?.twoD if (twoD) { sceneInfra.modelingSend({ @@ -877,10 +922,22 @@ export const modelingMachine = setup({ 'listen for center rectangle origin': ({ context: { sketchDetails } }) => { if (!sketchDetails) return - // setupNoPointsListener has the code for startProfileAt onClick - sceneEntitiesManager.setupNoPointsListener({ - sketchDetails, - afterClick: (args) => { + 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) => { const twoD = args.intersectionPoint?.twoD if (twoD) { sceneInfra.modelingSend({ @@ -915,7 +972,7 @@ export const modelingMachine = setup({ if (!args) return if (args.mouseEvent.which !== 1) return const { intersectionPoint } = args - if (!intersectionPoint?.twoD || !sketchDetails?.sketchPathToNode) + if (!intersectionPoint?.twoD || !sketchDetails?.sketchEntryNodePath) return const twoD = args.intersectionPoint?.twoD if (twoD) { @@ -929,81 +986,68 @@ export const modelingMachine = setup({ }, }) }, - '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 + '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' ) - }, - '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) - }) - }, + 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) + ) + if (planeArtifact?.type !== 'plane') return {} + const newPaths = getPathsFromPlaneArtifact(planeArtifact) + return { + sketchDetails: { + ...sketchDetails, + sketchNodePaths: newPaths, + sketchEntryNodePath: newPaths[0], + }, + selectionRanges: { + otherSelections: [], + graphSelections: [], + }, + } + }), 'show default planes': () => { // eslint-disable-next-line @typescript-eslint/no-floating-promises kclManager.showPlanes() }, - 'setup noPoints onClick listener': ({ context: { sketchDetails } }) => { + 'setup noPoints onClick listener': ({ + context: { sketchDetails, currentTool }, + }) => { if (!sketchDetails) return sceneEntitiesManager.setupNoPointsListener({ sketchDetails, - afterClick: () => sceneInfra.modelingSend({ type: 'Add start point' }), + currentTool, + afterClick: (_, data) => + sceneInfra.modelingSend( + currentTool === 'tangentialArc' + ? { type: 'Continue existing profile', data } + : { type: 'Add start point', data } + ), }) }, 'add axis n grid': ({ context: { sketchDetails } }) => { @@ -1012,7 +1056,7 @@ export const modelingMachine = setup({ // eslint-disable-next-line @typescript-eslint/no-floating-promises sceneEntitiesManager.createSketchAxis( - sketchDetails.sketchPathToNode || [], + sketchDetails.sketchEntryNodePath || [], sketchDetails.zAxis, sketchDetails.yAxis, sketchDetails.origin @@ -1106,6 +1150,8 @@ export const modelingMachine = setup({ if (!sketchDetails) return let updatedAst = await sceneEntitiesManager.updateAstAndRejigSketch( pathToNodeMap[0], + sketchDetails.sketchNodePaths, + sketchDetails.planeNodePath, constraint.modifiedAst, sketchDetails.zAxis, sketchDetails.yAxis, @@ -1142,7 +1188,9 @@ export const modelingMachine = setup({ const { modifiedAst, pathToNodeMap } = constraint if (!sketchDetails) return const updatedAst = await sceneEntitiesManager.updateAstAndRejigSketch( - sketchDetails.sketchPathToNode, + sketchDetails.sketchEntryNodePath, + sketchDetails.sketchNodePaths, + sketchDetails.planeNodePath, modifiedAst, sketchDetails.zAxis, sketchDetails.yAxis, @@ -1177,7 +1225,9 @@ export const modelingMachine = setup({ const { modifiedAst, pathToNodeMap } = constraint if (!sketchDetails) return const updatedAst = await sceneEntitiesManager.updateAstAndRejigSketch( - sketchDetails.sketchPathToNode || [], + sketchDetails.sketchEntryNodePath || [], + sketchDetails.sketchNodePaths, + sketchDetails.planeNodePath, modifiedAst, sketchDetails.zAxis, sketchDetails.yAxis, @@ -1210,7 +1260,9 @@ export const modelingMachine = setup({ const { modifiedAst, pathToNodeMap } = constraint if (!sketchDetails) return const updatedAst = await sceneEntitiesManager.updateAstAndRejigSketch( - sketchDetails?.sketchPathToNode || [], + sketchDetails?.sketchEntryNodePath || [], + sketchDetails.sketchNodePaths, + sketchDetails.planeNodePath, modifiedAst, sketchDetails.zAxis, sketchDetails.yAxis, @@ -1244,7 +1296,9 @@ export const modelingMachine = setup({ const { modifiedAst, pathToNodeMap } = constraint if (!sketchDetails) return const updatedAst = await sceneEntitiesManager.updateAstAndRejigSketch( - sketchDetails?.sketchPathToNode || [], + sketchDetails?.sketchEntryNodePath || [], + sketchDetails.sketchNodePaths, + sketchDetails.planeNodePath, modifiedAst, sketchDetails.zAxis, sketchDetails.yAxis, @@ -1278,7 +1332,9 @@ export const modelingMachine = setup({ const { modifiedAst, pathToNodeMap } = constraint if (!sketchDetails) return const updatedAst = await sceneEntitiesManager.updateAstAndRejigSketch( - sketchDetails?.sketchPathToNode || [], + sketchDetails?.sketchEntryNodePath || [], + sketchDetails.sketchNodePaths, + sketchDetails.planeNodePath, modifiedAst, sketchDetails.zAxis, sketchDetails.yAxis, @@ -1312,7 +1368,9 @@ export const modelingMachine = setup({ const { modifiedAst, pathToNodeMap } = constraint if (!sketchDetails) return const updatedAst = await sceneEntitiesManager.updateAstAndRejigSketch( - sketchDetails?.sketchPathToNode || [], + sketchDetails?.sketchEntryNodePath || [], + sketchDetails.sketchNodePaths, + sketchDetails.planeNodePath, modifiedAst, sketchDetails.zAxis, sketchDetails.yAxis, @@ -1353,7 +1411,9 @@ export const modelingMachine = setup({ if (err(recastAst) || !resultIsOk(recastAst)) return const updatedAst = await sceneEntitiesManager.updateAstAndRejigSketch( - sketchDetails?.sketchPathToNode || [], + sketchDetails?.sketchEntryNodePath || [], + sketchDetails.sketchNodePaths, + sketchDetails.planeNodePath, recastAst.program, sketchDetails.zAxis, sketchDetails.yAxis, @@ -1387,7 +1447,9 @@ export const modelingMachine = setup({ const { modifiedAst, pathToNodeMap } = constraint if (!sketchDetails) return const updatedAst = await sceneEntitiesManager.updateAstAndRejigSketch( - sketchDetails?.sketchPathToNode || [], + sketchDetails?.sketchEntryNodePath || [], + sketchDetails.sketchNodePaths, + sketchDetails.planeNodePath, modifiedAst, sketchDetails.zAxis, sketchDetails.yAxis, @@ -1449,24 +1511,12 @@ export const modelingMachine = setup({ ), 'animate-to-face': fromPromise( async (_: { input?: ExtrudeFacePlane | DefaultPlane | OffsetPlane }) => { - return {} as - | undefined - | { - sketchPathToNode: PathToNode - zAxis: [number, number, number] - yAxis: [number, number, number] - origin: [number, number, number] - } + return {} as ModelingMachineContext['sketchDetails'] } ), 'animate-to-sketch': fromPromise( async (_: { input: Pick }) => { - return {} as { - sketchPathToNode: PathToNode - zAxis: [number, number, number] - yAxis: [number, number, number] - origin: [number, number, number] - } + return {} as ModelingMachineContext['sketchDetails'] } ), 'Get horizontal info': fromPromise( @@ -1663,10 +1713,49 @@ 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+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 */ + /** @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 */ id: 'Modeling', context: ({ input }) => ({ @@ -1879,6 +1968,7 @@ export const modelingMachine = setup({ 'change tool': { target: 'Change Tool', + reenter: true, }, }, @@ -2007,30 +2097,23 @@ 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 without teardown', + actions: 'set up draft line', }, Cancel: '#Modeling.Sketch.undo startSketchOn', }, }, + + normal: { + on: { + 'Close sketch': 'Init', + }, + }, }, initial: 'Init', @@ -2038,6 +2121,7 @@ export const modelingMachine = setup({ on: { 'change tool': { target: 'Change Tool', + reenter: true, }, }, }, @@ -2053,13 +2137,33 @@ 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': { @@ -2075,8 +2179,6 @@ export const modelingMachine = setup({ }, 'Rectangle tool': { - entry: ['listen for rectangle origin'], - states: { 'Awaiting second corner': { on: { @@ -2087,14 +2189,47 @@ export const modelingMachine = setup({ 'Awaiting origin': { on: { 'Add rectangle origin': { - target: 'Awaiting second corner', - actions: 'set up draft rectangle', + target: 'adding draft rectangle', + reenter: true, }, }, + + entry: 'listen for rectangle origin', }, 'Finished Rectangle': { - always: '#Modeling.Sketch.SketchIdle', + 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, + } + }, + }, }, }, @@ -2103,13 +2238,12 @@ 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: { @@ -2120,15 +2254,47 @@ export const modelingMachine = setup({ 'Awaiting origin': { on: { 'Add center rectangle origin': { - target: 'Awaiting corner', - // TODO - actions: 'set up draft center rectangle', + target: 'add draft center rectangle', + reenter: true, }, }, + + entry: 'listen for center rectangle origin', }, 'Finished Center Rectangle': { - always: '#Modeling.Sketch.SketchIdle', + 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, + } + }, + }, }, }, @@ -2137,12 +2303,14 @@ export const modelingMachine = setup({ on: { 'change tool': { target: 'Change Tool', + reenter: true, }, }, }, 'clean slate': { always: 'SketchIdle', + entry: 're-eval nodePaths', }, 'Converting to named value': { @@ -2312,7 +2480,7 @@ export const modelingMachine = setup({ }, }, - 'Change Tool': { + 'Change Tool ifs': { always: [ { target: 'SketchIdle', @@ -2339,22 +2507,26 @@ export const modelingMachine = setup({ guard: 'next is center rectangle', }, ], - - entry: ['assign tool in context', 'reset selections'], }, + 'Circle tool': { on: { - 'change tool': 'Change Tool', + 'change tool': { + target: 'Change Tool', + reenter: true, + }, }, states: { 'Awaiting origin': { on: { 'Add circle origin': { - target: 'Awaiting Radius', - actions: 'set up draft circle', + target: 'adding draft circle', + reenter: true, }, }, + + entry: 'listen for circle origin', }, 'Awaiting Radius': { @@ -2364,12 +2536,77 @@ export const modelingMachine = setup({ }, 'Finished Circle': { - always: '#Modeling.Sketch.SketchIdle', + 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, + } + }, + }, }, }, initial: 'Awaiting origin', - entry: 'listen for circle 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'], }, }, @@ -2437,10 +2674,12 @@ 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: [ @@ -2449,6 +2688,8 @@ export const modelingMachine = setup({ 'enter sketching mode', ], }, + + onError: 'idle', }, }, @@ -2537,34 +2778,40 @@ export function isEditingExistingSketch({ }): boolean { // should check that the variable declaration is a pipeExpression // and that the pipeExpression contains a "startProfileAt" callExpression - if (!sketchDetails?.sketchPathToNode) return false + if (!sketchDetails?.sketchEntryNodePath) return false const variableDeclaration = getNodeFromPath( kclManager.ast, - sketchDetails.sketchPathToNode, + sketchDetails.sketchEntryNodePath, 'VariableDeclarator' ) - if (err(variableDeclaration)) return false + if (variableDeclaration instanceof Error) return false if (variableDeclaration.node.type !== 'VariableDeclarator') return false - const pipeExpression = variableDeclaration.node.init - if (pipeExpression.type !== 'PipeExpression') return false - const hasStartProfileAt = pipeExpression.body.some( + 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( (item) => item.type === 'CallExpression' && item.callee.name === 'startProfileAt' ) - const hasCircle = pipeExpression.body.some( + const hasCircle = maybePipeExpression.body.some( (item) => item.type === 'CallExpression' && item.callee.name === 'circle' ) - return (hasStartProfileAt && pipeExpression.body.length > 2) || hasCircle + return (hasStartProfileAt && maybePipeExpression.body.length > 1) || hasCircle } export function pipeHasCircle({ sketchDetails, }: { sketchDetails: SketchDetails | null }): boolean { - if (!sketchDetails?.sketchPathToNode) return false + if (!sketchDetails?.sketchEntryNodePath) return false const variableDeclaration = getNodeFromPath( kclManager.ast, - sketchDetails.sketchPathToNode, + sketchDetails.sketchEntryNodePath, 'VariableDeclarator' ) if (err(variableDeclaration)) return false @@ -2576,41 +2823,3 @@ 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') - ) -}