Compare commits
	
		
			7 Commits
		
	
	
		
			jtran/unit
			...
			pierremtb/
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 66638368cc | |||
| a2699aa3d1 | |||
| 6da22b14a6 | |||
| 79b47a57c5 | |||
| 3cd1b9a3be | |||
| 46c6d5bed3 | |||
| 9cba212f32 | 
| @ -57,23 +57,26 @@ async function doBasicSketch(page: Page, openPanes: string[]) { | ||||
|   const startXPx = 600 | ||||
|   await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) | ||||
|   if (openPanes.includes('code')) { | ||||
|     await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ') | ||||
|   |> startProfileAt(${commonPoints.startAt}, %)`) | ||||
|     await expect(u.codeLocator).toContainText( | ||||
|       `sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${commonPoints.startAt}, sketch001)` | ||||
|     ) | ||||
|   } | ||||
|   await page.waitForTimeout(500) | ||||
|   await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10) | ||||
|   await page.waitForTimeout(500) | ||||
|  | ||||
|   if (openPanes.includes('code')) { | ||||
|     await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ') | ||||
|   |> startProfileAt(${commonPoints.startAt}, %) | ||||
|     await expect(u.codeLocator) | ||||
|       .toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${commonPoints.startAt}, sketch001) | ||||
|   |> xLine(${commonPoints.num1}, %)`) | ||||
|   } | ||||
|   await page.waitForTimeout(500) | ||||
|   await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20) | ||||
|   if (openPanes.includes('code')) { | ||||
|     await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ') | ||||
|   |> startProfileAt(${commonPoints.startAt}, %) | ||||
|     await expect(u.codeLocator) | ||||
|       .toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${ | ||||
|       commonPoints.startAt | ||||
|     }, sketch001) | ||||
|   |> xLine(${commonPoints.num1}, %) | ||||
|   |> yLine(${commonPoints.num1 + 0.01}, %)`) | ||||
|   } else { | ||||
| @ -82,8 +85,10 @@ async function doBasicSketch(page: Page, openPanes: string[]) { | ||||
|   await page.waitForTimeout(200) | ||||
|   await page.mouse.click(startXPx, 500 - PUR * 20) | ||||
|   if (openPanes.includes('code')) { | ||||
|     await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ') | ||||
|   |> startProfileAt(${commonPoints.startAt}, %) | ||||
|     await expect(u.codeLocator) | ||||
|       .toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${ | ||||
|       commonPoints.startAt | ||||
|     }, sketch001) | ||||
|   |> xLine(${commonPoints.num1}, %) | ||||
|   |> yLine(${commonPoints.num1 + 0.01}, %) | ||||
|   |> xLine(${commonPoints.num2 * -1}, %)`) | ||||
| @ -140,8 +145,10 @@ async function doBasicSketch(page: Page, openPanes: string[]) { | ||||
|  | ||||
|   // Open the code pane. | ||||
|   await u.openKclCodePanel() | ||||
|   await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ') | ||||
|   |> startProfileAt(${commonPoints.startAt}, %) | ||||
|   await expect(u.codeLocator) | ||||
|     .toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${ | ||||
|     commonPoints.startAt | ||||
|   }, sketch001) | ||||
|   |> xLine(${commonPoints.num1}, %, $seg01) | ||||
|   |> yLine(${commonPoints.num1 + 0.01}, %) | ||||
|   |> xLine(-segLen(seg01), %)`) | ||||
|  | ||||
| @ -44,8 +44,7 @@ test.describe('Can create sketches on all planes and their back sides', () => { | ||||
|       }, | ||||
|     } | ||||
|  | ||||
|     const code = `sketch001 = startSketchOn('${plane}') | ||||
|     |> startProfileAt([0.9, -1.22], %)` | ||||
|     const code = `sketch001 = startSketchOn('${plane}')profile001 = startProfileAt([0.9, -1.22], sketch001)` | ||||
|  | ||||
|     await u.openDebugPanel() | ||||
|  | ||||
|  | ||||
| @ -11,6 +11,7 @@ import { | ||||
|  | ||||
| type mouseParams = { | ||||
|   pixelDiff?: number | ||||
|   shouldDbClick?: boolean | ||||
| } | ||||
| type mouseDragToParams = mouseParams & { | ||||
|   fromPoint: { x: number; y: number } | ||||
| @ -75,11 +76,16 @@ export class SceneFixture { | ||||
|         if (clickParams?.pixelDiff) { | ||||
|           return doAndWaitForImageDiff( | ||||
|             this.page, | ||||
|             () => this.page.mouse.click(x, y), | ||||
|             () => | ||||
|               clickParams?.shouldDbClick | ||||
|                 ? this.page.mouse.dblclick(x, y) | ||||
|                 : this.page.mouse.click(x, y), | ||||
|             clickParams.pixelDiff | ||||
|           ) | ||||
|         } | ||||
|         return this.page.mouse.click(x, y) | ||||
|         return clickParams?.shouldDbClick | ||||
|           ? this.page.mouse.dblclick(x, y) | ||||
|           : this.page.mouse.click(x, y) | ||||
|       }, | ||||
|       (moveParams?: mouseParams) => { | ||||
|         if (moveParams?.pixelDiff) { | ||||
|  | ||||
| @ -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() | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -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,16 +179,11 @@ test.describe('verify sketch on chamfer works', () => { | ||||
|  | ||||
|         afterChamferSelectSnippet: | ||||
|           'sketch002 = startSketchOn(extrude001, seg03)', | ||||
|         afterRectangle1stClickSnippet: 'startProfileAt([205.96, 254.59], %)', | ||||
|         afterRectangle1stClickSnippet: | ||||
|           'startProfileAt([205.96, 254.59], sketch002)', | ||||
|         afterRectangle2ndClickSnippet: `angledLine([0,11.39],%,$rectangleSegmentA002) | ||||
|     |> angledLine([ | ||||
|          segAng(rectangleSegmentA002) - 90, | ||||
|          105.26 | ||||
|        ], %, $rectangleSegmentB001) | ||||
|     |> angledLine([ | ||||
|          segAng(rectangleSegmentA002), | ||||
|          -segLen(rectangleSegmentA002) | ||||
|        ], %, $rectangleSegmentC001) | ||||
|         |>angledLine([segAng(rectangleSegmentA002)-90,105.26],%) | ||||
|         |>angledLine([segAng(rectangleSegmentA002),-segLen(rectangleSegmentA002)],%) | ||||
|         |>lineTo([profileStartX(%),profileStartY(%)],%) | ||||
|         |>close(%)`, | ||||
|       }) | ||||
| @ -209,19 +206,15 @@ test.describe('verify sketch on chamfer works', () => { | ||||
|  | ||||
|         afterChamferSelectSnippet: | ||||
|           'sketch003 = startSketchOn(extrude001, seg04)', | ||||
|         afterRectangle1stClickSnippet: 'startProfileAt([-209.64, 255.28], %)', | ||||
|         afterRectangle1stClickSnippet: | ||||
|           'startProfileAt([-209.64, 255.28], sketch003)', | ||||
|         afterRectangle2ndClickSnippet: `angledLine([0,11.56],%,$rectangleSegmentA003) | ||||
|     |> angledLine([ | ||||
|          segAng(rectangleSegmentA003) - 90, | ||||
|          106.84 | ||||
|        ], %, $rectangleSegmentB002) | ||||
|     |> angledLine([ | ||||
|          segAng(rectangleSegmentA003), | ||||
|          -segLen(rectangleSegmentA003) | ||||
|        ], %, $rectangleSegmentC002) | ||||
|         |>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,17 +247,11 @@ test.describe('verify sketch on chamfer works', () => { | ||||
|        }, %)`, | ||||
|         afterChamferSelectSnippet: | ||||
|           'sketch005 = startSketchOn(extrude001, seg06)', | ||||
|         afterRectangle1stClickSnippet: 'startProfileAt([-23.43, 19.69], %)', | ||||
|         afterRectangle1stClickSnippet: | ||||
|           'startProfileAt([-23.43, 19.69], sketch005)', | ||||
|         afterRectangle2ndClickSnippet: `angledLine([0,9.1],%,$rectangleSegmentA005) | ||||
|  | ||||
|     |> angledLine([ | ||||
|          segAng(rectangleSegmentA005) - 90, | ||||
|          84.07 | ||||
|        ], %, $rectangleSegmentB004) | ||||
|     |> angledLine([ | ||||
|          segAng(rectangleSegmentA005), | ||||
|          -segLen(rectangleSegmentA005) | ||||
|        ], %, $rectangleSegmentC004) | ||||
|         |>angledLine([segAng(rectangleSegmentA005)-90,84.07],%) | ||||
|         |>angledLine([segAng(rectangleSegmentA005),-segLen(rectangleSegmentA005)],%) | ||||
|         |>lineTo([profileStartX(%),profileStartY(%)],%) | ||||
|         |>close(%)`, | ||||
|       }) | ||||
| @ -277,7 +259,6 @@ test.describe('verify sketch on chamfer works', () => { | ||||
|       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([ | ||||
| @ -305,55 +286,55 @@ test.describe('verify sketch on chamfer works', () => { | ||||
|        tags = [getNextAdjacentEdge(yo)] | ||||
|      }, %, $seg06) | ||||
| sketch005 = startSketchOn(extrude001, seg06) | ||||
|       |> startProfileAt([-23.43, 19.69], %) | ||||
| profile004 = startProfileAt([-23.43, 19.69], sketch005) | ||||
|   |> 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], %) | ||||
| profile003 = startProfileAt([82.57, 322.96], sketch004) | ||||
|   |> 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], %) | ||||
| profile002 = startProfileAt([-209.64, 255.28], sketch003) | ||||
|   |> 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], %) | ||||
| 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(%) | ||||
| `, | ||||
| @ -392,16 +373,11 @@ test.describe('verify sketch on chamfer works', () => { | ||||
|         beforeChamferSnippetEnd: '}, extrude001)', | ||||
|         afterChamferSelectSnippet: | ||||
|           'sketch002 = startSketchOn(extrude001, seg03)', | ||||
|         afterRectangle1stClickSnippet: 'startProfileAt([205.96, 254.59], %)', | ||||
|         afterRectangle1stClickSnippet: | ||||
|           'startProfileAt([205.96, 254.59], sketch002)', | ||||
|         afterRectangle2ndClickSnippet: `angledLine([0,11.39],%,$rectangleSegmentA002) | ||||
|     |> angledLine([ | ||||
|          segAng(rectangleSegmentA002) - 90, | ||||
|          105.26 | ||||
|        ], %, $rectangleSegmentB001) | ||||
|     |> angledLine([ | ||||
|          segAng(rectangleSegmentA002), | ||||
|          -segLen(rectangleSegmentA002) | ||||
|        ], %, $rectangleSegmentC001) | ||||
|         |>angledLine([segAng(rectangleSegmentA002)-90,105.26],%) | ||||
|         |>angledLine([segAng(rectangleSegmentA002),-segLen(rectangleSegmentA002)],%) | ||||
|         |>lineTo([profileStartX(%),profileStartY(%)],%) | ||||
|         |>close(%)`, | ||||
|       }) | ||||
| @ -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() | ||||
| @ -692,10 +668,10 @@ loftPointAndClickCases.forEach(({ shouldPreselect }) => { | ||||
|     cmdBar, | ||||
|   }) => { | ||||
|     const initialCode = `sketch001 = startSketchOn('XZ') | ||||
|     |> circle({ center = [0, 0], radius = 30 }, %) | ||||
| profile001 = circle({ center = [0, 0], radius = 30 }, sketch001) | ||||
| plane001 = offsetPlane('XZ', 50) | ||||
| sketch002 = startSketchOn(plane001) | ||||
|     |> circle({ center = [0, 0], radius = 20 }, %) | ||||
| profile002 = circle({ center = [0, 0], radius = 20 }, sketch002) | ||||
| ` | ||||
|     await app.initialise(initialCode) | ||||
|  | ||||
| @ -706,7 +682,7 @@ loftPointAndClickCases.forEach(({ shouldPreselect }) => { | ||||
|       testPoint.x, | ||||
|       testPoint.y + 80 | ||||
|     ) | ||||
|     const loftDeclaration = 'loft001 = loft([sketch001, sketch002])' | ||||
|     const loftDeclaration = 'loft001 = loft([profile001, profile002])' | ||||
|  | ||||
|     await test.step(`Look for the white of the sketch001 shape`, async () => { | ||||
|       await scene.expectPixelColor([254, 254, 254], testPoint, 15) | ||||
|  | ||||
| @ -114,9 +114,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 +126,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 +135,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 +149,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 +673,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 +709,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 +746,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 +814,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 +1142,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], %) | ||||
|     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', | ||||
| @ -1419,3 +1437,560 @@ 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], %)`) | ||||
|         } | ||||
|       ) | ||||
|     } | ||||
|   ) | ||||
| }) | ||||
|  | ||||
| @ -446,8 +446,7 @@ test( | ||||
|  | ||||
|     const startXPx = 600 | ||||
|     await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) | ||||
|     code += ` | ||||
|   |> startProfileAt([7.19, -9.7], %)` | ||||
|     code += `profile001 = startProfileAt([7.19, -9.7], sketch001)` | ||||
|     await expect(page.locator('.cm-content')).toHaveText(code) | ||||
|     await page.waitForTimeout(100) | ||||
|  | ||||
| @ -469,6 +468,10 @@ test( | ||||
|       .getByRole('button', { name: 'arc Tangential Arc', exact: true }) | ||||
|       .click() | ||||
|  | ||||
|     // click to continue profile | ||||
|     await page.mouse.move(813, 392, { steps: 10 }) | ||||
|     await page.waitForTimeout(100) | ||||
|  | ||||
|     await page.mouse.move(startXPx + PUR * 30, 500 - PUR * 20, { steps: 10 }) | ||||
|  | ||||
|     await page.waitForTimeout(1000) | ||||
| @ -591,8 +594,7 @@ test( | ||||
|       mask: [page.getByTestId('model-state-indicator')], | ||||
|     }) | ||||
|     await expect(page.locator('.cm-content')).toHaveText( | ||||
|       `sketch001 = startSketchOn('XZ') | ||||
|   |> circle({ center = [14.44, -2.44], radius = 1 }, %)` | ||||
|       `sketch001 = startSketchOn('XZ')profile001 = circle({ center = [14.44, -2.44], radius = 1 }, sketch001)` | ||||
|     ) | ||||
|   } | ||||
| ) | ||||
| @ -636,8 +638,7 @@ test.describe( | ||||
|  | ||||
|       const startXPx = 600 | ||||
|       await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) | ||||
|       code += ` | ||||
|   |> startProfileAt([7.19, -9.7], %)` | ||||
|       code += `profile001 = startProfileAt([7.19, -9.7], sketch001)` | ||||
|       await expect(u.codeLocator).toHaveText(code) | ||||
|       await page.waitForTimeout(100) | ||||
|  | ||||
| @ -655,6 +656,10 @@ test.describe( | ||||
|         .click() | ||||
|       await page.waitForTimeout(100) | ||||
|  | ||||
|       // click to continue profile | ||||
|       await page.mouse.click(813, 392) | ||||
|       await page.waitForTimeout(100) | ||||
|  | ||||
|       await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20) | ||||
|  | ||||
|       code += ` | ||||
| @ -741,8 +746,7 @@ test.describe( | ||||
|  | ||||
|       const startXPx = 600 | ||||
|       await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) | ||||
|       code += ` | ||||
|   |> startProfileAt([182.59, -246.32], %)` | ||||
|       code += `profile001 = startProfileAt([182.59, -246.32], sketch001)` | ||||
|       await expect(u.codeLocator).toHaveText(code) | ||||
|       await page.waitForTimeout(100) | ||||
|  | ||||
| @ -760,6 +764,10 @@ test.describe( | ||||
|         .click() | ||||
|       await page.waitForTimeout(100) | ||||
|  | ||||
|       // click to continue profile | ||||
|       await page.mouse.click(813, 392) | ||||
|       await page.waitForTimeout(100) | ||||
|  | ||||
|       await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20) | ||||
|  | ||||
|       code += ` | ||||
|  | ||||
| Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 53 KiB | 
| Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 50 KiB | 
| Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 50 KiB | 
| Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 47 KiB | 
| Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB | 
| Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB | 
| Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 52 KiB | 
| Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 48 KiB | 
| Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 48 KiB | 
| Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 45 KiB | 
| Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 45 KiB | 
| Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB | 
| Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 42 KiB | 
| Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB | 
| Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB | 
| Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB | 
| @ -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, %) | ||||
|  | ||||
| @ -77,30 +77,31 @@ test.describe('Testing selections', () => { | ||||
|       const startXPx = 600 | ||||
|       await u.closeDebugPanel() | ||||
|       await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) | ||||
|       await expect(page.locator('.cm-content')) | ||||
|         .toHaveText(`sketch001 = startSketchOn('XZ') | ||||
|     |> startProfileAt(${commonPoints.startAt}, %)`) | ||||
|       await expect(page.locator('.cm-content')).toHaveText( | ||||
|         `sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${commonPoints.startAt}, sketch001)` | ||||
|       ) | ||||
|  | ||||
|       await page.waitForTimeout(100) | ||||
|       await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10) | ||||
|  | ||||
|       await expect(page.locator('.cm-content')) | ||||
|         .toHaveText(`sketch001 = startSketchOn('XZ') | ||||
|     |> startProfileAt(${commonPoints.startAt}, %) | ||||
|         .toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${commonPoints.startAt}, sketch001) | ||||
|     |> xLine(${commonPoints.num1}, %)`) | ||||
|  | ||||
|       await page.waitForTimeout(100) | ||||
|       await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20) | ||||
|       await expect(page.locator('.cm-content')) | ||||
|         .toHaveText(`sketch001 = startSketchOn('XZ') | ||||
|     |> startProfileAt(${commonPoints.startAt}, %) | ||||
|         .toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${ | ||||
|         commonPoints.startAt | ||||
|       }, sketch001) | ||||
|     |> xLine(${commonPoints.num1}, %) | ||||
|     |> yLine(${commonPoints.num1 + 0.01}, %)`) | ||||
|       await page.waitForTimeout(100) | ||||
|       await page.mouse.click(startXPx, 500 - PUR * 20) | ||||
|       await expect(page.locator('.cm-content')) | ||||
|         .toHaveText(`sketch001 = startSketchOn('XZ') | ||||
|     |> startProfileAt(${commonPoints.startAt}, %) | ||||
|         .toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${ | ||||
|         commonPoints.startAt | ||||
|       }, sketch001) | ||||
|     |> xLine(${commonPoints.num1}, %) | ||||
|     |> yLine(${commonPoints.num1 + 0.01}, %) | ||||
|     |> xLine(${commonPoints.num2 * -1}, %)`) | ||||
| @ -330,6 +331,28 @@ part009 = startSketchOn('XY') | ||||
|   |> angledLineToX({ angle = 60, to = pipeLargeDia }, %) | ||||
|   |> close(%) | ||||
| rev = revolve({ axis = 'y' }, part009) | ||||
| sketch006 = startSketchOn('XY') | ||||
| profile001 = circle({ | ||||
|   center = [42.91, -70.42], | ||||
|   radius = 17.96 | ||||
| }, sketch006) | ||||
| profile002 = startProfileAt([86.92, -63.81], sketch006) | ||||
|   |> angledLine([0, 63.81], %, $rectangleSegmentA001) | ||||
|   |> angledLine([ | ||||
|        segAng(rectangleSegmentA001) - 90, | ||||
|        17.05 | ||||
|      ], %) | ||||
|   |> angledLine([ | ||||
|        segAng(rectangleSegmentA001), | ||||
|        -segLen(rectangleSegmentA001) | ||||
|      ], %) | ||||
|   |> lineTo([profileStartX(%), profileStartY(%)], %) | ||||
|   |> close(%) | ||||
| profile003 = startProfileAt([40.16, -120.48], sketch006) | ||||
|   |> line([26.95, 24.21], %) | ||||
|   |> line([20.91, -28.61], %) | ||||
|   |> line([32.46, 18.71], %) | ||||
|  | ||||
| ` | ||||
|       ) | ||||
|     }, KCL_DEFAULT_LENGTH) | ||||
| @ -362,9 +385,10 @@ rev = revolve({ axis = 'y' }, part009) | ||||
|     }) | ||||
|     await page.waitForTimeout(100) | ||||
|  | ||||
|     const revolve = { x: 646, y: 248 } | ||||
|     const revolve = { x: 635, y: 253 } | ||||
|     const parentExtrude = { x: 915, y: 133 } | ||||
|     const solid2d = { x: 770, y: 167 } | ||||
|     const individualProfile = { x: 694, y: 432 } | ||||
|  | ||||
|     // DELETE REVOLVE | ||||
|     await page.mouse.click(revolve.x, revolve.y) | ||||
| @ -430,6 +454,20 @@ rev = revolve({ axis = 'y' }, part009) | ||||
|     await u.expectCmdLog('[data-message-type="execution-done"]', 10_000) | ||||
|     await page.waitForTimeout(200) | ||||
|     await expect(u.codeLocator).not.toContainText(`sketch005 = startSketchOn({`) | ||||
|  | ||||
|     // Delete a single profile | ||||
|     await page.mouse.click(individualProfile.x, individualProfile.y) | ||||
|     await page.waitForTimeout(100) | ||||
|     const codeToBeDeletedSnippet = | ||||
|       'profile003 = startProfileAt([40.16, -120.48], sketch006)' | ||||
|     await expect(page.locator('.cm-activeLine')).toHaveText( | ||||
|       '  |> line([20.91, -28.61], %)' | ||||
|     ) | ||||
|     await u.clearCommandLogs() | ||||
|     await page.keyboard.press('Backspace') | ||||
|     await u.expectCmdLog('[data-message-type="execution-done"]', 10_000) | ||||
|     await page.waitForTimeout(200) | ||||
|     await expect(u.codeLocator).not.toContainText(codeToBeDeletedSnippet) | ||||
|   }) | ||||
|   test("Deleting solid that the AST mod can't handle results in a toast message", async ({ | ||||
|     page, | ||||
| @ -1258,12 +1296,15 @@ extrude001 = extrude(50, sketch001) | ||||
|  | ||||
|     await page.waitForTimeout(600) | ||||
|  | ||||
|     const firstClickCoords = { x: 650, y: 200 } as const | ||||
|     // Place a point because the line tool will exit if no points are pressed | ||||
|     await page.mouse.click(650, 200) | ||||
|     await page.mouse.click(firstClickCoords.x, firstClickCoords.y) | ||||
|     await page.waitForTimeout(600) | ||||
|  | ||||
|     // Code before exiting the tool | ||||
|     let previousCodeContent = await page.locator('.cm-content').innerText() | ||||
|     let previousCodeContent = ( | ||||
|       await page.locator('.cm-content').innerText() | ||||
|     ).replace(/\s+/g, '') | ||||
|  | ||||
|     // deselect the line tool by clicking it | ||||
|     await page.getByRole('button', { name: 'line Line', exact: true }).click() | ||||
| @ -1275,14 +1316,23 @@ extrude001 = extrude(50, sketch001) | ||||
|     await page.mouse.click(750, 200) | ||||
|     await page.waitForTimeout(100) | ||||
|  | ||||
|     // expect no change | ||||
|     await expect(page.locator('.cm-content')).toHaveText(previousCodeContent) | ||||
|     await expect | ||||
|       .poll(async () => { | ||||
|         let str = await page.locator('.cm-content').innerText() | ||||
|         str = str.replace(/\s+/g, '') | ||||
|         return str | ||||
|       }) | ||||
|       .toBe(previousCodeContent) | ||||
|  | ||||
|     // select line tool again | ||||
|     await page.getByRole('button', { name: 'line Line', exact: true }).click() | ||||
|  | ||||
|     await u.closeDebugPanel() | ||||
|  | ||||
|     // Click to continue profile | ||||
|     await page.mouse.click(firstClickCoords.x, firstClickCoords.y) | ||||
|     await page.waitForTimeout(100) | ||||
|  | ||||
|     // line tool should work as expected again | ||||
|     await page.mouse.click(700, 200) | ||||
|     await expect(page.locator('.cm-content')).not.toHaveText( | ||||
|  | ||||
| @ -224,8 +224,13 @@ test('First escape in tool pops you out of tool, second exits sketch mode', asyn | ||||
|   // Draw a line | ||||
|   await page.mouse.move(700, 200, { steps: 5 }) | ||||
|   await page.mouse.click(700, 200) | ||||
|   await page.mouse.move(800, 250, { steps: 5 }) | ||||
|   await page.mouse.click(800, 250) | ||||
|  | ||||
|   const secondMousePosition = { x: 800, y: 250 } | ||||
|  | ||||
|   await page.mouse.move(secondMousePosition.x, secondMousePosition.y, { | ||||
|     steps: 5, | ||||
|   }) | ||||
|   await page.mouse.click(secondMousePosition.x, secondMousePosition.y) | ||||
|   // Unequip line tool | ||||
|   await page.keyboard.press('Escape') | ||||
|   // Make sure we didn't pop out of sketch mode. | ||||
| @ -234,9 +239,17 @@ test('First escape in tool pops you out of tool, second exits sketch mode', asyn | ||||
|   // Equip arc tool | ||||
|   await page.keyboard.press('a') | ||||
|   await expect(arcButton).toHaveAttribute('aria-pressed', 'true') | ||||
|  | ||||
|   // click in the same position again to continue the profile | ||||
|   await page.mouse.move(secondMousePosition.x, secondMousePosition.y, { | ||||
|     steps: 5, | ||||
|   }) | ||||
|   await page.mouse.click(secondMousePosition.x, secondMousePosition.y) | ||||
|  | ||||
|   await page.mouse.move(1000, 100, { steps: 5 }) | ||||
|   await page.mouse.click(1000, 100) | ||||
|   await page.keyboard.press('Escape') | ||||
|   await expect(arcButton).toHaveAttribute('aria-pressed', 'false') | ||||
|   await page.keyboard.press('l') | ||||
|   await expect(lineButton).toHaveAttribute('aria-pressed', 'true') | ||||
|  | ||||
| @ -537,9 +550,9 @@ test('Sketch on face', async ({ page }) => { | ||||
|  | ||||
|   await expect.poll(u.normalisedEditorCode).toContain( | ||||
|     u.normalisedCode(`sketch002 = startSketchOn(extrude001, seg01) | ||||
|   |> startProfileAt([-12.94, 6.6], %) | ||||
|   |> line([2.45, -0.2], %) | ||||
|   |> line([-2.6, -1.25], %) | ||||
| profile001 = startProfileAt([-12.88, 6.66], sketch002) | ||||
|   |> line([2.71, -0.22], %) | ||||
|   |> line([-2.87, -1.38], %) | ||||
|   |> lineTo([profileStartX(%), profileStartY(%)], %) | ||||
|   |> close(%)`) | ||||
|   ) | ||||
| @ -554,8 +567,7 @@ test('Sketch on face', async ({ page }) => { | ||||
|   await page.getByText('startProfileAt([-12').click() | ||||
|   await expect(page.getByRole('button', { name: 'Edit Sketch' })).toBeVisible() | ||||
|   await page.getByRole('button', { name: 'Edit Sketch' }).click() | ||||
|   await page.waitForTimeout(400) | ||||
|   await page.waitForTimeout(150) | ||||
|   await page.waitForTimeout(500) | ||||
|   await page.setViewportSize({ width: 1200, height: 1200 }) | ||||
|   await u.openAndClearDebugPanel() | ||||
|   await u.updateCamPosition([452, -152, 1166]) | ||||
|  | ||||
| @ -6,7 +6,6 @@ import { useCommandsContext } from 'hooks/useCommandsContext' | ||||
| import { useNetworkContext } from 'hooks/useNetworkContext' | ||||
| import { NetworkHealthState } from 'hooks/useNetworkStatus' | ||||
| import { ActionButton } from 'components/ActionButton' | ||||
| import { isSingleCursorInPipe } from 'lang/queryAst' | ||||
| import { useKclContext } from 'lang/KclProvider' | ||||
| import { ActionButtonDropdown } from 'components/ActionButtonDropdown' | ||||
| import { useHotkeys } from 'react-hotkeys-hook' | ||||
| @ -22,6 +21,7 @@ import { | ||||
| } from 'lib/toolbar' | ||||
| import { isDesktop } from 'lib/isDesktop' | ||||
| import { openExternalBrowserIfDesktop } from 'lib/openWindow' | ||||
| import { isCursorInFunctionDefinition } from 'lang/queryAst' | ||||
|  | ||||
| export function Toolbar({ | ||||
|   className = '', | ||||
| @ -38,7 +38,12 @@ export function Toolbar({ | ||||
|     '!border-transparent hover:!border-chalkboard-20 dark:enabled:hover:!border-primary pressed:!border-primary ui-open:!border-primary' | ||||
|  | ||||
|   const sketchPathId = useMemo(() => { | ||||
|     if (!isSingleCursorInPipe(context.selectionRanges, kclManager.ast)) | ||||
|     if ( | ||||
|       isCursorInFunctionDefinition( | ||||
|         kclManager.ast, | ||||
|         context.selectionRanges.graphSelections[0] | ||||
|       ) | ||||
|     ) | ||||
|       return false | ||||
|     return isCursorInSketchCommandRange( | ||||
|       engineCommandManager.artifactGraph, | ||||
|  | ||||
| @ -433,6 +433,8 @@ export async function deleteSegment({ | ||||
|   if (!sketchDetails) return | ||||
|   await sceneEntitiesManager.updateAstAndRejigSketch( | ||||
|     pathToNode, | ||||
|     sketchDetails.sketchNodePaths, | ||||
|     sketchDetails.planeNodePath, | ||||
|     modifiedAst, | ||||
|     sketchDetails.zAxis, | ||||
|     sketchDetails.yAxis, | ||||
|  | ||||
| @ -691,19 +691,21 @@ export function createProfileStartHandle({ | ||||
|   scale = 1, | ||||
|   theme, | ||||
|   isSelected, | ||||
|   size = 12, | ||||
|   ...rest | ||||
| }: { | ||||
|   from: Coords2d | ||||
|   scale?: number | ||||
|   theme: Themes | ||||
|   isSelected?: boolean | ||||
|   size?: number | ||||
| } & ( | ||||
|   | { isDraft: true } | ||||
|   | { isDraft: false; id: string; pathToNode: PathToNode } | ||||
| )) { | ||||
|   const group = new Group() | ||||
|  | ||||
|   const geometry = new BoxGeometry(12, 12, 12) // in pixels scaled later | ||||
|   const geometry = new BoxGeometry(size, size, size) // in pixels scaled later | ||||
|   const baseColor = getThemeColorForThreeJs(theme) | ||||
|   const color = isSelected ? 0x0000ff : baseColor | ||||
|   const body = new MeshBasicMaterial({ color }) | ||||
|  | ||||
| @ -24,7 +24,7 @@ import { useSetupEngineManager } from 'hooks/useSetupEngineManager' | ||||
| import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' | ||||
| import { | ||||
|   isCursorInSketchCommandRange, | ||||
|   updatePathToNodeFromMap, | ||||
|   updateSketchDetailsNodePaths, | ||||
| } from 'lang/util' | ||||
| import { | ||||
|   kclManager, | ||||
| @ -71,14 +71,24 @@ import { | ||||
|   replaceValueAtNodePath, | ||||
|   sketchOnExtrudedFace, | ||||
|   sketchOnOffsetPlane, | ||||
|   splitPipedProfile, | ||||
|   startSketchOnDefault, | ||||
| } from 'lang/modifyAst' | ||||
| import { PathToNode, Program, parse, recast, resultIsOk } from 'lang/wasm' | ||||
| import { | ||||
|   PathToNode, | ||||
|   Program, | ||||
|   VariableDeclaration, | ||||
|   parse, | ||||
|   recast, | ||||
|   resultIsOk, | ||||
| } from 'lang/wasm' | ||||
| import { | ||||
|   doesSceneHaveExtrudedSketch, | ||||
|   doesSceneHaveSweepableSketch, | ||||
|   getNodePathFromSourceRange, | ||||
|   isSingleCursorInPipe, | ||||
|   doesSketchPipeNeedSplitting, | ||||
|   getNodeFromPath, | ||||
|   isCursorInFunctionDefinition, | ||||
|   traverse, | ||||
| } from 'lang/queryAst' | ||||
| import { exportFromEngine } from 'lib/exportFromEngine' | ||||
| import { Models } from '@kittycad/lib/dist/types/src' | ||||
| @ -86,7 +96,7 @@ import toast from 'react-hot-toast' | ||||
| import { EditorSelection, Transaction } from '@codemirror/state' | ||||
| import { useLoaderData, useNavigate, useSearchParams } from 'react-router-dom' | ||||
| import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls' | ||||
| import { err, reportRejection, trap } from 'lib/trap' | ||||
| import { err, reportRejection, trap, reject } from 'lib/trap' | ||||
| import { useCommandsContext } from 'hooks/useCommandsContext' | ||||
| import { modelingMachineEvent } from 'editor/manager' | ||||
| import { hasValidEdgeTreatmentSelection } from 'lang/modifyAst/addEdgeTreatment' | ||||
| @ -100,6 +110,10 @@ import { useFileContext } from 'hooks/useFileContext' | ||||
| import { uuidv4 } from 'lib/utils' | ||||
| import { IndexLoaderData } from 'lib/types' | ||||
| import { Node } from 'wasm-lib/kcl/bindings/Node' | ||||
| import { | ||||
|   getPathsFromArtifact, | ||||
|   getPlaneFromArtifact, | ||||
| } from 'lang/std/artifactGraph' | ||||
|  | ||||
| type MachineContext<T extends AnyStateMachine> = { | ||||
|   state: StateFrom<T> | ||||
| @ -290,7 +304,7 @@ export const ModelingMachineProvider = ({ | ||||
|           return { | ||||
|             sketchDetails: { | ||||
|               ...sketchDetails, | ||||
|               sketchPathToNode: event.data, | ||||
|               sketchEntryNodePath: event.data, | ||||
|             }, | ||||
|           } | ||||
|         }), | ||||
| @ -413,9 +427,17 @@ export const ModelingMachineProvider = ({ | ||||
|                 selectionRanges: setSelections.selection, | ||||
|                 sketchDetails: { | ||||
|                   ...sketchDetails, | ||||
|                   sketchPathToNode: | ||||
|                     setSelections.updatedPathToNode || | ||||
|                     sketchDetails?.sketchPathToNode || | ||||
|                   sketchEntryNodePath: | ||||
|                     setSelections.updatedSketchEntryNodePath || | ||||
|                     sketchDetails?.sketchEntryNodePath || | ||||
|                     [], | ||||
|                   sketchNodePaths: | ||||
|                     setSelections.updatedSketchNodePaths || | ||||
|                     sketchDetails?.sketchNodePaths || | ||||
|                     [], | ||||
|                   planeNodePath: | ||||
|                     setSelections.updatedPlaneNodePath || | ||||
|                     sketchDetails?.planeNodePath || | ||||
|                     [], | ||||
|                 }, | ||||
|               } | ||||
| @ -570,7 +592,6 @@ export const ModelingMachineProvider = ({ | ||||
|             // BUT only if there's extrudable geometry | ||||
|             return doesSceneHaveSweepableSketch(kclManager.ast) | ||||
|           } | ||||
|           if (!isSketchPipe(selectionRanges)) return false | ||||
|  | ||||
|           const canSweep = canSweepSelection(selectionRanges) | ||||
|           if (err(canSweep)) return false | ||||
| @ -625,7 +646,6 @@ export const ModelingMachineProvider = ({ | ||||
|           } | ||||
|  | ||||
|           const canShell = canShellSelection(selectionRanges) | ||||
|           console.log('canShellSelection', canShellSelection(selectionRanges)) | ||||
|           if (err(canShell)) return false | ||||
|           return canShell | ||||
|         }, | ||||
| @ -648,7 +668,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, | ||||
| @ -679,10 +704,32 @@ export const ModelingMachineProvider = ({ | ||||
|               // this assumes no changes have been made to the sketch besides what we did when entering the sketch | ||||
|               // i.e. doesn't account for user's adding code themselves, maybe we need store a flag userEditedSinceSketchMode? | ||||
|               const newAst = structuredClone(kclManager.ast) | ||||
|               const varDecIndex = sketchDetails.sketchPathToNode[1][0] | ||||
|               const varDecIndex = sketchDetails.planeNodePath[1][0] | ||||
|  | ||||
|               const varDec = getNodeFromPath<VariableDeclaration>( | ||||
|                 newAst, | ||||
|                 sketchDetails.planeNodePath, | ||||
|                 'VariableDeclaration' | ||||
|               ) | ||||
|               if (err(varDec)) return reject(new Error('No varDec')) | ||||
|               const variableName = varDec.node.declaration.id.name | ||||
|               let isIdentifierUsed = false | ||||
|               traverse(newAst, { | ||||
|                 enter: (node) => { | ||||
|                   if ( | ||||
|                     node.type === 'Identifier' && | ||||
|                     node.name === variableName | ||||
|                   ) { | ||||
|                     isIdentifierUsed = true | ||||
|                   } | ||||
|                 }, | ||||
|               }) | ||||
|               if (isIdentifierUsed) return | ||||
|  | ||||
|               // remove body item at varDecIndex | ||||
|               newAst.body = newAst.body.filter((_, i) => i !== varDecIndex) | ||||
|               await kclManager.executeAstMock(newAst) | ||||
|               await codeManager.updateEditorWithAstAndWriteToFile(newAst) | ||||
|             } | ||||
|             sceneInfra.setCallbacks({ | ||||
|               onClick: () => {}, | ||||
| @ -692,7 +739,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' | ||||
| @ -719,7 +766,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, | ||||
| @ -739,7 +788,9 @@ export const ModelingMachineProvider = ({ | ||||
|           ) | ||||
|  | ||||
|           return { | ||||
|             sketchPathToNode: pathToNode, | ||||
|             sketchEntryNodePath: [], | ||||
|             planeNodePath: pathToNode, | ||||
|             sketchNodePaths: [], | ||||
|             zAxis: input.zAxis, | ||||
|             yAxis: input.yAxis, | ||||
|             origin: [0, 0, 0], | ||||
| @ -747,12 +798,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 || [] | ||||
|             ) | ||||
| @ -760,8 +813,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( | ||||
| @ -773,7 +835,7 @@ export const ModelingMachineProvider = ({ | ||||
|  | ||||
|         'Get horizontal info': fromPromise( | ||||
|           async ({ input: { selectionRanges, sketchDetails } }) => { | ||||
|             const { modifiedAst, pathToNodeMap } = | ||||
|             const { modifiedAst, pathToNodeMap, exprInsertIndex } = | ||||
|               await applyConstraintHorzVertDistance({ | ||||
|                 constraint: 'setHorzDistance', | ||||
|                 selectionRanges, | ||||
| @ -785,13 +847,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, | ||||
| @ -812,13 +884,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, | ||||
| @ -829,13 +903,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, | ||||
| @ -856,7 +940,9 @@ export const ModelingMachineProvider = ({ | ||||
|             return { | ||||
|               selectionType: 'completeSelection', | ||||
|               selection, | ||||
|               updatedPathToNode, | ||||
|               updatedSketchEntryNodePath, | ||||
|               updatedSketchNodePaths, | ||||
|               updatedPlaneNodePath, | ||||
|             } | ||||
|           } | ||||
|         ), | ||||
| @ -866,7 +952,8 @@ export const ModelingMachineProvider = ({ | ||||
|               selectionRanges, | ||||
|             }) | ||||
|             if (err(info)) return Promise.reject(info) | ||||
|             const { modifiedAst, pathToNodeMap } = await (info.enabled | ||||
|             const { modifiedAst, pathToNodeMap, exprInsertIndex } = | ||||
|               await (info.enabled | ||||
|                 ? applyConstraintAngleBetween({ | ||||
|                     selectionRanges, | ||||
|                   }) | ||||
| @ -882,13 +969,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, | ||||
| @ -909,7 +1006,9 @@ export const ModelingMachineProvider = ({ | ||||
|             return { | ||||
|               selectionType: 'completeSelection', | ||||
|               selection, | ||||
|               updatedPathToNode, | ||||
|               updatedSketchEntryNodePath, | ||||
|               updatedSketchNodePaths, | ||||
|               updatedPlaneNodePath, | ||||
|             } | ||||
|           } | ||||
|         ), | ||||
| @ -924,20 +1023,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, | ||||
| @ -958,13 +1067,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, | ||||
|               }) | ||||
| @ -974,13 +1085,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, | ||||
| @ -1001,13 +1121,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, | ||||
| @ -1018,13 +1140,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, | ||||
| @ -1045,13 +1176,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, | ||||
| @ -1062,13 +1195,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, | ||||
| @ -1089,7 +1231,9 @@ export const ModelingMachineProvider = ({ | ||||
|             return { | ||||
|               selectionType: 'completeSelection', | ||||
|               selection, | ||||
|               updatedPathToNode, | ||||
|               updatedSketchEntryNodePath, | ||||
|               updatedSketchNodePaths, | ||||
|               updatedPlaneNodePath, | ||||
|             } | ||||
|           } | ||||
|         ), | ||||
| @ -1109,9 +1253,11 @@ export const ModelingMachineProvider = ({ | ||||
|             let result: { | ||||
|               modifiedAst: Node<Program> | ||||
|               pathToReplaced: PathToNode | null | ||||
|               exprInsertIndex: number | ||||
|             } = { | ||||
|               modifiedAst: parsed, | ||||
|               pathToReplaced: null, | ||||
|               exprInsertIndex: -1, | ||||
|             } | ||||
|             // If the user provided a constant name, | ||||
|             // we need to insert the named constant | ||||
| @ -1141,6 +1287,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, | ||||
| @ -1171,10 +1318,22 @@ export const ModelingMachineProvider = ({ | ||||
|             parsed = parsed as Node<Program> | ||||
|             if (!result.pathToReplaced) | ||||
|               return Promise.reject(new Error('No path to replaced node')) | ||||
|             const { | ||||
|               updatedSketchEntryNodePath, | ||||
|               updatedSketchNodePaths, | ||||
|               updatedPlaneNodePath, | ||||
|             } = updateSketchDetailsNodePaths({ | ||||
|               sketchEntryNodePath: sketchDetails.sketchEntryNodePath, | ||||
|               sketchNodePaths: sketchDetails.sketchNodePaths, | ||||
|               planeNodePath: sketchDetails.planeNodePath, | ||||
|               exprInsertIndex: result.exprInsertIndex, | ||||
|             }) | ||||
|  | ||||
|             const updatedAst = | ||||
|               await sceneEntitiesManager.updateAstAndRejigSketch( | ||||
|                 result.pathToReplaced || [], | ||||
|                 updatedSketchEntryNodePath, | ||||
|                 updatedSketchNodePaths, | ||||
|                 updatedPlaneNodePath, | ||||
|                 parsed, | ||||
|                 sketchDetails.zAxis, | ||||
|                 sketchDetails.yAxis, | ||||
| @ -1195,7 +1354,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, | ||||
|             } | ||||
|           } | ||||
|         ), | ||||
|  | ||||
| @ -136,6 +136,7 @@ export async function applyConstraintIntersect({ | ||||
| }): Promise<{ | ||||
|   modifiedAst: Node<Program> | ||||
|   pathToNodeMap: PathToNodeMap | ||||
|   exprInsertIndex: number | ||||
| }> { | ||||
|   const info = intersectInfo({ | ||||
|     selectionRanges, | ||||
| @ -174,6 +175,7 @@ export async function applyConstraintIntersect({ | ||||
|     return { | ||||
|       modifiedAst, | ||||
|       pathToNodeMap, | ||||
|       exprInsertIndex: -1, | ||||
|     } | ||||
|   } | ||||
|   // transform again but forcing certain values | ||||
| @ -192,6 +194,7 @@ export async function applyConstraintIntersect({ | ||||
|   const { modifiedAst: _modifiedAst, pathToNodeMap: _pathToNodeMap } = | ||||
|     transform2 | ||||
|  | ||||
|   let exprInsertIndex = -1 | ||||
|   if (variableName) { | ||||
|     const newBody = [..._modifiedAst.body] | ||||
|     newBody.splice( | ||||
| @ -204,9 +207,11 @@ export async function applyConstraintIntersect({ | ||||
|       const index = pathToNode.findIndex((a) => a[0] === 'body') + 1 | ||||
|       pathToNode[index][0] = Number(pathToNode[index][0]) + 1 | ||||
|     }) | ||||
|     exprInsertIndex = newVariableInsertIndex | ||||
|   } | ||||
|   return { | ||||
|     modifiedAst: _modifiedAst, | ||||
|     pathToNodeMap: _pathToNodeMap, | ||||
|     exprInsertIndex, | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -28,7 +28,7 @@ export function removeConstrainingValuesInfo({ | ||||
|   | Error { | ||||
|   const _nodes = selectionRanges.graphSelections.map(({ codeRef }) => { | ||||
|     const tmp = getNodeFromPath<Expr>(kclManager.ast, codeRef.pathToNode) | ||||
|     if (err(tmp)) return tmp | ||||
|     if (tmp instanceof Error) return tmp | ||||
|     return tmp.node | ||||
|   }) | ||||
|   const _err1 = _nodes.find(err) | ||||
|  | ||||
| @ -93,6 +93,7 @@ export async function applyConstraintAbsDistance({ | ||||
| }): Promise<{ | ||||
|   modifiedAst: Program | ||||
|   pathToNodeMap: PathToNodeMap | ||||
|   exprInsertIndex: number | ||||
| }> { | ||||
|   const info = absDistanceInfo({ | ||||
|     selectionRanges, | ||||
| @ -132,6 +133,7 @@ export async function applyConstraintAbsDistance({ | ||||
|   if (err(transform2)) return Promise.reject(transform2) | ||||
|   const { modifiedAst: _modifiedAst, pathToNodeMap } = transform2 | ||||
|  | ||||
|   let exprInsertIndex = -1 | ||||
|   if (variableName) { | ||||
|     const newBody = [..._modifiedAst.body] | ||||
|     newBody.splice( | ||||
| @ -144,8 +146,9 @@ export async function applyConstraintAbsDistance({ | ||||
|       const index = pathToNode.findIndex((a) => a[0] === 'body') + 1 | ||||
|       pathToNode[index][0] = Number(pathToNode[index][0]) + 1 | ||||
|     }) | ||||
|     exprInsertIndex = newVariableInsertIndex | ||||
|   } | ||||
|   return { modifiedAst: _modifiedAst, pathToNodeMap } | ||||
|   return { modifiedAst: _modifiedAst, pathToNodeMap, exprInsertIndex } | ||||
| } | ||||
|  | ||||
| export function applyConstraintAxisAlign({ | ||||
|  | ||||
| @ -86,6 +86,7 @@ export async function applyConstraintAngleBetween({ | ||||
| }): Promise<{ | ||||
|   modifiedAst: Program | ||||
|   pathToNodeMap: PathToNodeMap | ||||
|   exprInsertIndex: number | ||||
| }> { | ||||
|   const info = angleBetweenInfo({ selectionRanges }) | ||||
|   if (err(info)) return Promise.reject(info) | ||||
| @ -122,6 +123,7 @@ export async function applyConstraintAngleBetween({ | ||||
|     return { | ||||
|       modifiedAst, | ||||
|       pathToNodeMap, | ||||
|       exprInsertIndex: -1, | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @ -141,6 +143,7 @@ export async function applyConstraintAngleBetween({ | ||||
|   const { modifiedAst: _modifiedAst, pathToNodeMap: _pathToNodeMap } = | ||||
|     transformed2 | ||||
|  | ||||
|   let exprInsertIndex = -1 | ||||
|   if (variableName) { | ||||
|     const newBody = [..._modifiedAst.body] | ||||
|     newBody.splice( | ||||
| @ -153,9 +156,11 @@ export async function applyConstraintAngleBetween({ | ||||
|       const index = pathToNode.findIndex((a) => a[0] === 'body') + 1 | ||||
|       pathToNode[index][0] = Number(pathToNode[index][0]) + 1 | ||||
|     }) | ||||
|     exprInsertIndex = newVariableInsertIndex | ||||
|   } | ||||
|   return { | ||||
|     modifiedAst: _modifiedAst, | ||||
|     pathToNodeMap: _pathToNodeMap, | ||||
|     exprInsertIndex, | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -87,15 +87,13 @@ export function horzVertDistanceInfo({ | ||||
| export async function applyConstraintHorzVertDistance({ | ||||
|   selectionRanges, | ||||
|   constraint, | ||||
|   // TODO align will always be false (covered by synconous applyConstraintHorzVertAlign), remove it | ||||
|   isAlign = false, | ||||
| }: { | ||||
|   selectionRanges: Selections | ||||
|   constraint: 'setHorzDistance' | 'setVertDistance' | ||||
|   isAlign?: false | ||||
| }): Promise<{ | ||||
|   modifiedAst: Program | ||||
|   pathToNodeMap: PathToNodeMap | ||||
|   exprInsertIndex: number | ||||
| }> { | ||||
|   const info = horzVertDistanceInfo({ | ||||
|     selectionRanges: selectionRanges, | ||||
| @ -133,13 +131,12 @@ export async function applyConstraintHorzVertDistance({ | ||||
|     return { | ||||
|       modifiedAst, | ||||
|       pathToNodeMap, | ||||
|       exprInsertIndex: -1, | ||||
|     } | ||||
|   } else { | ||||
|     if (!isExprBinaryPart(valueNode)) | ||||
|       return Promise.reject('Invalid valueNode, is not a BinaryPart') | ||||
|     let finalValue = isAlign | ||||
|       ? createLiteral(0) | ||||
|       : removeDoubleNegatives(valueNode, sign, variableName) | ||||
|     let finalValue = removeDoubleNegatives(valueNode, sign, variableName) | ||||
|     // transform again but forcing certain values | ||||
|     const transformed = transformSecondarySketchLinesTagFirst({ | ||||
|       ast: kclManager.ast, | ||||
| @ -152,6 +149,7 @@ export async function applyConstraintHorzVertDistance({ | ||||
|  | ||||
|     if (err(transformed)) return Promise.reject(transformed) | ||||
|     const { modifiedAst: _modifiedAst, pathToNodeMap } = transformed | ||||
|     let exprInsertIndex = -1 | ||||
|     if (variableName) { | ||||
|       const newBody = [..._modifiedAst.body] | ||||
|       newBody.splice( | ||||
| @ -164,10 +162,12 @@ export async function applyConstraintHorzVertDistance({ | ||||
|         const index = pathToNode.findIndex((a) => a[0] === 'body') + 1 | ||||
|         pathToNode[index][0] = Number(pathToNode[index][0]) + 1 | ||||
|       }) | ||||
|       exprInsertIndex = newVariableInsertIndex | ||||
|     } | ||||
|     return { | ||||
|       modifiedAst: _modifiedAst, | ||||
|       pathToNodeMap, | ||||
|       exprInsertIndex, | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -70,10 +70,14 @@ export async function applyConstraintLength({ | ||||
| }: { | ||||
|   length: KclCommandValue | ||||
|   selectionRanges: Selections | ||||
| }) { | ||||
| }): Promise<{ | ||||
|   modifiedAst: Program | ||||
|   pathToNodeMap: PathToNodeMap | ||||
|   exprInsertIndex: number | ||||
| }> { | ||||
|   const ast = kclManager.ast | ||||
|   const angleLength = angleLengthInfo({ selectionRanges }) | ||||
|   if (err(angleLength)) return angleLength | ||||
|   if (err(angleLength)) return Promise.reject(angleLength) | ||||
|   const { transforms } = angleLength | ||||
|  | ||||
|   let distanceExpression: Expr = length.valueAst | ||||
| @ -94,7 +98,7 @@ export async function applyConstraintLength({ | ||||
|   } | ||||
|  | ||||
|   if (!isExprBinaryPart(distanceExpression)) { | ||||
|     return new Error('Invalid valueNode, is not a BinaryPart') | ||||
|     return Promise.reject('Invalid valueNode, is not a BinaryPart') | ||||
|   } | ||||
|  | ||||
|   const retval = transformAstSketchLines({ | ||||
| @ -112,6 +116,12 @@ export async function applyConstraintLength({ | ||||
|   return { | ||||
|     modifiedAst: _modifiedAst, | ||||
|     pathToNodeMap, | ||||
|     exprInsertIndex: | ||||
|       'variableName' in length && | ||||
|       length.variableName && | ||||
|       length.insertIndex !== undefined | ||||
|         ? length.insertIndex | ||||
|         : -1, | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -124,6 +134,7 @@ export async function applyConstraintAngleLength({ | ||||
| }): Promise<{ | ||||
|   modifiedAst: Program | ||||
|   pathToNodeMap: PathToNodeMap | ||||
|   exprInsertIndex: number | ||||
| }> { | ||||
|   const angleLength = angleLengthInfo({ selectionRanges, angleOrLength }) | ||||
|   if (err(angleLength)) return Promise.reject(angleLength) | ||||
| @ -208,5 +219,6 @@ export async function applyConstraintAngleLength({ | ||||
|   return { | ||||
|     modifiedAst: _modifiedAst, | ||||
|     pathToNodeMap, | ||||
|     exprInsertIndex: variableName ? newVariableInsertIndex : -1, | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -311,8 +311,6 @@ export class KclManager { | ||||
|     // Do not send send scene commands if the program was interrupted, go to clean up | ||||
|     if (!isInterrupted) { | ||||
|       this.addDiagnostics(await lintAst({ ast: ast })) | ||||
|  | ||||
|       sceneInfra.modelingSend({ type: 'code edit during sketch' }) | ||||
|       setSelectionFilterToDefault(execState.memory, this.engineCommandManager) | ||||
|  | ||||
|       if (args.zoomToFit) { | ||||
| @ -358,7 +356,11 @@ export class KclManager { | ||||
|       this.lastSuccessfulProgramMemory = execState.memory | ||||
|     } | ||||
|     this.ast = { ...ast } | ||||
|     // updateArtifactGraph relies on updated executeState/programMemory | ||||
|     await this.engineCommandManager.updateArtifactGraph(this.ast) | ||||
|     this._executeCallback() | ||||
|     if (!isInterrupted) | ||||
|       sceneInfra.modelingSend({ type: 'code edit during sketch' }) | ||||
|     this.engineCommandManager.addCommandLog({ | ||||
|       type: 'execution-done', | ||||
|       data: null, | ||||
| @ -400,6 +402,7 @@ export class KclManager { | ||||
|  | ||||
|     this._logs = logs | ||||
|     this.addDiagnostics(kclErrorsToDiagnostics(errors)) | ||||
|  | ||||
|     this._execState = execState | ||||
|     this._programMemory = execState.memory | ||||
|     if (!errors.length) { | ||||
| @ -411,7 +414,7 @@ export class KclManager { | ||||
|     // problem this solves, but either way we should strive to remove it. | ||||
|     Array.from(this.engineCommandManager.artifactGraph).forEach( | ||||
|       ([commandId, artifact]) => { | ||||
|         if (!('codeRef' in artifact)) return | ||||
|         if (!('codeRef' in artifact && artifact.codeRef)) return | ||||
|         const _node1 = getNodeFromPath<Node<CallExpression>>( | ||||
|           this.ast, | ||||
|           artifact.codeRef.pathToNode, | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| import { | ||||
|   Program, | ||||
|   _executor, | ||||
|   executor, | ||||
|   ProgramMemory, | ||||
|   kclLint, | ||||
|   emptyExecState, | ||||
| @ -64,12 +64,9 @@ export async function executeAst({ | ||||
|   try { | ||||
|     const execState = await (programMemoryOverride | ||||
|       ? enginelessExecutor(ast, programMemoryOverride) | ||||
|       : _executor(ast, engineCommandManager)) | ||||
|  | ||||
|     await engineCommandManager.waitForAllCommands( | ||||
|       programMemoryOverride !== undefined | ||||
|     ) | ||||
|       : executor(ast, engineCommandManager)) | ||||
|  | ||||
|     await engineCommandManager.waitForAllCommands() | ||||
|     return { | ||||
|       logs: [], | ||||
|       errors: [], | ||||
|  | ||||
| @ -16,6 +16,7 @@ import { | ||||
|   deleteSegmentFromPipeExpression, | ||||
|   removeSingleConstraintInfo, | ||||
|   deleteFromSelection, | ||||
|   splitPipedProfile, | ||||
| } from './modifyAst' | ||||
| import { enginelessExecutor } from '../lib/testHelpers' | ||||
| import { findUsesOfTagInPipe, getNodePathFromSourceRange } from './queryAst' | ||||
| @ -918,3 +919,63 @@ sketch002 = startSketchOn({ | ||||
|     } | ||||
|   ) | ||||
| }) | ||||
|  | ||||
| describe('Testing splitPipedProfile', () => { | ||||
|   it('should split the pipe expression correctly', () => { | ||||
|     const codeBefore = `part001 = startSketchOn('XZ') | ||||
|   |> startProfileAt([1, 2], %) | ||||
|   |> line([3, 4], %) | ||||
|   |> line([5, 6], %) | ||||
|   |> close(%) | ||||
| extrude001 = extrude(5, part001) | ||||
|     ` | ||||
|  | ||||
|     const expectedCodeAfter = `sketch001 = startSketchOn('XZ') | ||||
| part001 = startProfileAt([1, 2], sketch001) | ||||
|   |> line([3, 4], %) | ||||
|   |> line([5, 6], %) | ||||
|   |> close(%) | ||||
| extrude001 = extrude(5, part001) | ||||
|     ` | ||||
|  | ||||
|     const ast = assertParse(codeBefore) | ||||
|  | ||||
|     const codeOfInterest = `startSketchOn('XZ')` | ||||
|     const range: [number, number, boolean] = [ | ||||
|       codeBefore.indexOf(codeOfInterest), | ||||
|       codeBefore.indexOf(codeOfInterest) + codeOfInterest.length, | ||||
|       true, | ||||
|     ] | ||||
|     const pathToPipe = getNodePathFromSourceRange(ast, range) | ||||
|  | ||||
|     const result = splitPipedProfile(ast, pathToPipe) | ||||
|  | ||||
|     if (err(result)) throw result | ||||
|  | ||||
|     const newCode = recast(result.modifiedAst) | ||||
|     if (err(newCode)) throw newCode | ||||
|     expect(newCode.trim()).toBe(expectedCodeAfter.trim()) | ||||
|   }) | ||||
|   it('should return error for already split pipe', () => { | ||||
|     const codeBefore = `sketch001 = startSketchOn('XZ') | ||||
| part001 = startProfileAt([1, 2], sketch001) | ||||
|   |> line([3, 4], %) | ||||
|   |> line([5, 6], %) | ||||
|   |> close(%) | ||||
| extrude001 = extrude(5, part001) | ||||
|     ` | ||||
|  | ||||
|     const ast = assertParse(codeBefore) | ||||
|  | ||||
|     const codeOfInterest = `startProfileAt([1, 2], sketch001)` | ||||
|     const range: [number, number, boolean] = [ | ||||
|       codeBefore.indexOf(codeOfInterest), | ||||
|       codeBefore.indexOf(codeOfInterest) + codeOfInterest.length, | ||||
|       true, | ||||
|     ] | ||||
|     const pathToPipe = getNodePathFromSourceRange(ast, range) | ||||
|  | ||||
|     const result = splitPipedProfile(ast, pathToPipe) | ||||
|     expect(result instanceof Error).toBe(true) | ||||
|   }) | ||||
| }) | ||||
|  | ||||
| @ -29,6 +29,8 @@ import { | ||||
|   getNodePathFromSourceRange, | ||||
|   isNodeSafeToReplace, | ||||
|   traverse, | ||||
|   getBodyIndex, | ||||
|   isCallExprWithName, | ||||
| } from './queryAst' | ||||
| import { addTagForSketchOnFace, getConstraintInfo } from './std/sketch' | ||||
| import { | ||||
| @ -46,6 +48,7 @@ import { Models } from '@kittycad/lib' | ||||
| import { ExtrudeFacePlane } from 'machines/modelingMachine' | ||||
| import { Node } from 'wasm-lib/kcl/bindings/Node' | ||||
| import { KclExpressionWithVariable } from 'lib/commandTypes' | ||||
| import { Artifact, getPathsFromArtifact } from './std/artifactGraph' | ||||
|  | ||||
| export function startSketchOnDefault( | ||||
|   node: Node<Program>, | ||||
| @ -78,41 +81,54 @@ export function startSketchOnDefault( | ||||
|   } | ||||
| } | ||||
|  | ||||
| export function addStartProfileAt( | ||||
| export function insertNewStartProfileAt( | ||||
|   node: Node<Program>, | ||||
|   pathToNode: PathToNode, | ||||
|   at: [number, number] | ||||
| ): { modifiedAst: Node<Program>; pathToNode: PathToNode } | Error { | ||||
|   const _node1 = getNodeFromPath<VariableDeclaration>( | ||||
|     node, | ||||
|     pathToNode, | ||||
|     'VariableDeclaration' | ||||
|   ) | ||||
|   if (err(_node1)) return _node1 | ||||
|   const variableDeclaration = _node1.node | ||||
|   if (variableDeclaration.type !== 'VariableDeclaration') { | ||||
|     return new Error('variableDeclaration.init.type !== PipeExpression') | ||||
|   sketchEntryNodePath: PathToNode, | ||||
|   sketchNodePaths: PathToNode[], | ||||
|   planeNodePath: PathToNode, | ||||
|   at: [number, number], | ||||
|   insertType: 'start' | 'end' = 'end' | ||||
| ): | ||||
|   | { | ||||
|       modifiedAst: Node<Program> | ||||
|       updatedSketchNodePaths: PathToNode[] | ||||
|       updatedEntryNodePath: PathToNode | ||||
|     } | ||||
|   const _node = { ...node } | ||||
|   const init = variableDeclaration.declaration.init | ||||
|   const startProfileAt = createCallExpressionStdLib('startProfileAt', [ | ||||
|   | Error { | ||||
|   const varDec = getNodeFromPath<VariableDeclarator>( | ||||
|     node, | ||||
|     planeNodePath, | ||||
|     'VariableDeclarator' | ||||
|   ) | ||||
|   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])), | ||||
|       ]), | ||||
|     createPipeSubstitution(), | ||||
|       createIdentifier(varDec.node.id.name), | ||||
|     ]) | ||||
|   if (init.type === 'PipeExpression') { | ||||
|     init.body.splice(1, 0, startProfileAt) | ||||
|   } else { | ||||
|     variableDeclaration.declaration.init = createPipeExpression([ | ||||
|       init, | ||||
|       startProfileAt, | ||||
|     ]) | ||||
|   } | ||||
|   ) | ||||
|   const insertIndex = getInsertIndex(sketchNodePaths, planeNodePath, insertType) | ||||
|  | ||||
|   const _node = structuredClone(node) | ||||
|   // TODO the rest of this function will not be robust to work for sketches defined within a function declaration | ||||
|   _node.body.splice(insertIndex, 0, newExpression) | ||||
|  | ||||
|   const { updatedEntryNodePath, updatedSketchNodePaths } = | ||||
|     updateSketchNodePathsWithInsertIndex({ | ||||
|       insertIndex, | ||||
|       insertType, | ||||
|       sketchNodePaths, | ||||
|     }) | ||||
|   return { | ||||
|     modifiedAst: _node, | ||||
|     pathToNode, | ||||
|     updatedSketchNodePaths, | ||||
|     updatedEntryNodePath, | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -253,7 +269,7 @@ export function mutateObjExpProp( | ||||
| export function extrudeSketch( | ||||
|   node: Node<Program>, | ||||
|   pathToNode: PathToNode, | ||||
|   shouldPipe = false, | ||||
|   artifact?: Artifact, | ||||
|   distance: Expr = createLiteral(4) | ||||
| ): | ||||
|   | { | ||||
| @ -262,10 +278,14 @@ export function extrudeSketch( | ||||
|       pathToExtrudeArg: PathToNode | ||||
|     } | ||||
|   | Error { | ||||
|   const orderedSketchNodePaths = getPathsFromArtifact({ | ||||
|     artifact: artifact, | ||||
|     sketchPathToNode: pathToNode, | ||||
|   }) | ||||
|   if (err(orderedSketchNodePaths)) return orderedSketchNodePaths | ||||
|   const _node = structuredClone(node) | ||||
|   const _node1 = getNodeFromPath(_node, pathToNode) | ||||
|   if (err(_node1)) return _node1 | ||||
|   const { node: sketchExpression } = _node1 | ||||
|  | ||||
|   // determine if sketchExpression is in a pipeExpression or not | ||||
|   const _node2 = getNodeFromPath<PipeExpression>( | ||||
| @ -274,9 +294,6 @@ export function extrudeSketch( | ||||
|     'PipeExpression' | ||||
|   ) | ||||
|   if (err(_node2)) return _node2 | ||||
|   const { node: pipeExpression } = _node2 | ||||
|  | ||||
|   const isInPipeExpression = pipeExpression.type === 'PipeExpression' | ||||
|  | ||||
|   const _node3 = getNodeFromPath<VariableDeclarator>( | ||||
|     _node, | ||||
| @ -284,49 +301,23 @@ export function extrudeSketch( | ||||
|     'VariableDeclarator' | ||||
|   ) | ||||
|   if (err(_node3)) return _node3 | ||||
|   const { node: variableDeclarator, shallowPath: pathToDecleration } = _node3 | ||||
|   const { node: variableDeclarator } = _node3 | ||||
|  | ||||
|   const extrudeCall = createCallExpressionStdLib('extrude', [ | ||||
|     distance, | ||||
|     shouldPipe | ||||
|       ? createPipeSubstitution() | ||||
|       : createIdentifier(variableDeclarator.id.name), | ||||
|     createIdentifier(variableDeclarator.id.name), | ||||
|   ]) | ||||
|  | ||||
|   if (shouldPipe) { | ||||
|     const pipeChain = createPipeExpression( | ||||
|       isInPipeExpression | ||||
|         ? [...pipeExpression.body, extrudeCall] | ||||
|         : [sketchExpression as any, extrudeCall] | ||||
|     ) | ||||
|  | ||||
|     variableDeclarator.init = pipeChain | ||||
|     const pathToExtrudeArg: PathToNode = [ | ||||
|       ...pathToDecleration, | ||||
|       ['init', 'VariableDeclarator'], | ||||
|       ['body', ''], | ||||
|       [pipeChain.body.length - 1, 'index'], | ||||
|       ['arguments', 'CallExpression'], | ||||
|       [0, 'index'], | ||||
|     ] | ||||
|  | ||||
|     return { | ||||
|       modifiedAst: _node, | ||||
|       pathToNode, | ||||
|       pathToExtrudeArg, | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // We're not creating a pipe expression, | ||||
|   // but rather a separate constant for the extrusion | ||||
|   const name = findUniqueName(node, KCL_DEFAULT_CONSTANT_PREFIXES.EXTRUDE) | ||||
|   const VariableDeclaration = createVariableDeclaration(name, extrudeCall) | ||||
|  | ||||
|   const sketchIndexInPathToNode = | ||||
|     pathToDecleration.findIndex((a) => a[0] === 'body') + 1 | ||||
|   const sketchIndexInBody = pathToDecleration[ | ||||
|     sketchIndexInPathToNode | ||||
|   ][0] as number | ||||
|   const lastSketchNodePath = | ||||
|     orderedSketchNodePaths[orderedSketchNodePaths.length - 1] | ||||
|  | ||||
|   console.log('lastSketchNodePath', lastSketchNodePath, orderedSketchNodePaths) | ||||
|   const sketchIndexInBody = Number(lastSketchNodePath[1][0]) | ||||
|   _node.body.splice(sketchIndexInBody + 1, 0, VariableDeclaration) | ||||
|  | ||||
|   const pathToExtrudeArg: PathToNode = [ | ||||
| @ -1295,7 +1286,8 @@ export async function deleteFromSelection( | ||||
|     const pipeBody = varDec.node.init.body | ||||
|     if ( | ||||
|       pipeBody[0].type === 'CallExpression' && | ||||
|       pipeBody[0].callee.name === 'startSketchOn' | ||||
|       (pipeBody[0].callee.name === 'startSketchOn' || | ||||
|         pipeBody[0].callee.name === 'startProfileAt') | ||||
|     ) { | ||||
|       // remove varDec | ||||
|       const varDecIndex = varDec.shallowPath[1][0] as number | ||||
| @ -1310,3 +1302,149 @@ export async function deleteFromSelection( | ||||
| const nonCodeMetaEmpty = () => { | ||||
|   return { nonCodeNodes: {}, startNodes: [], start: 0, end: 0 } | ||||
| } | ||||
|  | ||||
| export function getInsertIndex( | ||||
|   sketchNodePaths: PathToNode[], | ||||
|   planeNodePath: PathToNode, | ||||
|   insertType: 'start' | 'end' | ||||
| ) { | ||||
|   let minIndex = 0 | ||||
|   let maxIndex = 0 | ||||
|   for (const path of sketchNodePaths) { | ||||
|     const index = Number(path[1][0]) | ||||
|     if (index < minIndex) minIndex = index | ||||
|     if (index > maxIndex) maxIndex = index | ||||
|   } | ||||
|  | ||||
|   const insertIndex = !sketchNodePaths.length | ||||
|     ? Number(planeNodePath[1][0]) + 1 | ||||
|     : insertType === 'start' | ||||
|     ? minIndex | ||||
|     : maxIndex + 1 | ||||
|   return insertIndex | ||||
| } | ||||
|  | ||||
| export function updateSketchNodePathsWithInsertIndex({ | ||||
|   insertIndex, | ||||
|   insertType, | ||||
|   sketchNodePaths, | ||||
| }: { | ||||
|   insertIndex: number | ||||
|   insertType: 'start' | 'end' | ||||
|   sketchNodePaths: PathToNode[] | ||||
| }): { | ||||
|   updatedEntryNodePath: PathToNode | ||||
|   updatedSketchNodePaths: PathToNode[] | ||||
| } { | ||||
|   // TODO the rest of this function will not be robust to work for sketches defined within a function declaration | ||||
|   const newExpressionPathToNode: PathToNode = [ | ||||
|     ['body', ''], | ||||
|     [insertIndex, 'index'], | ||||
|     ['declaration', 'VariableDeclaration'], | ||||
|     ['init', 'VariableDeclarator'], | ||||
|   ] | ||||
|   let updatedSketchNodePaths = structuredClone(sketchNodePaths) | ||||
|   if (insertType === 'start') { | ||||
|     updatedSketchNodePaths = updatedSketchNodePaths.map((path) => { | ||||
|       path[1][0] = Number(path[1][0]) + 1 | ||||
|       return path | ||||
|     }) | ||||
|     updatedSketchNodePaths.unshift(newExpressionPathToNode) | ||||
|   } else { | ||||
|     updatedSketchNodePaths.push(newExpressionPathToNode) | ||||
|   } | ||||
|   return { | ||||
|     updatedSketchNodePaths, | ||||
|     updatedEntryNodePath: newExpressionPathToNode, | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  *  | ||||
|  * Split the following pipe expression into  | ||||
|  * ```ts | ||||
|  * part001 = startSketchOn('XZ') | ||||
|   |> startProfileAt([1, 2], %) | ||||
|   |> line([3, 4], %) | ||||
|   |> line([5, 6], %) | ||||
|   |> close(%) | ||||
| extrude001 = extrude(5, part001) | ||||
| ``` | ||||
| into | ||||
| ```ts | ||||
| sketch001 = startSketchOn('XZ') | ||||
| part001 = startProfileAt([1, 2], sketch001) | ||||
|   |> line([3, 4], %) | ||||
|   |> line([5, 6], %) | ||||
|   |> close(%) | ||||
| extrude001 = extrude(5, part001) | ||||
| ``` | ||||
| Notice that the `startSketchOn` is what gets the new variable name, this is so part001 still has the same data as before | ||||
| making it safe for later code that uses part001 (the extrude in this example) | ||||
|  *  | ||||
|  */ | ||||
| export function splitPipedProfile( | ||||
|   ast: Program, | ||||
|   pathToPipe: PathToNode | ||||
| ): | ||||
|   | { | ||||
|       modifiedAst: Program | ||||
|       pathToProfile: PathToNode | ||||
|       pathToPlane: PathToNode | ||||
|     } | ||||
|   | Error { | ||||
|   const _ast = structuredClone(ast) | ||||
|   const varDec = getNodeFromPath<VariableDeclaration>( | ||||
|     _ast, | ||||
|     pathToPipe, | ||||
|     'VariableDeclaration' | ||||
|   ) | ||||
|   if (err(varDec)) return varDec | ||||
|   if ( | ||||
|     varDec.node.type !== 'VariableDeclaration' || | ||||
|     varDec.node.declaration.init.type !== 'PipeExpression' | ||||
|   ) { | ||||
|     return new Error('pathToNode does not point to pipe') | ||||
|   } | ||||
|   const init = varDec.node.declaration.init | ||||
|   const firstCall = init.body[0] | ||||
|   if (!isCallExprWithName(firstCall, 'startSketchOn')) | ||||
|     return new Error('First call is not startSketchOn') | ||||
|   const secondCall = init.body[1] | ||||
|   if (!isCallExprWithName(secondCall, 'startProfileAt')) | ||||
|     return new Error('Second call is not startProfileAt') | ||||
|  | ||||
|   const varName = varDec.node.declaration.id.name | ||||
|   const newVarName = findUniqueName(_ast, 'sketch') | ||||
|   const secondCallArgs = structuredClone(secondCall.arguments) | ||||
|   secondCallArgs[1] = createIdentifier(newVarName) | ||||
|   const firstCallOfNewPipe = createCallExpression( | ||||
|     'startProfileAt', | ||||
|     secondCallArgs | ||||
|   ) | ||||
|   const newSketch = createVariableDeclaration( | ||||
|     newVarName, | ||||
|     varDec.node.declaration.init.body[0] | ||||
|   ) | ||||
|   const newProfile = createVariableDeclaration( | ||||
|     varName, | ||||
|     varDec.node.declaration.init.body.length <= 2 | ||||
|       ? firstCallOfNewPipe | ||||
|       : createPipeExpression([ | ||||
|           firstCallOfNewPipe, | ||||
|           ...varDec.node.declaration.init.body.slice(2), | ||||
|         ]) | ||||
|   ) | ||||
|   const index = getBodyIndex(pathToPipe) | ||||
|   if (err(index)) return index | ||||
|   _ast.body.splice(index, 1, newSketch, newProfile) | ||||
|   const pathToPlane = structuredClone(pathToPipe) | ||||
|   const pathToProfile = structuredClone(pathToPipe) | ||||
|   pathToProfile[1][0] = index + 1 | ||||
|  | ||||
|   return { | ||||
|     modifiedAst: _ast, | ||||
|     pathToProfile, | ||||
|     pathToPlane, | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -275,7 +275,7 @@ const runModifyAstCloneWithEdgeTreatmentAndTag = async ( | ||||
|   const selection: Selections = { | ||||
|     graphSelections: segmentRanges.map((segmentRange) => { | ||||
|       const maybeArtifact = [...artifactGraph].find(([, a]) => { | ||||
|         if (!('codeRef' in a)) return false | ||||
|         if (!('codeRef' in a && a.codeRef)) return false | ||||
|         return isOverlap(a.codeRef.range, segmentRange) | ||||
|       }) | ||||
|       return { | ||||
| @ -487,8 +487,8 @@ extrude001 = extrude(-15, sketch001) | ||||
|   |> lineTo([profileStartX(%), profileStartY(%)], %) | ||||
|   |> close(%) | ||||
| extrude001 = extrude(-15, sketch001) | ||||
|   |> chamfer({ length: 5, tags: [seg01] }, %) | ||||
|   |> ${edgeTreatmentType}({ ${parameterName}: 3, tags: [seg02] }, %)` | ||||
|   |> chamfer({ length = 5, tags = [seg01] }, %) | ||||
|   |> ${edgeTreatmentType}({ ${parameterName} = 3, tags = [seg02] }, %)` | ||||
|  | ||||
|         await runModifyAstCloneWithEdgeTreatmentAndTag( | ||||
|           code, | ||||
|  | ||||
| @ -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. | ||||
| @ -596,7 +597,13 @@ export function findAllPreviousVariables( | ||||
| type ReplacerFn = ( | ||||
|   _ast: Node<Program>, | ||||
|   varName: string | ||||
| ) => { modifiedAst: Node<Program>; pathToReplaced: PathToNode } | Error | ||||
| ) => | ||||
|   | { | ||||
|       modifiedAst: Node<Program> | ||||
|       pathToReplaced: PathToNode | ||||
|       exprInsertIndex: number | ||||
|     } | ||||
|   | Error | ||||
|  | ||||
| export function isNodeSafeToReplacePath( | ||||
|   ast: Program, | ||||
| @ -648,7 +655,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') | ||||
| @ -767,8 +774,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 | ||||
| @ -1044,13 +1058,15 @@ export function doesSceneHaveSweepableSketch(ast: Node<Program>, count = 1) { | ||||
|             hasCircle = true | ||||
|           } | ||||
|         } | ||||
|         if ( | ||||
|           (hasStartProfileAt || hasCircle) && | ||||
|           hasStartSketchOn && | ||||
|           (hasClose || hasCircle) | ||||
|         ) { | ||||
|         if ((hasStartProfileAt && hasClose) || hasCircle) { | ||||
|           theMap[node.id.name] = true | ||||
|         } | ||||
|       } else if ( | ||||
|         node.type === 'VariableDeclaration' && | ||||
|         node.declaration.init.type === 'CallExpression' && | ||||
|         node.declaration.init.callee.name === 'circle' | ||||
|       ) { | ||||
|         theMap[node.declaration.id.name] = true | ||||
|       } else if ( | ||||
|         node.type === 'CallExpression' && | ||||
|         (node.callee.name === 'extrude' || node.callee.name === 'revolve') && | ||||
| @ -1101,3 +1117,57 @@ export function getObjExprProperty( | ||||
|   if (index === -1) return null | ||||
|   return { expr: node.properties[index].value, index } | ||||
| } | ||||
|  | ||||
| export function isCursorInFunctionDefinition( | ||||
|   ast: Node<Program>, | ||||
|   selectionRanges: Selection | ||||
| ): boolean { | ||||
|   if (!selectionRanges?.codeRef?.pathToNode) return false | ||||
|   const node = getNodeFromPath<FunctionExpression>( | ||||
|     ast, | ||||
|     selectionRanges.codeRef.pathToNode, | ||||
|     'FunctionExpression' | ||||
|   ) | ||||
|   if (err(node)) return false | ||||
|   if (node.node.type === 'FunctionExpression') return true | ||||
|   return false | ||||
| } | ||||
|  | ||||
| export function getBodyIndex(pathToNode: PathToNode): number | Error { | ||||
|   const index = Number(pathToNode[1][0]) | ||||
|   if (Number.isInteger(index)) return index | ||||
|   return new Error('Expected number index') | ||||
| } | ||||
|  | ||||
| export function isCallExprWithName( | ||||
|   expr: Expr | CallExpression, | ||||
|   name: string | ||||
| ): expr is CallExpression { | ||||
|   if (expr.type === 'CallExpression' && expr.callee.type === 'Identifier') { | ||||
|     return expr.callee.name === name | ||||
|   } | ||||
|   return false | ||||
| } | ||||
|  | ||||
| export function doesSketchPipeNeedSplitting( | ||||
|   ast: Node<Program>, | ||||
|   pathToPipe: PathToNode | ||||
| ): boolean | Error { | ||||
|   const varDec = getNodeFromPath<VariableDeclarator>( | ||||
|     ast, | ||||
|     pathToPipe, | ||||
|     'VariableDeclarator' | ||||
|   ) | ||||
|   if (err(varDec)) return varDec | ||||
|   if (varDec.node.type !== 'VariableDeclarator') return new Error('Not a var') | ||||
|   const pipeExpression = varDec.node.init | ||||
|   if (pipeExpression.type !== 'PipeExpression') return false | ||||
|   const [firstPipe, secondPipe] = pipeExpression.body | ||||
|   if (!firstPipe || !secondPipe) return false | ||||
|   if ( | ||||
|     isCallExprWithName(firstPipe, 'startSketchOn') && | ||||
|     isCallExprWithName(secondPipe, 'startProfileAt') | ||||
|   ) | ||||
|     return true | ||||
|   return false | ||||
| } | ||||
|  | ||||
| @ -212,6 +212,19 @@ Map { | ||||
|     "type": "wall", | ||||
|   }, | ||||
|   "UUID-10" => { | ||||
|     "codeRef": { | ||||
|       "pathToNode": [ | ||||
|         [ | ||||
|           "body", | ||||
|           "", | ||||
|         ], | ||||
|       ], | ||||
|       "range": [ | ||||
|         501, | ||||
|         522, | ||||
|         0, | ||||
|       ], | ||||
|     }, | ||||
|     "edgeCutEdgeIds": [], | ||||
|     "id": "UUID", | ||||
|     "pathIds": [ | ||||
|  | ||||
| @ -1,7 +1,8 @@ | ||||
| import { PathToNode, Program, SourceRange } from 'lang/wasm' | ||||
| import { Expr, PathToNode, Program, SourceRange } from 'lang/wasm' | ||||
| import { Models } from '@kittycad/lib' | ||||
| import { getNodePathFromSourceRange } from 'lang/queryAst' | ||||
| import { err } from 'lib/trap' | ||||
| import { engineCommandManager, kclManager } from 'lib/singletons' | ||||
|  | ||||
| export type ArtifactId = string | ||||
|  | ||||
| @ -34,7 +35,7 @@ export interface PathArtifact extends BaseArtifact { | ||||
|   codeRef: CodeRef | ||||
| } | ||||
|  | ||||
| interface solid2D extends BaseArtifact { | ||||
| interface Solid2DArtifact extends BaseArtifact { | ||||
|   type: 'solid2D' | ||||
|   pathId: ArtifactId | ||||
| } | ||||
| @ -61,7 +62,7 @@ interface SegmentArtifactRich extends BaseArtifact { | ||||
|   type: 'segment' | ||||
|   path: PathArtifact | ||||
|   surf: WallArtifact | ||||
|   edges: Array<SweepEdge> | ||||
|   edges: Array<SweepEdgeArtifact> | ||||
|   edgeCut?: EdgeCut | ||||
|   codeRef: CodeRef | ||||
| } | ||||
| @ -80,7 +81,7 @@ interface SweepArtifactRich extends BaseArtifact { | ||||
|   subType: 'extrusion' | 'revolve' | ||||
|   path: PathArtifact | ||||
|   surfaces: Array<WallArtifact | CapArtifact> | ||||
|   edges: Array<SweepEdge> | ||||
|   edges: Array<SweepEdgeArtifact> | ||||
|   codeRef: CodeRef | ||||
| } | ||||
|  | ||||
| @ -90,6 +91,9 @@ interface WallArtifact extends BaseArtifact { | ||||
|   edgeCutEdgeIds: Array<ArtifactId> | ||||
|   sweepId: ArtifactId | ||||
|   pathIds: Array<ArtifactId> | ||||
|   // codeRef is for the sketchOnFace plane, not for the wall itself | ||||
|   // traverse to the extrude and or segment to get the wall's codeRef | ||||
|   codeRef?: CodeRef | ||||
| } | ||||
| interface CapArtifact extends BaseArtifact { | ||||
|   type: 'cap' | ||||
| @ -99,7 +103,7 @@ interface CapArtifact extends BaseArtifact { | ||||
|   pathIds: Array<ArtifactId> | ||||
| } | ||||
|  | ||||
| interface SweepEdge extends BaseArtifact { | ||||
| interface SweepEdgeArtifact extends BaseArtifact { | ||||
|   type: 'sweepEdge' | ||||
|   segId: ArtifactId | ||||
|   sweepId: ArtifactId | ||||
| @ -129,10 +133,10 @@ export type Artifact = | ||||
|   | SweepArtifact | ||||
|   | WallArtifact | ||||
|   | CapArtifact | ||||
|   | SweepEdge | ||||
|   | SweepEdgeArtifact | ||||
|   | EdgeCut | ||||
|   | EdgeCutEdge | ||||
|   | solid2D | ||||
|   | Solid2DArtifact | ||||
|  | ||||
| export type ArtifactGraph = Map<ArtifactId, Artifact> | ||||
|  | ||||
| @ -284,6 +288,7 @@ export function getArtifactsToUpdate({ | ||||
|             edgeCutEdgeIds: existingPlane.edgeCutEdgeIds, | ||||
|             sweepId: existingPlane.sweepId, | ||||
|             pathIds: existingPlane.pathIds, | ||||
|             codeRef, | ||||
|           }, | ||||
|         }, | ||||
|       ] | ||||
| @ -733,7 +738,7 @@ export function getCapCodeRef( | ||||
| } | ||||
|  | ||||
| export function getSolid2dCodeRef( | ||||
|   solid2D: solid2D, | ||||
|   solid2D: Solid2DArtifact, | ||||
|   artifactGraph: ArtifactGraph | ||||
| ): CodeRef | Error { | ||||
|   const path = getArtifactOfTypes( | ||||
| @ -757,7 +762,7 @@ export function getWallCodeRef( | ||||
| } | ||||
|  | ||||
| export function getSweepEdgeCodeRef( | ||||
|   edge: SweepEdge, | ||||
|   edge: SweepEdgeArtifact, | ||||
|   artifactGraph: ArtifactGraph | ||||
| ): CodeRef | Error { | ||||
|   const seg = getArtifactOfTypes( | ||||
| @ -871,3 +876,202 @@ export function codeRefFromRange(range: SourceRange, ast: Program): CodeRef { | ||||
|     pathToNode: getNodePathFromSourceRange(ast, range), | ||||
|   } | ||||
| } | ||||
|  | ||||
| function getPlaneFromPath( | ||||
|   path: PathArtifact, | ||||
|   graph: ArtifactGraph | ||||
| ): PlaneArtifact | WallArtifact | Error { | ||||
|   const plane = getArtifactOfTypes( | ||||
|     { key: path.planeId, types: ['plane', 'wall'] }, | ||||
|     graph | ||||
|   ) | ||||
|   if (err(plane)) return plane | ||||
|   return plane | ||||
| } | ||||
|  | ||||
| function getPlaneFromSegment( | ||||
|   segment: SegmentArtifact, | ||||
|   graph: ArtifactGraph | ||||
| ): PlaneArtifact | WallArtifact | 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 | 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 | 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 | 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 | 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 | ||||
| } | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| import { defaultSourceRange, SourceRange } from 'lang/wasm' | ||||
| import { defaultSourceRange, SourceRange, Program } from 'lang/wasm' | ||||
| import { VITE_KC_API_WS_MODELING_URL, VITE_KC_DEV_TOKEN } from 'env' | ||||
| import { Models } from '@kittycad/lib' | ||||
| import { exportSave } from 'lib/exportSave' | ||||
| @ -2099,30 +2099,23 @@ export class EngineCommandManager extends EventTarget { | ||||
|    * When an execution takes place we want to wait until we've got replies for all of the commands | ||||
|    * When this is done when we build the artifact map synchronously. | ||||
|    */ | ||||
|   async waitForAllCommands(useFakeExecutor = false) { | ||||
|     await Promise.all(Object.values(this.pendingCommands).map((a) => a.promise)) | ||||
|     setTimeout(() => { | ||||
|       // the ast is wrong without this one tick timeout. | ||||
|       // an example is `Solids should be select and deletable` e2e test will fail | ||||
|       // because the out of date ast messes with selections | ||||
|       // TODO: race condition | ||||
|       if (!this?.kclManager) return | ||||
|   waitForAllCommands() { | ||||
|     return Promise.all( | ||||
|       Object.values(this.pendingCommands).map((a) => a.promise) | ||||
|     ) | ||||
|   } | ||||
|   updateArtifactGraph(ast: Program) { | ||||
|     this.artifactGraph = createArtifactGraph({ | ||||
|       orderedCommands: this.orderedCommands, | ||||
|       responseMap: this.responseMap, | ||||
|         ast: this.kclManager.ast, | ||||
|       ast, | ||||
|     }) | ||||
|       if (useFakeExecutor) { | ||||
|         // mock executions don't produce an artifactGraph, so this will always be empty | ||||
|         // skipping the below logic to wait for the next real execution | ||||
|         return | ||||
|       } | ||||
|     // TODO check if these still need to be deferred once e2e tests are working again. | ||||
|     if (this.artifactGraph.size) { | ||||
|       this.deferredArtifactEmptied(null) | ||||
|     } else { | ||||
|       this.deferredArtifactPopulated(null) | ||||
|     } | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   /** | ||||
| @ -2209,7 +2202,11 @@ export class EngineCommandManager extends EventTarget { | ||||
|     commandTypeToTarget: string | ||||
|   ): string | undefined { | ||||
|     for (const [artifactId, artifact] of this.artifactGraph) { | ||||
|       if ('codeRef' in artifact && isOverlap(range, artifact.codeRef.range)) { | ||||
|       if ( | ||||
|         'codeRef' in artifact && | ||||
|         artifact.codeRef && | ||||
|         isOverlap(range, artifact.codeRef.range) | ||||
|       ) { | ||||
|         if (commandTypeToTarget === artifact.type) return artifactId | ||||
|       } | ||||
|     } | ||||
|  | ||||
| @ -297,14 +297,20 @@ export const lineTo: SketchLineHelper = { | ||||
|   add: ({ node, pathToNode, segmentInput, replaceExistingCallback }) => { | ||||
|     if (segmentInput.type !== 'straight-segment') return STRAIGHT_SEGMENT_ERR | ||||
|     const to = segmentInput.to | ||||
|     const _node = { ...node } | ||||
|     const _node = structuredClone(node) | ||||
|     const nodeMeta = getNodeFromPath<PipeExpression>( | ||||
|       _node, | ||||
|       pathToNode, | ||||
|       'PipeExpression' | ||||
|     ) | ||||
|     if (err(nodeMeta)) return nodeMeta | ||||
|     const { node: pipe } = nodeMeta | ||||
|     const varDec = getNodeFromPath<VariableDeclaration>( | ||||
|       _node, | ||||
|       pathToNode, | ||||
|       'VariableDeclaration' | ||||
|     ) | ||||
|     if (err(varDec)) return varDec | ||||
|     const dec = varDec.node.declaration | ||||
|  | ||||
|     const newVals: [Expr, Expr] = [ | ||||
|       createLiteral(roundOff(to[0], 2)), | ||||
| @ -333,14 +339,20 @@ export const lineTo: SketchLineHelper = { | ||||
|       ]) | ||||
|       if (err(result)) return result | ||||
|       const { callExp, valueUsedInTransform } = result | ||||
|       pipe.body[callIndex] = callExp | ||||
|       if (dec.init.type === 'PipeExpression') { | ||||
|         dec.init.body[callIndex] = callExp | ||||
|       } else { | ||||
|         dec.init = callExp | ||||
|       } | ||||
|       return { | ||||
|         modifiedAst: _node, | ||||
|         pathToNode, | ||||
|         valueUsedInTransform: valueUsedInTransform, | ||||
|       } | ||||
|     } else if (dec.init.type === 'PipeExpression') { | ||||
|       dec.init.body = [...dec.init.body, newLine] | ||||
|     } else { | ||||
|       pipe.body = [...pipe.body, newLine] | ||||
|       dec.init = createPipeExpression([dec.init, newLine]) | ||||
|     } | ||||
|     return { | ||||
|       modifiedAst: _node, | ||||
| @ -663,11 +675,11 @@ export const xLine: SketchLineHelper = { | ||||
|   add: ({ node, pathToNode, segmentInput, replaceExistingCallback }) => { | ||||
|     if (segmentInput.type !== 'straight-segment') return STRAIGHT_SEGMENT_ERR | ||||
|     const { from, to } = segmentInput | ||||
|     const _node = { ...node } | ||||
|     const _node = structuredClone(node) | ||||
|     const getNode = getNodeFromPathCurry(_node, pathToNode) | ||||
|     const _node1 = getNode<PipeExpression>('PipeExpression') | ||||
|     if (err(_node1)) return _node1 | ||||
|     const { node: pipe } = _node1 | ||||
|     const varDec = getNode<VariableDeclaration>('VariableDeclaration') | ||||
|     if (err(varDec)) return varDec | ||||
|     const dec = varDec.node.declaration | ||||
|  | ||||
|     const newVal = createLiteral(roundOff(to[0] - from[0], 2)) | ||||
|  | ||||
| @ -682,7 +694,11 @@ export const xLine: SketchLineHelper = { | ||||
|       ]) | ||||
|       if (err(result)) return result | ||||
|       const { callExp, valueUsedInTransform } = result | ||||
|       pipe.body[callIndex] = callExp | ||||
|       if (dec.init.type === 'PipeExpression') { | ||||
|         dec.init.body[callIndex] = callExp | ||||
|       } else { | ||||
|         dec.init = callExp | ||||
|       } | ||||
|       return { | ||||
|         modifiedAst: _node, | ||||
|         pathToNode, | ||||
| @ -694,7 +710,11 @@ export const xLine: SketchLineHelper = { | ||||
|       newVal, | ||||
|       createPipeSubstitution(), | ||||
|     ]) | ||||
|     pipe.body = [...pipe.body, newLine] | ||||
|     if (dec.init.type === 'PipeExpression') { | ||||
|       dec.init.body = [...dec.init.body, newLine] | ||||
|     } else { | ||||
|       dec.init = createPipeExpression([dec.init, newLine]) | ||||
|     } | ||||
|     return { modifiedAst: _node, pathToNode } | ||||
|   }, | ||||
|   updateArgs: ({ node, pathToNode, input }) => { | ||||
| @ -731,11 +751,11 @@ export const yLine: SketchLineHelper = { | ||||
|   add: ({ node, pathToNode, segmentInput, replaceExistingCallback }) => { | ||||
|     if (segmentInput.type !== 'straight-segment') return STRAIGHT_SEGMENT_ERR | ||||
|     const { from, to } = segmentInput | ||||
|     const _node = { ...node } | ||||
|     const _node = structuredClone(node) | ||||
|     const getNode = getNodeFromPathCurry(_node, pathToNode) | ||||
|     const _node1 = getNode<PipeExpression>('PipeExpression') | ||||
|     if (err(_node1)) return _node1 | ||||
|     const { node: pipe } = _node1 | ||||
|     const varDec = getNode<VariableDeclaration>('VariableDeclaration') | ||||
|     if (err(varDec)) return varDec | ||||
|     const dec = varDec.node.declaration | ||||
|     const newVal = createLiteral(roundOff(to[1] - from[1], 2)) | ||||
|     if (replaceExistingCallback) { | ||||
|       const { index: callIndex } = splitPathAtPipeExpression(pathToNode) | ||||
| @ -748,7 +768,11 @@ export const yLine: SketchLineHelper = { | ||||
|       ]) | ||||
|       if (err(result)) return result | ||||
|       const { callExp, valueUsedInTransform } = result | ||||
|       pipe.body[callIndex] = callExp | ||||
|       if (dec.init.type === 'PipeExpression') { | ||||
|         dec.init.body[callIndex] = callExp | ||||
|       } else { | ||||
|         dec.init = callExp | ||||
|       } | ||||
|       return { | ||||
|         modifiedAst: _node, | ||||
|         pathToNode, | ||||
| @ -760,7 +784,11 @@ export const yLine: SketchLineHelper = { | ||||
|       newVal, | ||||
|       createPipeSubstitution(), | ||||
|     ]) | ||||
|     pipe.body = [...pipe.body, newLine] | ||||
|     if (dec.init.type === 'PipeExpression') { | ||||
|       dec.init.body = [...dec.init.body, newLine] | ||||
|     } else { | ||||
|       dec.init = createPipeExpression([dec.init, newLine]) | ||||
|     } | ||||
|     return { modifiedAst: _node, pathToNode } | ||||
|   }, | ||||
|   updateArgs: ({ node, pathToNode, input }) => { | ||||
| @ -2145,8 +2173,6 @@ function addTagToChamfer( | ||||
|   if (err(variableDec)) return variableDec | ||||
|   const isPipeExpression = pipeExpr.node.type === 'PipeExpression' | ||||
|  | ||||
|   console.log('pipeExpr', pipeExpr, variableDec) | ||||
|   // const callExpr = isPipeExpression ? pipeExpr.node.body[pipeIndex] : variableDec.node.init | ||||
|   const callExpr = isPipeExpression | ||||
|     ? pipeExpr.node.body[pipeIndex] | ||||
|     : variableDec.node.init | ||||
| @ -2227,7 +2253,6 @@ function addTagToChamfer( | ||||
|   if (isPipeExpression) { | ||||
|     pipeExpr.node.body.splice(pipeIndex, 0, newExpressionToInsert) | ||||
|   } else { | ||||
|     console.log('yo', createPipeExpression([newExpressionToInsert, callExpr])) | ||||
|     callExpr.arguments[1] = createPipeSubstitution() | ||||
|     variableDec.node.init = createPipeExpression([ | ||||
|       newExpressionToInsert, | ||||
|  | ||||
| @ -9,20 +9,47 @@ import { | ||||
| import { ArtifactGraph, filterArtifacts } from 'lang/std/artifactGraph' | ||||
| import { isOverlap } from 'lib/utils' | ||||
|  | ||||
| export function updatePathToNodeFromMap( | ||||
|   oldPath: PathToNode, | ||||
|   pathToNodeMap: { [key: number]: PathToNode } | ||||
| /** | ||||
|  * Updates pathToNode body indices to account for the insertion of an expression | ||||
|  * PathToNode expression is after the insertion index, that the body index is incremented | ||||
|  * Negative insertion index means no insertion | ||||
|  */ | ||||
| export function updatePathToNodePostExprInjection( | ||||
|   pathToNode: PathToNode, | ||||
|   exprInsertIndex: number | ||||
| ): PathToNode { | ||||
|   const updatedPathToNode = structuredClone(oldPath) | ||||
|   let max = 0 | ||||
|   Object.values(pathToNodeMap).forEach((path) => { | ||||
|     const index = Number(path[1][0]) | ||||
|     if (index > max) { | ||||
|       max = index | ||||
|   if (exprInsertIndex < 0) return pathToNode | ||||
|   const bodyIndex = Number(pathToNode[1][0]) | ||||
|   if (bodyIndex < exprInsertIndex) return pathToNode | ||||
|   const clone = structuredClone(pathToNode) | ||||
|   clone[1][0] = bodyIndex + 1 | ||||
|   return clone | ||||
| } | ||||
|  | ||||
| export function updateSketchDetailsNodePaths({ | ||||
|   sketchEntryNodePath, | ||||
|   sketchNodePaths, | ||||
|   planeNodePath, | ||||
|   exprInsertIndex, | ||||
| }: { | ||||
|   sketchEntryNodePath: PathToNode | ||||
|   sketchNodePaths: Array<PathToNode> | ||||
|   planeNodePath: PathToNode | ||||
|   exprInsertIndex: number | ||||
| }) { | ||||
|   return { | ||||
|     updatedSketchEntryNodePath: updatePathToNodePostExprInjection( | ||||
|       sketchEntryNodePath, | ||||
|       exprInsertIndex | ||||
|     ), | ||||
|     updatedSketchNodePaths: sketchNodePaths.map((path) => | ||||
|       updatePathToNodePostExprInjection(path, exprInsertIndex) | ||||
|     ), | ||||
|     updatedPlaneNodePath: updatePathToNodePostExprInjection( | ||||
|       planeNodePath, | ||||
|       exprInsertIndex | ||||
|     ), | ||||
|   } | ||||
|   }) | ||||
|   updatedPathToNode[1][0] = max | ||||
|   return updatedPathToNode | ||||
| } | ||||
|  | ||||
| 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) => | ||||
|  | ||||
| @ -491,26 +491,6 @@ export const executor = async ( | ||||
|   if (programMemoryOverride !== null && err(programMemoryOverride)) | ||||
|     return Promise.reject(programMemoryOverride) | ||||
|  | ||||
|   // eslint-disable-next-line @typescript-eslint/no-floating-promises | ||||
|   engineCommandManager.startNewSession() | ||||
|   const _programMemory = await _executor( | ||||
|     node, | ||||
|     engineCommandManager, | ||||
|     programMemoryOverride | ||||
|   ) | ||||
|   await engineCommandManager.waitForAllCommands() | ||||
|  | ||||
|   return _programMemory | ||||
| } | ||||
|  | ||||
| export const _executor = async ( | ||||
|   node: Node<Program>, | ||||
|   engineCommandManager: EngineCommandManager, | ||||
|   programMemoryOverride: ProgramMemory | Error | null = null | ||||
| ): Promise<ExecState> => { | ||||
|   if (programMemoryOverride !== null && err(programMemoryOverride)) | ||||
|     return Promise.reject(programMemoryOverride) | ||||
|  | ||||
|   try { | ||||
|     let jsAppSettings = default_app_settings() | ||||
|     if (!TEST) { | ||||
|  | ||||
| @ -29,7 +29,7 @@ import { | ||||
|  */ | ||||
| export const getRectangleCallExpressions = ( | ||||
|   rectangleOrigin: [number, number], | ||||
|   tags: [string, string, string] | ||||
|   tag: string | ||||
| ) => [ | ||||
|   createCallExpressionStdLib('angledLine', [ | ||||
|     createArrayExpression([ | ||||
| @ -37,30 +37,28 @@ export const getRectangleCallExpressions = ( | ||||
|       createLiteral(0), // This will be the width of the rectangle | ||||
|     ]), | ||||
|     createPipeSubstitution(), | ||||
|     createTagDeclarator(tags[0]), | ||||
|     createTagDeclarator(tag), | ||||
|   ]), | ||||
|   createCallExpressionStdLib('angledLine', [ | ||||
|     createArrayExpression([ | ||||
|       createBinaryExpression([ | ||||
|         createCallExpressionStdLib('segAng', [createIdentifier(tags[0])]), | ||||
|         createCallExpressionStdLib('segAng', [createIdentifier(tag)]), | ||||
|         '+', | ||||
|         createLiteral(90), | ||||
|       ]), // 90 offset from the previous line | ||||
|       createLiteral(0), // This will be the height of the rectangle | ||||
|     ]), | ||||
|     createPipeSubstitution(), | ||||
|     createTagDeclarator(tags[1]), | ||||
|   ]), | ||||
|   createCallExpressionStdLib('angledLine', [ | ||||
|     createArrayExpression([ | ||||
|       createCallExpressionStdLib('segAng', [createIdentifier(tags[0])]), // same angle as the first line | ||||
|       createCallExpressionStdLib('segAng', [createIdentifier(tag)]), // same angle as the first line | ||||
|       createUnaryExpression( | ||||
|         createCallExpressionStdLib('segLen', [createIdentifier(tags[0])]), | ||||
|         createCallExpressionStdLib('segLen', [createIdentifier(tag)]), | ||||
|         '-' | ||||
|       ), // negative height | ||||
|     ]), | ||||
|     createPipeSubstitution(), | ||||
|     createTagDeclarator(tags[2]), | ||||
|   ]), | ||||
|   createCallExpressionStdLib('lineTo', [ | ||||
|     createArrayExpression([ | ||||
| @ -85,12 +83,12 @@ export function updateRectangleSketch( | ||||
|   y: number, | ||||
|   tag: string | ||||
| ) { | ||||
|   ;((pipeExpression.body[2] as CallExpression) | ||||
|   ;((pipeExpression.body[1] as CallExpression) | ||||
|     .arguments[0] as ArrayExpression) = createArrayExpression([ | ||||
|     createLiteral(x >= 0 ? 0 : 180), | ||||
|     createLiteral(Math.abs(x)), | ||||
|   ]) | ||||
|   ;((pipeExpression.body[3] as CallExpression) | ||||
|   ;((pipeExpression.body[2] as CallExpression) | ||||
|     .arguments[0] as ArrayExpression) = createArrayExpression([ | ||||
|     createBinaryExpression([ | ||||
|       createCallExpressionStdLib('segAng', [createIdentifier(tag)]), | ||||
| @ -120,7 +118,7 @@ export function updateCenterRectangleSketch( | ||||
|   let startY = originY - Math.abs(deltaY) | ||||
|  | ||||
|   // pipeExpression.body[1] is startProfileAt | ||||
|   let callExpression = pipeExpression.body[1] | ||||
|   let callExpression = pipeExpression.body[0] | ||||
|   if (isCallExpression(callExpression)) { | ||||
|     const arrayExpression = callExpression.arguments[0] | ||||
|     if (isArrayExpression(arrayExpression)) { | ||||
| @ -134,7 +132,7 @@ export function updateCenterRectangleSketch( | ||||
|   const twoX = deltaX * 2 | ||||
|   const twoY = deltaY * 2 | ||||
|  | ||||
|   callExpression = pipeExpression.body[2] | ||||
|   callExpression = pipeExpression.body[1] | ||||
|   if (isCallExpression(callExpression)) { | ||||
|     const arrayExpression = callExpression.arguments[0] | ||||
|     if (isArrayExpression(arrayExpression)) { | ||||
| @ -148,7 +146,7 @@ export function updateCenterRectangleSketch( | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   callExpression = pipeExpression.body[3] | ||||
|   callExpression = pipeExpression.body[2] | ||||
|   if (isCallExpression(callExpression)) { | ||||
|     const arrayExpression = callExpression.arguments[0] | ||||
|     if (isArrayExpression(arrayExpression)) { | ||||
|  | ||||
| @ -280,18 +280,19 @@ export function getEventForSegmentSelection( | ||||
|   } | ||||
|   if (!id || !group) return null | ||||
|   const artifact = engineCommandManager.artifactGraph.get(id) | ||||
|   const codeRefs = getCodeRefsByArtifactId( | ||||
|     id, | ||||
|     engineCommandManager.artifactGraph | ||||
|   ) | ||||
|   if (!artifact || !codeRefs) return null | ||||
|   if (!artifact) return null | ||||
|   const node = getNodeFromPath<Expr>(kclManager.ast, group.userData.pathToNode) | ||||
|   if (err(node)) return null | ||||
|   return { | ||||
|     type: 'Set selection', | ||||
|     data: { | ||||
|       selectionType: 'singleCodeCursor', | ||||
|       selection: { | ||||
|         artifact, | ||||
|         codeRef: codeRefs[0], | ||||
|         codeRef: { | ||||
|           pathToNode: group?.userData?.pathToNode, | ||||
|           range: [node.node.start, node.node.end, true], | ||||
|         }, | ||||
|       }, | ||||
|     }, | ||||
|   } | ||||
| @ -550,10 +551,18 @@ function nodeHasClose(node: CommonASTNode) { | ||||
|   }) | ||||
| } | ||||
| function nodeHasCircle(node: CommonASTNode) { | ||||
|   return doesPipeHaveCallExp({ | ||||
|   return ( | ||||
|     doesPipeHaveCallExp({ | ||||
|       calleeName: 'circle', | ||||
|       ...node, | ||||
|   }) | ||||
|     }) || | ||||
|     node.ast.body.some( | ||||
|       (expression) => | ||||
|         expression.type === 'VariableDeclaration' && | ||||
|         expression.declaration.init.type === 'CallExpression' && | ||||
|         expression.declaration.init.callee.name === 'circle' | ||||
|     ) | ||||
|   ) | ||||
| } | ||||
|  | ||||
| export function canSweepSelection(selection: Selections) { | ||||
| @ -561,8 +570,6 @@ export function canSweepSelection(selection: Selections) { | ||||
|     buildCommonNodeFromSelection(selection, i) | ||||
|   ) | ||||
|   return ( | ||||
|     !!isSketchPipe(selection) && | ||||
|     commonNodes.every((n) => !hasSketchPipeBeenExtruded(n.selection, n.ast)) && | ||||
|     (commonNodes.every((n) => nodeHasClose(n)) || | ||||
|       commonNodes.every((n) => nodeHasCircle(n))) && | ||||
|     commonNodes.every((n) => !nodeHasExtrude(n)) | ||||
| @ -675,8 +682,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 +691,6 @@ export function getSelectionTypeDisplayText( | ||||
|           .replace('solid2D', 'face') | ||||
|           .replace('segment', 'face')}${count > 1 ? 's' : ''}` | ||||
|     ) | ||||
|     .toArray() | ||||
|     .join(', ') | ||||
| } | ||||
|  | ||||
| @ -695,7 +700,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 +723,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 +966,6 @@ export function updateSelections( | ||||
|             JSON.stringify(pathToNode) | ||||
|           ) { | ||||
|             artifact = a | ||||
|             console.log('found artifact', a) | ||||
|             break | ||||
|           } | ||||
|         } | ||||
|  | ||||
| @ -1,20 +1,16 @@ | ||||
| import { | ||||
|   Program, | ||||
|   ProgramMemory, | ||||
|   _executor, | ||||
|   executor, | ||||
|   SourceRange, | ||||
|   ExecState, | ||||
| } from '../lang/wasm' | ||||
| import { | ||||
|   EngineCommandManager, | ||||
|   EngineCommandManagerEvents, | ||||
| } from 'lang/std/engineConnection' | ||||
| import { EngineCommandManager } from 'lang/std/engineConnection' | ||||
| import { EngineCommand } from 'lang/std/artifactGraph' | ||||
| import { Models } from '@kittycad/lib' | ||||
| import { v4 as uuidv4 } from 'uuid' | ||||
| import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes' | ||||
| import { err, reportRejection } from 'lib/trap' | ||||
| import { toSync } from './utils' | ||||
| import { err } from 'lib/trap' | ||||
| import { Node } from 'wasm-lib/kcl/bindings/Node' | ||||
|  | ||||
| type WebSocketResponse = Models['WebSocketResponse_type'] | ||||
| @ -94,36 +90,7 @@ export async function enginelessExecutor( | ||||
|   }) as any as EngineCommandManager | ||||
|   // eslint-disable-next-line @typescript-eslint/no-floating-promises | ||||
|   mockEngineCommandManager.startNewSession() | ||||
|   const execState = await _executor(ast, mockEngineCommandManager, pmo) | ||||
|   const execState = await executor(ast, mockEngineCommandManager, pmo) | ||||
|   await mockEngineCommandManager.waitForAllCommands() | ||||
|   return execState | ||||
| } | ||||
|  | ||||
| export async function executor( | ||||
|   ast: Node<Program>, | ||||
|   pmo: ProgramMemory = ProgramMemory.empty() | ||||
| ): Promise<ExecState> { | ||||
|   const engineCommandManager = new EngineCommandManager() | ||||
|   engineCommandManager.start({ | ||||
|     setIsStreamReady: () => {}, | ||||
|     setMediaStream: () => {}, | ||||
|     width: 0, | ||||
|     height: 0, | ||||
|     makeDefaultPlanes: () => { | ||||
|       return new Promise((resolve) => resolve(defaultPlanes)) | ||||
|     }, | ||||
|   }) | ||||
|  | ||||
|   return new Promise((resolve) => { | ||||
|     engineCommandManager.addEventListener( | ||||
|       EngineCommandManagerEvents.SceneReady, | ||||
|       toSync(async () => { | ||||
|         // eslint-disable-next-line @typescript-eslint/no-floating-promises | ||||
|         engineCommandManager.startNewSession() | ||||
|         const execState = await _executor(ast, engineCommandManager, pmo) | ||||
|         await engineCommandManager.waitForAllCommands() | ||||
|         resolve(execState) | ||||
|       }, reportRejection) | ||||
|     ) | ||||
|   }) | ||||
| } | ||||
|  | ||||
| @ -2,8 +2,6 @@ import { CustomIconName } from 'components/CustomIcon' | ||||
| import { DEV } from 'env' | ||||
| import { commandBarMachine } from 'machines/commandBarMachine' | ||||
| import { | ||||
|   canRectangleOrCircleTool, | ||||
|   isClosedSketch, | ||||
|   isEditingExistingSketch, | ||||
|   modelingMachine, | ||||
|   pipeHasCircle, | ||||
| @ -73,7 +71,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = { | ||||
|         status: 'available', | ||||
|         disabled: (state) => !state.matches('idle'), | ||||
|         title: ({ sketchPathId }) => | ||||
|           `${sketchPathId ? 'Edit' : 'Start'} Sketch`, | ||||
|           sketchPathId ? 'Edit Sketch' : 'Start Sketch', | ||||
|         showTitle: true, | ||||
|         hotkey: 'S', | ||||
|         description: 'Start drawing a 2D sketch', | ||||
| @ -315,13 +313,6 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = { | ||||
|       { | ||||
|         id: 'line', | ||||
|         onClick: ({ modelingState, modelingSend }) => { | ||||
|           if (modelingState.matches({ Sketch: { 'Line tool': 'No Points' } })) { | ||||
|             // Exit the sketch state if there are no points and they press ESC | ||||
|             modelingSend({ | ||||
|               type: 'Cancel', | ||||
|             }) | ||||
|           } else { | ||||
|             // Exit the tool if there are points and they press ESC | ||||
|           modelingSend({ | ||||
|             type: 'change tool', | ||||
|             data: { | ||||
| @ -330,7 +321,6 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = { | ||||
|                 : 'none', | ||||
|             }, | ||||
|           }) | ||||
|           } | ||||
|         }, | ||||
|         icon: 'line', | ||||
|         status: 'available', | ||||
| @ -341,8 +331,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = { | ||||
|           }) || | ||||
|           state.matches({ | ||||
|             Sketch: { 'Circle tool': 'Awaiting Radius' }, | ||||
|           }) || | ||||
|           isClosedSketch(state.context), | ||||
|           }), | ||||
|         title: 'Line', | ||||
|         hotkey: (state) => | ||||
|           state.matches({ Sketch: 'Line tool' }) ? ['Esc', 'L'] : 'L', | ||||
| @ -422,10 +411,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = { | ||||
|           icon: 'circle', | ||||
|           status: 'available', | ||||
|           title: 'Center circle', | ||||
|           disabled: (state) => | ||||
|             state.matches('Sketch no face') || | ||||
|             (!canRectangleOrCircleTool(state.context) && | ||||
|               !state.matches({ Sketch: 'Circle tool' })), | ||||
|           disabled: (state) => state.matches('Sketch no face'), | ||||
|           isActive: (state) => state.matches({ Sketch: 'Circle tool' }), | ||||
|           hotkey: (state) => | ||||
|             state.matches({ Sketch: 'Circle tool' }) ? ['Esc', 'C'] : 'C', | ||||
| @ -464,10 +450,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = { | ||||
|             }), | ||||
|           icon: 'rectangle', | ||||
|           status: 'available', | ||||
|           disabled: (state) => | ||||
|             state.matches('Sketch no face') || | ||||
|             (!canRectangleOrCircleTool(state.context) && | ||||
|               !state.matches({ Sketch: 'Rectangle tool' })), | ||||
|           disabled: (state) => state.matches('Sketch no face'), | ||||
|           title: 'Corner rectangle', | ||||
|           hotkey: (state) => | ||||
|             state.matches({ Sketch: 'Rectangle tool' }) ? ['Esc', 'R'] : 'R', | ||||
| @ -490,10 +473,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = { | ||||
|             }), | ||||
|           icon: 'arc', | ||||
|           status: 'available', | ||||
|           disabled: (state) => | ||||
|             state.matches('Sketch no face') || | ||||
|             (!canRectangleOrCircleTool(state.context) && | ||||
|               !state.matches({ Sketch: 'Center Rectangle tool' })), | ||||
|           disabled: (state) => state.matches('Sketch no face'), | ||||
|           title: 'Center rectangle', | ||||
|           hotkey: (state) => | ||||
|             state.matches({ Sketch: 'Center Rectangle tool' }) | ||||
|  | ||||
| @ -97,3 +97,7 @@ export function trap<T>( | ||||
|     }) | ||||
|   return true | ||||
| } | ||||
|  | ||||
| export function reject(errOrString: Error | string): Promise<never> { | ||||
|   return Promise.reject(errOrString) | ||||
| } | ||||
|  | ||||
