Compare commits
	
		
			52 Commits
		
	
	
		
			nightly-v2
			...
			kurt-delet
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| d70ebca165 | |||
| d8a9abba69 | |||
| 0fd18c14ef | |||
| 36d4830c34 | |||
| 4ce6054e64 | |||
| ced49f8ddc | |||
| e063622139 | |||
| 42178fa649 | |||
| 4bb23bc917 | |||
| 72272d5d98 | |||
| 5ef0a1e75f | |||
| d8dc49b08a | |||
| 87eabef450 | |||
| 40e4f2236f | |||
| 663076f790 | |||
| f2c76b0509 | |||
| 481bef859a | |||
| 1a67d344ee | |||
| 774e3efcb7 | |||
| 4ec44690bf | |||
| d2f0865f95 | |||
| 84d17454e9 | |||
| 5a5138a703 | |||
| 33468c4c96 | |||
| b3467bbe5a | |||
| 90086488b5 | |||
| 32e8975799 | |||
| 648616c667 | |||
| 482487cf57 | |||
| 5fe3023be9 | |||
| 30397ba7ab | |||
| 3344208c63 | |||
| fcf3272ad2 | |||
| d3e4b123d0 | |||
| 2bb548c000 | |||
| b09c240e36 | |||
| 6c9d14af93 | |||
| 0642e49189 | |||
| 6add1d73ad | |||
| 68c89746c7 | |||
| 9f323c207c | |||
| 7197b6c85d | |||
| 913f2641c3 | |||
| e9086c54ba | |||
| 9f93346dc6 | |||
| 1b9f5f20f5 | |||
| 3865637c61 | |||
| 2c40e8a97c | |||
| c696f0837a | |||
| 30edf2ad56 | |||
| e60cabb193 | |||
| 1e9cf6f256 | 
| @ -54,23 +54,26 @@ async function doBasicSketch( | |||||||
|   const startXPx = 600 |   const startXPx = 600 | ||||||
|   await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) |   await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) | ||||||
|   if (openPanes.includes('code')) { |   if (openPanes.includes('code')) { | ||||||
|     await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ') |     await expect(u.codeLocator).toContainText( | ||||||
|   |> startProfileAt(${commonPoints.startAt}, %)`) |       `sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${commonPoints.startAt}, sketch001)` | ||||||
|  |     ) | ||||||
|   } |   } | ||||||
|   await page.waitForTimeout(500) |   await page.waitForTimeout(500) | ||||||
|   await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10) |   await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10) | ||||||
|   await page.waitForTimeout(500) |   await page.waitForTimeout(500) | ||||||
|  |  | ||||||
|   if (openPanes.includes('code')) { |   if (openPanes.includes('code')) { | ||||||
|     await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ') |     await expect(u.codeLocator) | ||||||
|   |> startProfileAt(${commonPoints.startAt}, %) |       .toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${commonPoints.startAt}, sketch001) | ||||||
|   |> xLine(${commonPoints.num1}, %)`) |   |> xLine(${commonPoints.num1}, %)`) | ||||||
|   } |   } | ||||||
|   await page.waitForTimeout(500) |   await page.waitForTimeout(500) | ||||||
|   await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20) |   await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20) | ||||||
|   if (openPanes.includes('code')) { |   if (openPanes.includes('code')) { | ||||||
|     await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ') |     await expect(u.codeLocator) | ||||||
|   |> startProfileAt(${commonPoints.startAt}, %) |       .toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${ | ||||||
|  |       commonPoints.startAt | ||||||
|  |     }, sketch001) | ||||||
|   |> xLine(${commonPoints.num1}, %) |   |> xLine(${commonPoints.num1}, %) | ||||||
|   |> yLine(${commonPoints.num1 + 0.01}, %)`) |   |> yLine(${commonPoints.num1 + 0.01}, %)`) | ||||||
|   } else { |   } else { | ||||||
| @ -79,8 +82,10 @@ async function doBasicSketch( | |||||||
|   await page.waitForTimeout(200) |   await page.waitForTimeout(200) | ||||||
|   await page.mouse.click(startXPx, 500 - PUR * 20) |   await page.mouse.click(startXPx, 500 - PUR * 20) | ||||||
|   if (openPanes.includes('code')) { |   if (openPanes.includes('code')) { | ||||||
|     await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ') |     await expect(u.codeLocator) | ||||||
|   |> startProfileAt(${commonPoints.startAt}, %) |       .toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${ | ||||||
|  |       commonPoints.startAt | ||||||
|  |     }, sketch001) | ||||||
|   |> xLine(${commonPoints.num1}, %) |   |> xLine(${commonPoints.num1}, %) | ||||||
|   |> yLine(${commonPoints.num1 + 0.01}, %) |   |> yLine(${commonPoints.num1 + 0.01}, %) | ||||||
|   |> xLine(${commonPoints.num2 * -1}, %)`) |   |> xLine(${commonPoints.num2 * -1}, %)`) | ||||||
| @ -137,8 +142,10 @@ async function doBasicSketch( | |||||||
|  |  | ||||||
|   // Open the code pane. |   // Open the code pane. | ||||||
|   await u.openKclCodePanel() |   await u.openKclCodePanel() | ||||||
|   await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ') |   await expect(u.codeLocator) | ||||||
|   |> startProfileAt(${commonPoints.startAt}, %) |     .toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${ | ||||||
|  |     commonPoints.startAt | ||||||
|  |   }, sketch001) | ||||||
|   |> xLine(${commonPoints.num1}, %, $seg01) |   |> xLine(${commonPoints.num1}, %, $seg01) | ||||||
|   |> yLine(${commonPoints.num1 + 0.01}, %) |   |> yLine(${commonPoints.num1 + 0.01}, %) | ||||||
|   |> xLine(-segLen(seg01), %)`) |   |> xLine(-segLen(seg01), %)`) | ||||||
|  | |||||||
| @ -41,8 +41,7 @@ test.describe( | |||||||
|         }, |         }, | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       const code = `sketch001 = startSketchOn('${plane}') |       const code = `sketch001 = startSketchOn('${plane}')profile001 = startProfileAt([0.9, -1.22], sketch001)` | ||||||
|     |> startProfileAt([0.9, -1.22], %)` |  | ||||||
|  |  | ||||||
|       await u.openDebugPanel() |       await u.openDebugPanel() | ||||||
|  |  | ||||||
|  | |||||||
| @ -301,7 +301,7 @@ test( | |||||||
|   } |   } | ||||||
| ) | ) | ||||||
|  |  | ||||||
| test( | test.skip( | ||||||
|   'external change of file contents are reflected in editor', |   'external change of file contents are reflected in editor', | ||||||
|   { tag: '@electron' }, |   { tag: '@electron' }, | ||||||
|   async ({ context, page }, testInfo) => { |   async ({ context, page }, testInfo) => { | ||||||
|  | |||||||
| @ -9,13 +9,15 @@ import { | |||||||
|   sendCustomCmd, |   sendCustomCmd, | ||||||
| } from '../test-utils' | } from '../test-utils' | ||||||
|  |  | ||||||
| type mouseParams = { | type MouseParams = { | ||||||
|   pixelDiff?: number |   pixelDiff?: number | ||||||
|  |   shouldDbClick?: boolean | ||||||
|  |   delay?: number | ||||||
| } | } | ||||||
| type mouseDragToParams = mouseParams & { | type MouseDragToParams = MouseParams & { | ||||||
|   fromPoint: { x: number; y: number } |   fromPoint: { x: number; y: number } | ||||||
| } | } | ||||||
| type mouseDragFromParams = mouseParams & { | type MouseDragFromParams = MouseParams & { | ||||||
|   toPoint: { x: number; y: number } |   toPoint: { x: number; y: number } | ||||||
| } | } | ||||||
|  |  | ||||||
| @ -26,12 +28,12 @@ type SceneSerialised = { | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| type ClickHandler = (clickParams?: mouseParams) => Promise<void | boolean> | type ClickHandler = (clickParams?: MouseParams) => Promise<void | boolean> | ||||||
| type MoveHandler = (moveParams?: mouseParams) => Promise<void | boolean> | type MoveHandler = (moveParams?: MouseParams) => Promise<void | boolean> | ||||||
| type DblClickHandler = (clickParams?: mouseParams) => Promise<void | boolean> | type DblClickHandler = (clickParams?: MouseParams) => Promise<void | boolean> | ||||||
| type DragToHandler = (dragParams: mouseDragToParams) => Promise<void | boolean> | type DragToHandler = (dragParams: MouseDragToParams) => Promise<void | boolean> | ||||||
| type DragFromHandler = ( | type DragFromHandler = ( | ||||||
|   dragParams: mouseDragFromParams |   dragParams: MouseDragFromParams | ||||||
| ) => Promise<void | boolean> | ) => Promise<void | boolean> | ||||||
|  |  | ||||||
| export class SceneFixture { | export class SceneFixture { | ||||||
| @ -77,17 +79,26 @@ export class SceneFixture { | |||||||
|     { steps }: { steps: number } = { steps: 20 } |     { steps }: { steps: number } = { steps: 20 } | ||||||
|   ): [ClickHandler, MoveHandler, DblClickHandler] => |   ): [ClickHandler, MoveHandler, DblClickHandler] => | ||||||
|     [ |     [ | ||||||
|       (clickParams?: mouseParams) => { |       (clickParams?: MouseParams) => { | ||||||
|         if (clickParams?.pixelDiff) { |         if (clickParams?.pixelDiff) { | ||||||
|           return doAndWaitForImageDiff( |           return doAndWaitForImageDiff( | ||||||
|             this.page, |             this.page, | ||||||
|             () => this.page.mouse.click(x, y), |             () => | ||||||
|  |               clickParams?.shouldDbClick | ||||||
|  |                 ? this.page.mouse.dblclick(x, y, { | ||||||
|  |                     delay: clickParams?.delay || 0, | ||||||
|  |                   }) | ||||||
|  |                 : this.page.mouse.click(x, y, { | ||||||
|  |                     delay: clickParams?.delay || 0, | ||||||
|  |                   }), | ||||||
|             clickParams.pixelDiff |             clickParams.pixelDiff | ||||||
|           ) |           ) | ||||||
|         } |         } | ||||||
|         return this.page.mouse.click(x, y) |         return clickParams?.shouldDbClick | ||||||
|  |           ? this.page.mouse.dblclick(x, y, { delay: clickParams?.delay || 0 }) | ||||||
|  |           : this.page.mouse.click(x, y, { delay: clickParams?.delay || 0 }) | ||||||
|       }, |       }, | ||||||
|       (moveParams?: mouseParams) => { |       (moveParams?: MouseParams) => { | ||||||
|         if (moveParams?.pixelDiff) { |         if (moveParams?.pixelDiff) { | ||||||
|           return doAndWaitForImageDiff( |           return doAndWaitForImageDiff( | ||||||
|             this.page, |             this.page, | ||||||
| @ -97,7 +108,7 @@ export class SceneFixture { | |||||||
|         } |         } | ||||||
|         return this.page.mouse.move(x, y, { steps }) |         return this.page.mouse.move(x, y, { steps }) | ||||||
|       }, |       }, | ||||||
|       (clickParams?: mouseParams) => { |       (clickParams?: MouseParams) => { | ||||||
|         if (clickParams?.pixelDiff) { |         if (clickParams?.pixelDiff) { | ||||||
|           return doAndWaitForImageDiff( |           return doAndWaitForImageDiff( | ||||||
|             this.page, |             this.page, | ||||||
| @ -114,7 +125,7 @@ export class SceneFixture { | |||||||
|     { steps }: { steps: number } = { steps: 20 } |     { steps }: { steps: number } = { steps: 20 } | ||||||
|   ): [DragToHandler, DragFromHandler] => |   ): [DragToHandler, DragFromHandler] => | ||||||
|     [ |     [ | ||||||
|       (dragToParams: mouseDragToParams) => { |       (dragToParams: MouseDragToParams) => { | ||||||
|         if (dragToParams?.pixelDiff) { |         if (dragToParams?.pixelDiff) { | ||||||
|           return doAndWaitForImageDiff( |           return doAndWaitForImageDiff( | ||||||
|             this.page, |             this.page, | ||||||
| @ -131,7 +142,7 @@ export class SceneFixture { | |||||||
|           targetPosition: { x, y }, |           targetPosition: { x, y }, | ||||||
|         }) |         }) | ||||||
|       }, |       }, | ||||||
|       (dragFromParams: mouseDragFromParams) => { |       (dragFromParams: MouseDragFromParams) => { | ||||||
|         if (dragFromParams?.pixelDiff) { |         if (dragFromParams?.pixelDiff) { | ||||||
|           return doAndWaitForImageDiff( |           return doAndWaitForImageDiff( | ||||||
|             this.page, |             this.page, | ||||||
| @ -219,7 +230,7 @@ export class SceneFixture { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   expectPixelColor = async ( |   expectPixelColor = async ( | ||||||
|     colour: [number, number, number], |     colour: [number, number, number] | [number, number, number][], | ||||||
|     coords: { x: number; y: number }, |     coords: { x: number; y: number }, | ||||||
|     diff: number |     diff: number | ||||||
|   ) => { |   ) => { | ||||||
| @ -241,22 +252,36 @@ export class SceneFixture { | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | function isColourArray( | ||||||
|  |   colour: [number, number, number] | [number, number, number][] | ||||||
|  | ): colour is [number, number, number][] { | ||||||
|  |   return Array.isArray(colour[0]) | ||||||
|  | } | ||||||
|  |  | ||||||
| export async function expectPixelColor( | export async function expectPixelColor( | ||||||
|   page: Page, |   page: Page, | ||||||
|   colour: [number, number, number], |   colour: [number, number, number] | [number, number, number][], | ||||||
|   coords: { x: number; y: number }, |   coords: { x: number; y: number }, | ||||||
|   diff: number |   diff: number | ||||||
| ) { | ) { | ||||||
|   let finalValue = colour |   let finalValue = colour | ||||||
|   await expect |   await expect | ||||||
|     .poll(async () => { |     .poll( | ||||||
|  |       async () => { | ||||||
|         const pixel = (await getPixelRGBs(page)(coords, 1))[0] |         const pixel = (await getPixelRGBs(page)(coords, 1))[0] | ||||||
|         if (!pixel) return null |         if (!pixel) return null | ||||||
|         finalValue = pixel |         finalValue = pixel | ||||||
|  |         if (!isColourArray(colour)) { | ||||||
|           return pixel.every( |           return pixel.every( | ||||||
|             (channel, index) => Math.abs(channel - colour[index]) < diff |             (channel, index) => Math.abs(channel - colour[index]) < diff | ||||||
|           ) |           ) | ||||||
|     }) |         } | ||||||
|  |         return colour.some((c) => | ||||||
|  |           c.every((channel, index) => Math.abs(pixel[index] - channel) < diff) | ||||||
|  |         ) | ||||||
|  |       }, | ||||||
|  |       { timeout: 10_000 } | ||||||
|  |     ) | ||||||
|     .toBeTruthy() |     .toBeTruthy() | ||||||
|     .catch((cause) => { |     .catch((cause) => { | ||||||
|       throw new Error( |       throw new Error( | ||||||
|  | |||||||
| @ -22,7 +22,10 @@ export class ToolbarFixture { | |||||||
|   offsetPlaneButton!: Locator |   offsetPlaneButton!: Locator | ||||||
|   startSketchBtn!: Locator |   startSketchBtn!: Locator | ||||||
|   lineBtn!: Locator |   lineBtn!: Locator | ||||||
|  |   tangentialArcBtn!: Locator | ||||||
|  |   circleBtn!: Locator | ||||||
|   rectangleBtn!: Locator |   rectangleBtn!: Locator | ||||||
|  |   lengthConstraintBtn!: Locator | ||||||
|   exitSketchBtn!: Locator |   exitSketchBtn!: Locator | ||||||
|   editSketchBtn!: Locator |   editSketchBtn!: Locator | ||||||
|   fileTreeBtn!: Locator |   fileTreeBtn!: Locator | ||||||
| @ -51,7 +54,10 @@ export class ToolbarFixture { | |||||||
|     this.offsetPlaneButton = page.getByTestId('plane-offset') |     this.offsetPlaneButton = page.getByTestId('plane-offset') | ||||||
|     this.startSketchBtn = page.getByTestId('sketch') |     this.startSketchBtn = page.getByTestId('sketch') | ||||||
|     this.lineBtn = page.getByTestId('line') |     this.lineBtn = page.getByTestId('line') | ||||||
|  |     this.tangentialArcBtn = page.getByTestId('tangential-arc') | ||||||
|  |     this.circleBtn = page.getByTestId('circle-center') | ||||||
|     this.rectangleBtn = page.getByTestId('corner-rectangle') |     this.rectangleBtn = page.getByTestId('corner-rectangle') | ||||||
|  |     this.lengthConstraintBtn = page.getByTestId('constraint-length') | ||||||
|     this.exitSketchBtn = page.getByTestId('sketch-exit') |     this.exitSketchBtn = page.getByTestId('sketch-exit') | ||||||
|     this.editSketchBtn = page.getByText('Edit Sketch') |     this.editSketchBtn = page.getByText('Edit Sketch') | ||||||
|     this.fileTreeBtn = page.locator('[id="files-button-holder"]') |     this.fileTreeBtn = page.locator('[id="files-button-holder"]') | ||||||
| @ -117,6 +123,15 @@ export class ToolbarFixture { | |||||||
|       await expect(this.exeIndicator).toBeVisible({ timeout: 15_000 }) |       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() | ||||||
|  |   } | ||||||
|  |  | ||||||
|   async closePane(paneId: SidebarType) { |   async closePane(paneId: SidebarType) { | ||||||
|     return closePane(this.page, paneId + SIDEBAR_BUTTON_SUFFIX) |     return closePane(this.page, paneId + SIDEBAR_BUTTON_SUFFIX) | ||||||
|  | |||||||
| @ -437,7 +437,7 @@ test.describe('Onboarding tests', () => { | |||||||
|   ) |   ) | ||||||
| }) | }) | ||||||
|  |  | ||||||
| test.fixme( | test( | ||||||
|   'Restarting onboarding on desktop takes one attempt', |   'Restarting onboarding on desktop takes one attempt', | ||||||
|   { |   { | ||||||
|     appSettings: { |     appSettings: { | ||||||
| @ -514,10 +514,7 @@ test.fixme( | |||||||
|       const modelColor: [number, number, number] = [76, 76, 76] |       const modelColor: [number, number, number] = [76, 76, 76] | ||||||
|  |  | ||||||
|       await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y) |       await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y) | ||||||
|       await expectPixelColor(page, modelColor, XYPlanePoint, 8) |  | ||||||
|       await tutorialDismissButton.click() |       await tutorialDismissButton.click() | ||||||
|       // Make sure model still there. |  | ||||||
|       await expectPixelColor(page, modelColor, XYPlanePoint, 8) |  | ||||||
|     }) |     }) | ||||||
|  |  | ||||||
|     await test.step('Clear code and restart onboarding from settings', async () => { |     await test.step('Clear code and restart onboarding from settings', async () => { | ||||||
|  | |||||||
| @ -216,18 +216,13 @@ test.describe('Point-and-click tests', { tag: ['@skipWin'] }, () => { | |||||||
|  |  | ||||||
|         afterChamferSelectSnippet: |         afterChamferSelectSnippet: | ||||||
|           'sketch002 = startSketchOn(extrude001, seg03)', |           'sketch002 = startSketchOn(extrude001, seg03)', | ||||||
|         afterRectangle1stClickSnippet: 'startProfileAt([205.96, 254.59], %)', |         afterRectangle1stClickSnippet: | ||||||
|         afterRectangle2ndClickSnippet: `angledLine([0, 11.39], %, $rectangleSegmentA002) |           'startProfileAt([205.96, 254.59], sketch002)', | ||||||
|     |> angledLine([ |         afterRectangle2ndClickSnippet: `angledLine([0,11.39],%,$rectangleSegmentA002) | ||||||
|          segAng(rectangleSegmentA002) - 90, |         |>angledLine([segAng(rectangleSegmentA002)-90,105.26],%) | ||||||
|          105.26 |         |>angledLine([segAng(rectangleSegmentA002),-segLen(rectangleSegmentA002)],%) | ||||||
|        ], %, $rectangleSegmentB001) |         |>line(endAbsolute=[profileStartX(%),profileStartY(%)],%) | ||||||
|     |> angledLine([ |         |>close(%)`, | ||||||
|          segAng(rectangleSegmentA002), |  | ||||||
|          -segLen(rectangleSegmentA002) |  | ||||||
|        ], %, $rectangleSegmentC001) |  | ||||||
|     |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) |  | ||||||
|     |> close()`, |  | ||||||
|       }) |       }) | ||||||
|  |  | ||||||
|       await sketchOnAChamfer({ |       await sketchOnAChamfer({ | ||||||
| @ -248,19 +243,15 @@ test.describe('Point-and-click tests', { tag: ['@skipWin'] }, () => { | |||||||
|  |  | ||||||
|         afterChamferSelectSnippet: |         afterChamferSelectSnippet: | ||||||
|           'sketch003 = startSketchOn(extrude001, seg04)', |           'sketch003 = startSketchOn(extrude001, seg04)', | ||||||
|         afterRectangle1stClickSnippet: 'startProfileAt([-209.64, 255.28], %)', |         afterRectangle1stClickSnippet: | ||||||
|         afterRectangle2ndClickSnippet: `angledLine([0, 11.56], %, $rectangleSegmentA003) |           'startProfileAt([-209.64, 255.28], sketch003)', | ||||||
|     |> angledLine([ |         afterRectangle2ndClickSnippet: `angledLine([0,11.56],%,$rectangleSegmentA003) | ||||||
|          segAng(rectangleSegmentA003) - 90, |         |>angledLine([segAng(rectangleSegmentA003)-90,106.84],%) | ||||||
|          106.84 |         |>angledLine([segAng(rectangleSegmentA003),-segLen(rectangleSegmentA003)],%) | ||||||
|        ], %, $rectangleSegmentB002) |         |>line(endAbsolute=[profileStartX(%),profileStartY(%)],%) | ||||||
|     |> angledLine([ |         |>close(%)`, | ||||||
|          segAng(rectangleSegmentA003), |  | ||||||
|          -segLen(rectangleSegmentA003) |  | ||||||
|        ], %, $rectangleSegmentC002) |  | ||||||
|     |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) |  | ||||||
|     |> close()`, |  | ||||||
|       }) |       }) | ||||||
|  |  | ||||||
|       await sketchOnAChamfer({ |       await sketchOnAChamfer({ | ||||||
|         clickCoords: { x: 677, y: 87 }, |         clickCoords: { x: 677, y: 87 }, | ||||||
|         cameraPos: { x: -6200, y: 1500, z: 6200 }, |         cameraPos: { x: -6200, y: 1500, z: 6200 }, | ||||||
| @ -273,19 +264,14 @@ test.describe('Point-and-click tests', { tag: ['@skipWin'] }, () => { | |||||||
|          ] |          ] | ||||||
|        }, %)`, |        }, %)`, | ||||||
|         afterChamferSelectSnippet: |         afterChamferSelectSnippet: | ||||||
|           'sketch003 = startSketchOn(extrude001, seg04)', |           'sketch004 = startSketchOn(extrude001, seg05)', | ||||||
|         afterRectangle1stClickSnippet: 'startProfileAt([75.8, 317.2], %)', |         afterRectangle1stClickSnippet: | ||||||
|         afterRectangle2ndClickSnippet: `angledLine([0, 11.56], %, $rectangleSegmentA003) |           'startProfileAt([82.57, 322.96], sketch004)', | ||||||
|     |> angledLine([ |         afterRectangle2ndClickSnippet: `angledLine([0,11.16],%,$rectangleSegmentA004) | ||||||
|          segAng(rectangleSegmentA003) - 90, |         |>angledLine([segAng(rectangleSegmentA004)-90,103.07],%) | ||||||
|          106.84 |         |>angledLine([segAng(rectangleSegmentA004),-segLen(rectangleSegmentA004)],%) | ||||||
|        ], %, $rectangleSegmentB002) |         |>line(endAbsolute=[profileStartX(%),profileStartY(%)],%)| | ||||||
|     |> angledLine([ |         >close(%)`, | ||||||
|          segAng(rectangleSegmentA003), |  | ||||||
|          -segLen(rectangleSegmentA003) |  | ||||||
|        ], %, $rectangleSegmentC002) |  | ||||||
|     |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) |  | ||||||
|     |> close()`, |  | ||||||
|       }) |       }) | ||||||
|       /// last one |       /// last one | ||||||
|       await sketchOnAChamfer({ |       await sketchOnAChamfer({ | ||||||
| @ -298,25 +284,18 @@ test.describe('Point-and-click tests', { tag: ['@skipWin'] }, () => { | |||||||
|        }, %)`, |        }, %)`, | ||||||
|         afterChamferSelectSnippet: |         afterChamferSelectSnippet: | ||||||
|           'sketch005 = startSketchOn(extrude001, seg06)', |           'sketch005 = startSketchOn(extrude001, seg06)', | ||||||
|         afterRectangle1stClickSnippet: 'startProfileAt([-23.43, 19.69], %)', |         afterRectangle1stClickSnippet: | ||||||
|         afterRectangle2ndClickSnippet: `angledLine([0, 9.1], %, $rectangleSegmentA005) |           'startProfileAt([-23.43, 19.69], sketch005)', | ||||||
|  |         afterRectangle2ndClickSnippet: `angledLine([0,9.1],%,$rectangleSegmentA005) | ||||||
|     |> angledLine([ |         |>angledLine([segAng(rectangleSegmentA005)-90,84.07],%) | ||||||
|          segAng(rectangleSegmentA005) - 90, |         |>angledLine([segAng(rectangleSegmentA005),-segLen(rectangleSegmentA005)],%) | ||||||
|          84.07 |         |>line(endAbsolute=[profileStartX(%),profileStartY(%)],%) | ||||||
|        ], %, $rectangleSegmentB004) |         |>close(%)`, | ||||||
|     |> angledLine([ |  | ||||||
|          segAng(rectangleSegmentA005), |  | ||||||
|          -segLen(rectangleSegmentA005) |  | ||||||
|        ], %, $rectangleSegmentC004) |  | ||||||
|     |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) |  | ||||||
|     |> close()`, |  | ||||||
|       }) |       }) | ||||||
|  |  | ||||||
|       await test.step('verify at the end of the test that final code is what is expected', async () => { |       await test.step('verify at the end of the test that final code is what is expected', async () => { | ||||||
|         await editor.expectEditor.toContain( |         await editor.expectEditor.toContain( | ||||||
|           `sketch001 = startSketchOn('XZ') |           `sketch001 = startSketchOn('XZ') | ||||||
|  |  | ||||||
|   |> startProfileAt([75.8, 317.2], %) // [$startCapTag, $EndCapTag] |   |> startProfileAt([75.8, 317.2], %) // [$startCapTag, $EndCapTag] | ||||||
|   |> angledLine([0, 268.43], %, $rectangleSegmentA001) |   |> angledLine([0, 268.43], %, $rectangleSegmentA001) | ||||||
|   |> angledLine([ |   |> angledLine([ | ||||||
| @ -327,9 +306,9 @@ test.describe('Point-and-click tests', { tag: ['@skipWin'] }, () => { | |||||||
|        segAng(rectangleSegmentA001), |        segAng(rectangleSegmentA001), | ||||||
|        -segLen(rectangleSegmentA001) |        -segLen(rectangleSegmentA001) | ||||||
|      ], %, $yo) |      ], %, $yo) | ||||||
|       |> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $seg02) |   |> line(endAbsolute=[profileStartX(%), profileStartY(%)], %, $seg02) | ||||||
|       |> close() |   |> close(%) | ||||||
|     extrude001 = extrude(sketch001, length = 100) | extrude001 = extrude(100, sketch001) | ||||||
|   |> chamfer({ |   |> chamfer({ | ||||||
|        length = 30, |        length = 30, | ||||||
|        tags = [getOppositeEdge(seg01)] |        tags = [getOppositeEdge(seg01)] | ||||||
| @ -343,59 +322,59 @@ test.describe('Point-and-click tests', { tag: ['@skipWin'] }, () => { | |||||||
|        length = 30, |        length = 30, | ||||||
|        tags = [getNextAdjacentEdge(yo)] |        tags = [getNextAdjacentEdge(yo)] | ||||||
|      }, %, $seg06) |      }, %, $seg06) | ||||||
|     sketch005 = startSketchOn(extrude001, seg06) | sketch005 = startSketchOn(extrude001, seg06) | ||||||
|       |> startProfileAt([-23.43,19.69], %) | profile004 = startProfileAt([-23.43, 19.69], sketch005) | ||||||
|   |> angledLine([0, 9.1], %, $rectangleSegmentA005) |   |> angledLine([0, 9.1], %, $rectangleSegmentA005) | ||||||
|   |> angledLine([ |   |> angledLine([ | ||||||
|        segAng(rectangleSegmentA005) - 90, |        segAng(rectangleSegmentA005) - 90, | ||||||
|        84.07 |        84.07 | ||||||
|          ], %, $rectangleSegmentB004) |      ], %) | ||||||
|   |> angledLine([ |   |> angledLine([ | ||||||
|        segAng(rectangleSegmentA005), |        segAng(rectangleSegmentA005), | ||||||
|        -segLen(rectangleSegmentA005) |        -segLen(rectangleSegmentA005) | ||||||
|          ], %, $rectangleSegmentC004) |      ], %) | ||||||
|       |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) |   |> line(endAbsolute=[profileStartX(%), profileStartY(%)], %) | ||||||
|       |> close() |   |> close(%) | ||||||
|     sketch004 = startSketchOn(extrude001, seg05) | sketch004 = startSketchOn(extrude001, seg05) | ||||||
|       |> startProfileAt([82.57,322.96], %) | profile003 = startProfileAt([82.57, 322.96], sketch004) | ||||||
|   |> angledLine([0, 11.16], %, $rectangleSegmentA004) |   |> angledLine([0, 11.16], %, $rectangleSegmentA004) | ||||||
|   |> angledLine([ |   |> angledLine([ | ||||||
|        segAng(rectangleSegmentA004) - 90, |        segAng(rectangleSegmentA004) - 90, | ||||||
|        103.07 |        103.07 | ||||||
|          ], %, $rectangleSegmentB003) |      ], %) | ||||||
|   |> angledLine([ |   |> angledLine([ | ||||||
|        segAng(rectangleSegmentA004), |        segAng(rectangleSegmentA004), | ||||||
|        -segLen(rectangleSegmentA004) |        -segLen(rectangleSegmentA004) | ||||||
|          ], %, $rectangleSegmentC003) |      ], %) | ||||||
|       |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) |   |> line(endAbsolute=[profileStartX(%), profileStartY(%)], %) | ||||||
|       |> close() |   |> close(%) | ||||||
|     sketch003 = startSketchOn(extrude001, seg04) | sketch003 = startSketchOn(extrude001, seg04) | ||||||
|       |> startProfileAt([-209.64,255.28], %) | profile002 = startProfileAt([-209.64, 255.28], sketch003) | ||||||
|   |> angledLine([0, 11.56], %, $rectangleSegmentA003) |   |> angledLine([0, 11.56], %, $rectangleSegmentA003) | ||||||
|   |> angledLine([ |   |> angledLine([ | ||||||
|        segAng(rectangleSegmentA003) - 90, |        segAng(rectangleSegmentA003) - 90, | ||||||
|        106.84 |        106.84 | ||||||
|          ], %, $rectangleSegmentB002) |      ], %) | ||||||
|   |> angledLine([ |   |> angledLine([ | ||||||
|        segAng(rectangleSegmentA003), |        segAng(rectangleSegmentA003), | ||||||
|        -segLen(rectangleSegmentA003) |        -segLen(rectangleSegmentA003) | ||||||
|          ], %, $rectangleSegmentC002) |      ], %) | ||||||
|       |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) |   |> line(endAbsolute=[profileStartX(%), profileStartY(%)], %) | ||||||
|       |> close() |   |> close(%) | ||||||
|     sketch002 = startSketchOn(extrude001, seg03) | sketch002 = startSketchOn(extrude001, seg03) | ||||||
|       |> startProfileAt([205.96,254.59], %) | profile001 = startProfileAt([205.96, 254.59], sketch002) | ||||||
|   |> angledLine([0, 11.39], %, $rectangleSegmentA002) |   |> angledLine([0, 11.39], %, $rectangleSegmentA002) | ||||||
|   |> angledLine([ |   |> angledLine([ | ||||||
|        segAng(rectangleSegmentA002) - 90, |        segAng(rectangleSegmentA002) - 90, | ||||||
|        105.26 |        105.26 | ||||||
|          ], %, $rectangleSegmentB001) |      ], %) | ||||||
|   |> angledLine([ |   |> angledLine([ | ||||||
|        segAng(rectangleSegmentA002), |        segAng(rectangleSegmentA002), | ||||||
|        -segLen(rectangleSegmentA002) |        -segLen(rectangleSegmentA002) | ||||||
|          ], %, $rectangleSegmentC001) |      ], %) | ||||||
|       |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) |   |> line(endAbsolute=[profileStartX(%), profileStartY(%)], %) | ||||||
|       |> close() |   |> close(%) | ||||||
|     `, | `, | ||||||
|           { shouldNormalise: true } |           { shouldNormalise: true } | ||||||
|         ) |         ) | ||||||
|       }) |       }) | ||||||
| @ -439,18 +418,13 @@ test.describe('Point-and-click tests', { tag: ['@skipWin'] }, () => { | |||||||
|         beforeChamferSnippetEnd: '}, extrude001)', |         beforeChamferSnippetEnd: '}, extrude001)', | ||||||
|         afterChamferSelectSnippet: |         afterChamferSelectSnippet: | ||||||
|           'sketch002 = startSketchOn(extrude001, seg03)', |           'sketch002 = startSketchOn(extrude001, seg03)', | ||||||
|         afterRectangle1stClickSnippet: 'startProfileAt([205.96, 254.59], %)', |         afterRectangle1stClickSnippet: | ||||||
|         afterRectangle2ndClickSnippet: `angledLine([0, 11.39], %, $rectangleSegmentA002) |           'startProfileAt([205.96, 254.59], sketch002)', | ||||||
|     |> angledLine([ |         afterRectangle2ndClickSnippet: `angledLine([0,11.39],%,$rectangleSegmentA002) | ||||||
|          segAng(rectangleSegmentA002) - 90, |         |>angledLine([segAng(rectangleSegmentA002)-90,105.26],%) | ||||||
|          105.26 |         |>angledLine([segAng(rectangleSegmentA002),-segLen(rectangleSegmentA002)],%) | ||||||
|        ], %, $rectangleSegmentB001) |         |>line(endAbsolute=[profileStartX(%),profileStartY(%)],%) | ||||||
|     |> angledLine([ |         |>close(%)`, | ||||||
|          segAng(rectangleSegmentA002), |  | ||||||
|          -segLen(rectangleSegmentA002) |  | ||||||
|        ], %, $rectangleSegmentC001) |  | ||||||
|     |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) |  | ||||||
|     |> close()`, |  | ||||||
|       }) |       }) | ||||||
|       await editor.expectEditor.toContain( |       await editor.expectEditor.toContain( | ||||||
|         `sketch001 = startSketchOn('XZ') |         `sketch001 = startSketchOn('XZ') | ||||||
| @ -480,24 +454,119 @@ chamf = chamfer({ | |||||||
|        ] |        ] | ||||||
|      }, %) |      }, %) | ||||||
| sketch002 = startSketchOn(extrude001, seg03) | sketch002 = startSketchOn(extrude001, seg03) | ||||||
|   |> startProfileAt([205.96, 254.59], %) | profile001 = startProfileAt([205.96, 254.59], sketch002) | ||||||
|   |> angledLine([0, 11.39], %, $rectangleSegmentA002) |   |> angledLine([0, 11.39], %, $rectangleSegmentA002) | ||||||
|   |> angledLine([ |   |> angledLine([ | ||||||
|        segAng(rectangleSegmentA002) - 90, |        segAng(rectangleSegmentA002) - 90, | ||||||
|        105.26 |        105.26 | ||||||
|      ], %, $rectangleSegmentB001) |      ], %) | ||||||
|   |> angledLine([ |   |> angledLine([ | ||||||
|        segAng(rectangleSegmentA002), |        segAng(rectangleSegmentA002), | ||||||
|        -segLen(rectangleSegmentA002) |        -segLen(rectangleSegmentA002) | ||||||
|      ], %, $rectangleSegmentC001) |      ], %) | ||||||
|   |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) |   |> line(endAbsolute=[profileStartX(%), profileStartY(%)], %) | ||||||
|   |> close() |   |> close(%) | ||||||
| `, | `, | ||||||
|         { shouldNormalise: true } |         { shouldNormalise: true } | ||||||
|       ) |       ) | ||||||
|     }) |     }) | ||||||
|   }) |   }) | ||||||
|  |  | ||||||
|  |   test(`Verify axis, origin, and horizontal snapping`, async ({ | ||||||
|  |     page, | ||||||
|  |     homePage, | ||||||
|  |     editor, | ||||||
|  |     toolbar, | ||||||
|  |     scene, | ||||||
|  |   }) => { | ||||||
|  |     const viewPortSize = { width: 1200, height: 500 } | ||||||
|  |  | ||||||
|  |     await page.setBodyDimensions(viewPortSize) | ||||||
|  |  | ||||||
|  |     await homePage.goToModelingScene() | ||||||
|  |  | ||||||
|  |     // Constants and locators | ||||||
|  |     // These are mappings from screenspace to KCL coordinates, | ||||||
|  |     // until we merge in our coordinate system helpers | ||||||
|  |     const xzPlane = [ | ||||||
|  |       viewPortSize.width * 0.65, | ||||||
|  |       viewPortSize.height * 0.3, | ||||||
|  |     ] as const | ||||||
|  |     const originSloppy = { | ||||||
|  |       screen: [ | ||||||
|  |         viewPortSize.width / 2 + 3, // 3px off the center of the screen | ||||||
|  |         viewPortSize.height / 2, | ||||||
|  |       ], | ||||||
|  |       kcl: [0, 0], | ||||||
|  |     } as const | ||||||
|  |     const xAxisSloppy = { | ||||||
|  |       screen: [ | ||||||
|  |         viewPortSize.width * 0.75, | ||||||
|  |         viewPortSize.height / 2 - 3, // 3px off the X-axis | ||||||
|  |       ], | ||||||
|  |       kcl: [20.34, 0], | ||||||
|  |     } as const | ||||||
|  |     const offYAxis = { | ||||||
|  |       screen: [ | ||||||
|  |         viewPortSize.width * 0.6, // Well off the Y-axis, out of snapping range | ||||||
|  |         viewPortSize.height * 0.3, | ||||||
|  |       ], | ||||||
|  |       kcl: [8.14, 6.78], | ||||||
|  |     } as const | ||||||
|  |     const yAxisSloppy = { | ||||||
|  |       screen: [ | ||||||
|  |         viewPortSize.width / 2 + 5, // 5px off the Y-axis | ||||||
|  |         viewPortSize.height * 0.3, | ||||||
|  |       ], | ||||||
|  |       kcl: [0, 6.78], | ||||||
|  |     } as const | ||||||
|  |     const [clickOnXzPlane, moveToXzPlane] = scene.makeMouseHelpers(...xzPlane) | ||||||
|  |     const [clickOriginSloppy] = scene.makeMouseHelpers(...originSloppy.screen) | ||||||
|  |     const [clickXAxisSloppy, moveXAxisSloppy] = scene.makeMouseHelpers( | ||||||
|  |       ...xAxisSloppy.screen | ||||||
|  |     ) | ||||||
|  |     const [dragToOffYAxis, dragFromOffAxis] = scene.makeDragHelpers( | ||||||
|  |       ...offYAxis.screen | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     const expectedCodeSnippets = { | ||||||
|  |       sketchOnXzPlane: `sketch001 = startSketchOn('XZ')`, | ||||||
|  |       pointAtOrigin: `startProfileAt([${originSloppy.kcl[0]}, ${originSloppy.kcl[1]}], sketch001)`, | ||||||
|  |       segmentOnXAxis: `xLine(${xAxisSloppy.kcl[0]}, %)`, | ||||||
|  |       afterSegmentDraggedOffYAxis: `startProfileAt([${offYAxis.kcl[0]}, ${offYAxis.kcl[1]}], sketch001)`, | ||||||
|  |       afterSegmentDraggedOnYAxis: `startProfileAt([${yAxisSloppy.kcl[0]}, ${yAxisSloppy.kcl[1]}], sketch001)`, | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     await test.step(`Start a sketch on the XZ plane`, async () => { | ||||||
|  |       await editor.closePane() | ||||||
|  |       await toolbar.startSketchPlaneSelection() | ||||||
|  |       await moveToXzPlane() | ||||||
|  |       await clickOnXzPlane() | ||||||
|  |       // timeout wait for engine animation is unavoidable | ||||||
|  |       await page.waitForTimeout(600) | ||||||
|  |       await editor.expectEditor.toContain(expectedCodeSnippets.sketchOnXzPlane) | ||||||
|  |     }) | ||||||
|  |     await test.step(`Place a point a few pixels off the middle, verify it still snaps to 0,0`, async () => { | ||||||
|  |       await clickOriginSloppy() | ||||||
|  |       await editor.expectEditor.toContain(expectedCodeSnippets.pointAtOrigin) | ||||||
|  |     }) | ||||||
|  |     await test.step(`Add a segment on x-axis after moving the mouse a bit, verify it snaps`, async () => { | ||||||
|  |       await moveXAxisSloppy() | ||||||
|  |       await clickXAxisSloppy() | ||||||
|  |       await editor.expectEditor.toContain(expectedCodeSnippets.segmentOnXAxis) | ||||||
|  |     }) | ||||||
|  |     await test.step(`Unequip line tool`, async () => { | ||||||
|  |       await toolbar.lineBtn.click() | ||||||
|  |       await expect(toolbar.lineBtn).not.toHaveAttribute('aria-pressed', 'true') | ||||||
|  |     }) | ||||||
|  |     await test.step(`Drag the origin point up and to the right, verify it's past snapping`, async () => { | ||||||
|  |       await dragToOffYAxis({ | ||||||
|  |         fromPoint: { x: originSloppy.screen[0], y: originSloppy.screen[1] }, | ||||||
|  |       }) | ||||||
|  |     }) | ||||||
|  |   }) | ||||||
|  |   // yo | ||||||
|  |  | ||||||
|   test(`Verify axis, origin, and horizontal snapping`, async ({ |   test(`Verify axis, origin, and horizontal snapping`, async ({ | ||||||
|     page, |     page, | ||||||
|     homePage, |     homePage, | ||||||
| @ -1001,6 +1070,21 @@ sketch002 = startSketchOn('XZ') | |||||||
|       await cmdBar.progressCmdBar() |       await cmdBar.progressCmdBar() | ||||||
|       await page.waitForTimeout(500) |       await page.waitForTimeout(500) | ||||||
|     }) |     }) | ||||||
|  |     //   // yo | ||||||
|  |     //   await clickOnSketch2() | ||||||
|  |     //   await page.waitForTimeout(500) | ||||||
|  |     //   await cmdBar.progressCmdBar() | ||||||
|  |     //   await toolbar.openPane('code') | ||||||
|  |     //   await page.waitForTimeout(500) | ||||||
|  |     // }) | ||||||
|  |  | ||||||
|  |     // await test.step(`Confirm code is added to the editor, scene has changed`, async () => { | ||||||
|  |     //   await scene.expectPixelColor([135, 64, 73], testPoint, 15) | ||||||
|  |     //   await editor.expectEditor.toContain(sweepDeclaration) | ||||||
|  |     //   await editor.expectState({ | ||||||
|  |     //     diagnostics: [], | ||||||
|  |     //     activeLines: [sweepDeclaration], | ||||||
|  |     //     highlightedCode: '', | ||||||
|  |  | ||||||
|     await test.step(`Confirm code is added to the editor, scene has changed`, async () => { |     await test.step(`Confirm code is added to the editor, scene has changed`, async () => { | ||||||
|       await scene.expectPixelColor([135, 64, 73], testPoint, 15) |       await scene.expectPixelColor([135, 64, 73], testPoint, 15) | ||||||
|  | |||||||
| @ -444,8 +444,7 @@ test( | |||||||
|  |  | ||||||
|     const startXPx = 600 |     const startXPx = 600 | ||||||
|     await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) |     await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) | ||||||
|     code += ` |     code += `profile001 = startProfileAt([7.19, -9.7], sketch001)` | ||||||
|   |> startProfileAt([7.19, -9.7], %)` |  | ||||||
|     await expect(page.locator('.cm-content')).toHaveText(code) |     await expect(page.locator('.cm-content')).toHaveText(code) | ||||||
|     await page.waitForTimeout(100) |     await page.waitForTimeout(100) | ||||||
|  |  | ||||||
| @ -467,6 +466,10 @@ test( | |||||||
|       .getByRole('button', { name: 'arc Tangential Arc', exact: true }) |       .getByRole('button', { name: 'arc Tangential Arc', exact: true }) | ||||||
|       .click() |       .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.mouse.move(startXPx + PUR * 30, 500 - PUR * 20, { steps: 10 }) | ||||||
|  |  | ||||||
|     await page.waitForTimeout(1000) |     await page.waitForTimeout(1000) | ||||||
| @ -589,8 +592,7 @@ test( | |||||||
|       mask: [page.getByTestId('model-state-indicator')], |       mask: [page.getByTestId('model-state-indicator')], | ||||||
|     }) |     }) | ||||||
|     await expect(page.locator('.cm-content')).toHaveText( |     await expect(page.locator('.cm-content')).toHaveText( | ||||||
|       `sketch001 = startSketchOn('XZ') |       `sketch001 = startSketchOn('XZ')profile001 = circle({ center = [14.44, -2.44], radius = 1 }, sketch001)` | ||||||
|   |> circle({ center = [14.44, -2.44], radius = 1 }, %)` |  | ||||||
|     ) |     ) | ||||||
|   } |   } | ||||||
| ) | ) | ||||||
| @ -634,8 +636,7 @@ test.describe( | |||||||
|  |  | ||||||
|       const startXPx = 600 |       const startXPx = 600 | ||||||
|       await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) |       await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) | ||||||
|       code += ` |       code += `profile001 = startProfileAt([7.19, -9.7], sketch001)` | ||||||
|   |> startProfileAt([7.19, -9.7], %)` |  | ||||||
|       await expect(u.codeLocator).toHaveText(code) |       await expect(u.codeLocator).toHaveText(code) | ||||||
|       await page.waitForTimeout(100) |       await page.waitForTimeout(100) | ||||||
|  |  | ||||||
| @ -653,6 +654,10 @@ test.describe( | |||||||
|         .click() |         .click() | ||||||
|       await page.waitForTimeout(100) |       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) |       await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20) | ||||||
|  |  | ||||||
|       code += ` |       code += ` | ||||||
| @ -739,8 +744,7 @@ test.describe( | |||||||
|  |  | ||||||
|       const startXPx = 600 |       const startXPx = 600 | ||||||
|       await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) |       await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) | ||||||
|       code += ` |       code += `profile001 = startProfileAt([182.59, -246.32], sketch001)` | ||||||
|   |> startProfileAt([182.59, -246.32], %)` |  | ||||||
|       await expect(u.codeLocator).toHaveText(code) |       await expect(u.codeLocator).toHaveText(code) | ||||||
|       await page.waitForTimeout(100) |       await page.waitForTimeout(100) | ||||||
|  |  | ||||||
| @ -758,6 +762,10 @@ test.describe( | |||||||
|         .click() |         .click() | ||||||
|       await page.waitForTimeout(100) |       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) |       await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20) | ||||||
|  |  | ||||||
|       code += ` |       code += ` | ||||||
|  | |||||||
| Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 55 KiB | 
| Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 53 KiB | 
| Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB | 
| Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 55 KiB | 
| Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 49 KiB | 
| Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 46 KiB | 
| Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 43 KiB | 
| Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 51 KiB | 
| @ -1,6 +1,7 @@ | |||||||
| import { test, expect } from './zoo-test' | import { test, expect } from './zoo-test' | ||||||
|  |  | ||||||
| import { commonPoints, getUtils } from './test-utils' | import { commonPoints, getUtils } from './test-utils' | ||||||
|  | import { EngineCommand } from 'lang/std/artifactGraph' | ||||||
|  | import { uuidv4 } from 'lib/utils' | ||||||
|  |  | ||||||
| test.describe('Test network and connection issues', () => { | test.describe('Test network and connection issues', () => { | ||||||
|   test('simulate network down and network little widget', async ({ |   test('simulate network down and network little widget', async ({ | ||||||
| @ -110,17 +111,16 @@ test.describe('Test network and connection issues', () => { | |||||||
|  |  | ||||||
|     const startXPx = 600 |     const startXPx = 600 | ||||||
|     await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) |     await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) | ||||||
|     await expect(page.locator('.cm-content')) |     await expect(page.locator('.cm-content')).toHaveText( | ||||||
|       .toHaveText(`sketch001 = startSketchOn('XZ') |       `sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${commonPoints.startAt}, sketch001)` | ||||||
|   |> startProfileAt(${commonPoints.startAt}, %)`) |     ) | ||||||
|     await page.waitForTimeout(100) |     await page.waitForTimeout(100) | ||||||
|  |  | ||||||
|     await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10) |     await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10) | ||||||
|     await page.waitForTimeout(100) |     await page.waitForTimeout(100) | ||||||
|  |  | ||||||
|     await expect(page.locator('.cm-content')) |     await expect(page.locator('.cm-content')) | ||||||
|       .toHaveText(`sketch001 = startSketchOn('XZ') |       .toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${commonPoints.startAt}, sketch001) | ||||||
|   |> startProfileAt(${commonPoints.startAt}, %) |  | ||||||
|     |> xLine(${commonPoints.num1}, %)`) |     |> xLine(${commonPoints.num1}, %)`) | ||||||
|  |  | ||||||
|     // Expect the network to be up |     // Expect the network to be up | ||||||
| @ -168,7 +168,9 @@ test.describe('Test network and connection issues', () => { | |||||||
|     await page.mouse.click(100, 100) |     await page.mouse.click(100, 100) | ||||||
|  |  | ||||||
|     // select a line |     // select a line | ||||||
|     await page.getByText(`startProfileAt(${commonPoints.startAt}, %)`).click() |     await page | ||||||
|  |       .getByText(`startProfileAt(${commonPoints.startAt}, sketch001)`) | ||||||
|  |       .click() | ||||||
|  |  | ||||||
|     // enter sketch again |     // enter sketch again | ||||||
|     await u.doAndWaitForCmd( |     await u.doAndWaitForCmd( | ||||||
| @ -182,11 +184,36 @@ test.describe('Test network and connection issues', () => { | |||||||
|  |  | ||||||
|     await page.waitForTimeout(150) |     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 |     // Ensure we can continue sketching | ||||||
|     await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20) |     await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20) | ||||||
|     await expect.poll(u.normalisedEditorCode) |     await expect.poll(u.normalisedEditorCode) | ||||||
|       .toBe(`sketch001 = startSketchOn('XZ') |       .toBe(`sketch001 = startSketchOn('XZ') | ||||||
|   |> startProfileAt([12.34, -12.34], %) | profile001 = startProfileAt([12.34, -12.34], sketch001) | ||||||
|   |> xLine(12.34, %) |   |> xLine(12.34, %) | ||||||
|   |> line(end = [-12.34, 12.34]) |   |> line(end = [-12.34, 12.34]) | ||||||
|  |  | ||||||
| @ -196,7 +223,7 @@ test.describe('Test network and connection issues', () => { | |||||||
|  |  | ||||||
|     await expect.poll(u.normalisedEditorCode) |     await expect.poll(u.normalisedEditorCode) | ||||||
|       .toBe(`sketch001 = startSketchOn('XZ') |       .toBe(`sketch001 = startSketchOn('XZ') | ||||||
|   |> startProfileAt([12.34, -12.34], %) | profile001 = startProfileAt([12.34, -12.34], sketch001) | ||||||
|   |> xLine(12.34, %) |   |> xLine(12.34, %) | ||||||
|   |> line(end = [-12.34, 12.34]) |   |> line(end = [-12.34, 12.34]) | ||||||
|   |> xLine(-12.34, %) |   |> xLine(-12.34, %) | ||||||
|  | |||||||
| @ -19,7 +19,7 @@ test.describe('Testing constraints', { tag: ['@skipWin'] }, () => { | |||||||
|   |> line(end = [20, 0]) |   |> line(end = [20, 0]) | ||||||
|   |> line(end = [0, 20]) |   |> line(end = [0, 20]) | ||||||
|   |> xLine(-20, %) |   |> xLine(-20, %) | ||||||
|     ` | ` | ||||||
|       ) |       ) | ||||||
|     }) |     }) | ||||||
|  |  | ||||||
| @ -673,7 +673,7 @@ test.describe('Testing constraints', { tag: ['@skipWin'] }, () => { | |||||||
|       }, |       }, | ||||||
|     ] as const |     ] as const | ||||||
|     for (const { testName, addVariable, value, constraint } of cases) { |     for (const { testName, addVariable, value, constraint } of cases) { | ||||||
|       test(`${testName}`, async ({ context, homePage, page }) => { |       test(`${testName}`, async ({ context, homePage, page, editor }) => { | ||||||
|         // constants and locators |         // constants and locators | ||||||
|         const cmdBarKclInput = page |         const cmdBarKclInput = page | ||||||
|           .getByTestId('cmd-bar-arg-value') |           .getByTestId('cmd-bar-arg-value') | ||||||
| @ -706,8 +706,11 @@ part002 = startSketchOn('XZ') | |||||||
|         await page.setBodyDimensions({ width: 1200, height: 500 }) |         await page.setBodyDimensions({ width: 1200, height: 500 }) | ||||||
|  |  | ||||||
|         await homePage.goToModelingScene() |         await homePage.goToModelingScene() | ||||||
|  |         await u.waitForPageLoad() | ||||||
|  |  | ||||||
|         await page.getByText('line(end = [74.36, 130.4])').click() |         await editor.scrollToText('line(end = [74.36, 130.4], %)', true) | ||||||
|  |         await page.getByText('line(end = [74.36, 130.4], %)').click() | ||||||
|  |         await page.screenshot({ path: 'ok.png' }) | ||||||
|         await page.getByRole('button', { name: 'Edit Sketch' }).click() |         await page.getByRole('button', { name: 'Edit Sketch' }).click() | ||||||
|  |  | ||||||
|         const line3 = await u.getSegmentBodyCoords( |         const line3 = await u.getSegmentBodyCoords( | ||||||
|  | |||||||
| @ -63,33 +63,38 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => { | |||||||
|     await page.mouse.click(700, 200) |     await page.mouse.click(700, 200) | ||||||
|     await page.waitForTimeout(700) // wait for animation |     await page.waitForTimeout(700) // wait for animation | ||||||
|  |  | ||||||
|  |     // select a plane | ||||||
|  |     await page.mouse.click(700, 200) | ||||||
|  |     await page.waitForTimeout(700) // wait for animation | ||||||
|  |  | ||||||
|     const startXPx = 600 |     const startXPx = 600 | ||||||
|     await u.closeDebugPanel() |     await u.closeDebugPanel() | ||||||
|     await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) |     await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) | ||||||
|     await expect(page.locator('.cm-content')) |     await expect(page.locator('.cm-content')).toHaveText( | ||||||
|       .toHaveText(`sketch001 = startSketchOn('XZ') |       `sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${commonPoints.startAt}, sketch001)` | ||||||
|       |> startProfileAt(${commonPoints.startAt}, %)`) |     ) | ||||||
|  |  | ||||||
|     await page.waitForTimeout(100) |     await page.waitForTimeout(100) | ||||||
|     await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10) |     await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10) | ||||||
|  |  | ||||||
|     await expect(page.locator('.cm-content')) |     await expect(page.locator('.cm-content')) | ||||||
|       .toHaveText(`sketch001 = startSketchOn('XZ') |       .toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${commonPoints.startAt}, sketch001) | ||||||
|       |> startProfileAt(${commonPoints.startAt}, %) |  | ||||||
|     |> xLine(${commonPoints.num1}, %)`) |     |> xLine(${commonPoints.num1}, %)`) | ||||||
|  |  | ||||||
|     await page.waitForTimeout(100) |     await page.waitForTimeout(100) | ||||||
|     await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20) |     await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20) | ||||||
|     await expect(page.locator('.cm-content')) |     await expect(page.locator('.cm-content')) | ||||||
|       .toHaveText(`sketch001 = startSketchOn('XZ') |       .toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${ | ||||||
|       |> startProfileAt(${commonPoints.startAt}, %) |       commonPoints.startAt | ||||||
|  |     }, sketch001) | ||||||
|     |> xLine(${commonPoints.num1}, %) |     |> xLine(${commonPoints.num1}, %) | ||||||
|     |> yLine(${commonPoints.num1 + 0.01}, %)`) |     |> yLine(${commonPoints.num1 + 0.01}, %)`) | ||||||
|     await page.waitForTimeout(100) |     await page.waitForTimeout(100) | ||||||
|     await page.mouse.click(startXPx, 500 - PUR * 20) |     await page.mouse.click(startXPx, 500 - PUR * 20) | ||||||
|     await expect(page.locator('.cm-content')) |     await expect(page.locator('.cm-content')) | ||||||
|       .toHaveText(`sketch001 = startSketchOn('XZ') |       .toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${ | ||||||
|       |> startProfileAt(${commonPoints.startAt}, %) |       commonPoints.startAt | ||||||
|  |     }, sketch001) | ||||||
|     |> xLine(${commonPoints.num1}, %) |     |> xLine(${commonPoints.num1}, %) | ||||||
|     |> yLine(${commonPoints.num1 + 0.01}, %) |     |> yLine(${commonPoints.num1 + 0.01}, %) | ||||||
|     |> xLine(${commonPoints.num2 * -1}, %)`) |     |> xLine(${commonPoints.num2 * -1}, %)`) | ||||||
| @ -256,65 +261,87 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => { | |||||||
|         'persistCode', |         'persistCode', | ||||||
|         `sketch001 = startSketchOn('XZ') |         `sketch001 = startSketchOn('XZ') | ||||||
|         |> startProfileAt([-79.26, 95.04], %) |         |> startProfileAt([-79.26, 95.04], %) | ||||||
|       |> line(end = [112.54, 127.64], tag = $seg02) |         |> line(end=[112.54, 127.64], %, $seg02) | ||||||
|       |> line(end = [170.36, -121.61], tag = $seg01) |         |> line(end=[170.36, -121.61], %, $seg01) | ||||||
|       |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) |         |> line(endAbsolute=[profileStartX(%), profileStartY(%)], %) | ||||||
|       |> close() |         |> close(%) | ||||||
|   extrude001 = extrude(sketch001, length = 50) | extrude001 = extrude(50, sketch001) | ||||||
|   sketch005 = startSketchOn(extrude001, 'END') | sketch005 = startSketchOn(extrude001, 'END') | ||||||
|   |> startProfileAt([23.24, 136.52], %) |   |> startProfileAt([23.24, 136.52], %) | ||||||
|     |> line(end = [-8.44, 36.61]) |   |> line(end=[-8.44, 36.61], %) | ||||||
|     |> line(end = [49.4, 2.05]) |   |> line(end=[49.4, 2.05], %) | ||||||
|     |> line(end = [29.69, -46.95]) |   |> line(end=[29.69, -46.95], %) | ||||||
|     |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) |   |> line(endAbsolute=[profileStartX(%), profileStartY(%)], %) | ||||||
|     |> close() |   |> close(%) | ||||||
|   sketch003 = startSketchOn(extrude001, seg01) | sketch003 = startSketchOn(extrude001, seg01) | ||||||
|   |> startProfileAt([21.23, 17.81], %) |   |> startProfileAt([21.23, 17.81], %) | ||||||
|     |> line(end = [51.97, 21.32]) |   |> line(end=[51.97, 21.32], %) | ||||||
|     |> line(end = [4.07, -22.75]) |   |> line(end=[4.07, -22.75], %) | ||||||
|     |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) |   |> line(endAbsolute=[profileStartX(%), profileStartY(%)], %) | ||||||
|     |> close() |   |> close(%) | ||||||
|   sketch002 = startSketchOn(extrude001, seg02) | sketch002 = startSketchOn(extrude001, seg02) | ||||||
|   |> startProfileAt([-100.54, 16.99], %) |   |> startProfileAt([-100.54, 16.99], %) | ||||||
|     |> line(end = [0, 20.03]) |   |> line(end=[0, 20.03], %) | ||||||
|     |> line(end = [62.61, 0], tag = $seg03) |   |> line(end=[62.61, 0], %, $seg03) | ||||||
|     |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) |   |> line(endAbsolute=[profileStartX(%), profileStartY(%)], %) | ||||||
|     |> close() |   |> close(%) | ||||||
|   extrude002 = extrude(sketch002, length = 50) | extrude002 = extrude(50, sketch002) | ||||||
|   sketch004 = startSketchOn(extrude002, seg03) | sketch004 = startSketchOn(extrude002, seg03) | ||||||
|   |> startProfileAt([57.07, 134.77], %) |   |> startProfileAt([57.07, 134.77], %) | ||||||
|     |> line(end = [-4.72, 22.84]) |   |> line(end=[-4.72, 22.84], %) | ||||||
|     |> line(end = [28.8, 6.71]) |   |> line(end=[28.8, 6.71], %) | ||||||
|     |> line(end = [9.19, -25.33]) |   |> line(end=[9.19, -25.33], %) | ||||||
|     |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) |   |> line(endAbsolute=[profileStartX(%), profileStartY(%)], %) | ||||||
|     |> close() |   |> close(%) | ||||||
|   extrude003 = extrude(sketch004, length = 20) | extrude003 = extrude(20, sketch004) | ||||||
|   pipeLength = 40 | pipeLength = 40 | ||||||
|   pipeSmallDia = 10 | pipeSmallDia = 10 | ||||||
|   pipeLargeDia = 20 | pipeLargeDia = 20 | ||||||
|   thickness = 0.5 | thickness = 0.5 | ||||||
|   part009 = startSketchOn('XY') | part009 = startSketchOn('XY') | ||||||
|   |> startProfileAt([pipeLargeDia - (thickness / 2), 38], %) |   |> startProfileAt([pipeLargeDia - (thickness / 2), 38], %) | ||||||
|     |> line(end = [thickness, 0]) |   |> line(end=[thickness, 0], %) | ||||||
|     |> line(end = [0, -1]) |   |> line(end=[0, -1], %) | ||||||
|   |> angledLineToX({ |   |> angledLineToX({ | ||||||
|        angle = 60, |        angle = 60, | ||||||
|        to = pipeSmallDia + thickness |        to = pipeSmallDia + thickness | ||||||
|      }, %) |      }, %) | ||||||
|     |> line(end = [0, -pipeLength]) |   |> line(end=[0, -pipeLength], %) | ||||||
|   |> angledLineToX({ |   |> angledLineToX({ | ||||||
|        angle = -60, |        angle = -60, | ||||||
|        to = pipeLargeDia + thickness |        to = pipeLargeDia + thickness | ||||||
|      }, %) |      }, %) | ||||||
|     |> line(end = [0, -1]) |   |> line(end=[0, -1], %) | ||||||
|     |> line(end = [-thickness, 0]) |   |> line(end=[-thickness, 0], %) | ||||||
|     |> line(end = [0, 1]) |   |> line(end=[0, 1], %) | ||||||
|   |> angledLineToX({ angle = 120, to = pipeSmallDia }, %) |   |> angledLineToX({ angle = 120, to = pipeSmallDia }, %) | ||||||
|     |> line(end = [0, pipeLength]) |   |> line(end=[0, pipeLength], %) | ||||||
|   |> angledLineToX({ angle = 60, to = pipeLargeDia }, %) |   |> angledLineToX({ angle = 60, to = pipeLargeDia }, %) | ||||||
|     |> close() |   |> close(%) | ||||||
|   rev = revolve({ axis: 'y' }, part009) | 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) | ||||||
|  |      ], %) | ||||||
|  |   |> line(endAbsolute=[profileStartX(%), profileStartY(%)], %) | ||||||
|  |   |> close(%) | ||||||
|  | profile003 = startProfileAt([40.16, -120.48], sketch006) | ||||||
|  |   |> line(end=[26.95, 24.21], %) | ||||||
|  |   |> line(end=[20.91, -28.61], %) | ||||||
|  |   |> line(end=[32.46, 18.71], %) | ||||||
|  |  | ||||||
|  | ` | ||||||
|       ) |       ) | ||||||
|     }, KCL_DEFAULT_LENGTH) |     }, KCL_DEFAULT_LENGTH) | ||||||
|     await page.setBodyDimensions({ width: 1000, height: 500 }) |     await page.setBodyDimensions({ width: 1000, height: 500 }) | ||||||
| @ -346,9 +373,10 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => { | |||||||
|     }) |     }) | ||||||
|     await page.waitForTimeout(100) |     await page.waitForTimeout(100) | ||||||
|  |  | ||||||
|     const revolve = { x: 646, y: 248 } |     const revolve = { x: 635, y: 253 } | ||||||
|     const parentExtrude = { x: 915, y: 133 } |     const parentExtrude = { x: 915, y: 133 } | ||||||
|     const solid2d = { x: 770, y: 167 } |     const solid2d = { x: 770, y: 167 } | ||||||
|  |     const individualProfile = { x: 694, y: 432 } | ||||||
|  |  | ||||||
|     // DELETE REVOLVE |     // DELETE REVOLVE | ||||||
|     await page.mouse.click(revolve.x, revolve.y) |     await page.mouse.click(revolve.x, revolve.y) | ||||||
| @ -414,6 +442,20 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => { | |||||||
|     await u.expectCmdLog('[data-message-type="execution-done"]', 10_000) |     await u.expectCmdLog('[data-message-type="execution-done"]', 10_000) | ||||||
|     await page.waitForTimeout(200) |     await page.waitForTimeout(200) | ||||||
|     await expect(u.codeLocator).not.toContainText(`sketch005 = startSketchOn({`) |     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 ({ |   test("Deleting solid that the AST mod can't handle results in a toast message", async ({ | ||||||
|     page, |     page, | ||||||
| @ -1211,12 +1253,15 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => { | |||||||
|  |  | ||||||
|     await page.waitForTimeout(600) |     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 |     // 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) |     await page.waitForTimeout(600) | ||||||
|  |  | ||||||
|     // Code before exiting the tool |     // 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 |     // deselect the line tool by clicking it | ||||||
|     await page.getByRole('button', { name: 'line Line', exact: true }).click() |     await page.getByRole('button', { name: 'line Line', exact: true }).click() | ||||||
| @ -1228,14 +1273,23 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => { | |||||||
|     await page.mouse.click(750, 200) |     await page.mouse.click(750, 200) | ||||||
|     await page.waitForTimeout(100) |     await page.waitForTimeout(100) | ||||||
|  |  | ||||||
|     // expect no change |     await expect | ||||||
|     await expect(page.locator('.cm-content')).toHaveText(previousCodeContent) |       .poll(async () => { | ||||||
|  |         let str = await page.locator('.cm-content').innerText() | ||||||
|  |         str = str.replace(/\s+/g, '') | ||||||
|  |         return str | ||||||
|  |       }) | ||||||
|  |       .toBe(previousCodeContent) | ||||||
|  |  | ||||||
|     // select line tool again |     // select line tool again | ||||||
|     await page.getByRole('button', { name: 'line Line', exact: true }).click() |     await page.getByRole('button', { name: 'line Line', exact: true }).click() | ||||||
|  |  | ||||||
|     await u.closeDebugPanel() |     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 |     // line tool should work as expected again | ||||||
|     await page.mouse.click(700, 200) |     await page.mouse.click(700, 200) | ||||||
|     await expect(page.locator('.cm-content')).not.toHaveText( |     await expect(page.locator('.cm-content')).not.toHaveText( | ||||||
|  | |||||||
| @ -205,8 +205,13 @@ test('First escape in tool pops you out of tool, second exits sketch mode', asyn | |||||||
|   // Draw a line |   // Draw a line | ||||||
|   await page.mouse.move(700, 200, { steps: 5 }) |   await page.mouse.move(700, 200, { steps: 5 }) | ||||||
|   await page.mouse.click(700, 200) |   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 |   // Unequip line tool | ||||||
|   await page.keyboard.press('Escape') |   await page.keyboard.press('Escape') | ||||||
|   // Make sure we didn't pop out of sketch mode. |   // Make sure we didn't pop out of sketch mode. | ||||||
| @ -215,9 +220,17 @@ test('First escape in tool pops you out of tool, second exits sketch mode', asyn | |||||||
|   // Equip arc tool |   // Equip arc tool | ||||||
|   await page.keyboard.press('a') |   await page.keyboard.press('a') | ||||||
|   await expect(arcButton).toHaveAttribute('aria-pressed', 'true') |   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.move(1000, 100, { steps: 5 }) | ||||||
|   await page.mouse.click(1000, 100) |   await page.mouse.click(1000, 100) | ||||||
|   await page.keyboard.press('Escape') |   await page.keyboard.press('Escape') | ||||||
|  |   await expect(arcButton).toHaveAttribute('aria-pressed', 'false') | ||||||
|   await page.keyboard.press('l') |   await page.keyboard.press('l') | ||||||
|   await expect(lineButton).toHaveAttribute('aria-pressed', 'true') |   await expect(lineButton).toHaveAttribute('aria-pressed', 'true') | ||||||
|  |  | ||||||
| @ -519,11 +532,11 @@ extrude001 = extrude(sketch001, length = 5 + 7)` | |||||||
|  |  | ||||||
|   await expect.poll(u.normalisedEditorCode).toContain( |   await expect.poll(u.normalisedEditorCode).toContain( | ||||||
|     u.normalisedCode(`sketch002 = startSketchOn(extrude001, seg01) |     u.normalisedCode(`sketch002 = startSketchOn(extrude001, seg01) | ||||||
|   |> startProfileAt([-12.94, 6.6], %) | profile001 = startProfileAt([-12.88, 6.66], sketch002) | ||||||
|   |> line(end = [2.45, -0.2]) |   |> line(end = [2.71, -0.22], %) | ||||||
|   |> line(end = [-2.6, -1.25]) |   |> line(end = [-2.87, -1.38], %) | ||||||
|   |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) |   |> lineTo(endAbsolute = [profileStartX(%), profileStartY(%)], %) | ||||||
|   |> close() |   |> close(%) | ||||||
| `) | `) | ||||||
|   ) |   ) | ||||||
|  |  | ||||||
| @ -537,9 +550,8 @@ extrude001 = extrude(sketch001, length = 5 + 7)` | |||||||
|   await page.getByText('startProfileAt([-12').click() |   await page.getByText('startProfileAt([-12').click() | ||||||
|   await expect(page.getByRole('button', { name: 'Edit Sketch' })).toBeVisible() |   await expect(page.getByRole('button', { name: 'Edit Sketch' })).toBeVisible() | ||||||
|   await page.getByRole('button', { name: 'Edit Sketch' }).click() |   await page.getByRole('button', { name: 'Edit Sketch' }).click() | ||||||
|   await page.waitForTimeout(400) |   await page.waitForTimeout(500) | ||||||
|   await page.waitForTimeout(150) |   await page.setViewportSize({ width: 1200, height: 1200 }) | ||||||
|   await page.setBodyDimensions({ width: 1200, height: 1200 }) |  | ||||||
|   await u.openAndClearDebugPanel() |   await u.openAndClearDebugPanel() | ||||||
|   await u.updateCamPosition([452, -152, 1166]) |   await u.updateCamPosition([452, -152, 1166]) | ||||||
|   await u.closeDebugPanel() |   await u.closeDebugPanel() | ||||||
|  | |||||||
| @ -5,7 +5,6 @@ import { useModelingContext } from 'hooks/useModelingContext' | |||||||
| import { useNetworkContext } from 'hooks/useNetworkContext' | import { useNetworkContext } from 'hooks/useNetworkContext' | ||||||
| import { NetworkHealthState } from 'hooks/useNetworkStatus' | import { NetworkHealthState } from 'hooks/useNetworkStatus' | ||||||
| import { ActionButton } from 'components/ActionButton' | import { ActionButton } from 'components/ActionButton' | ||||||
| import { isSingleCursorInPipe } from 'lang/queryAst' |  | ||||||
| import { useKclContext } from 'lang/KclProvider' | import { useKclContext } from 'lang/KclProvider' | ||||||
| import { ActionButtonDropdown } from 'components/ActionButtonDropdown' | import { ActionButtonDropdown } from 'components/ActionButtonDropdown' | ||||||
| import { useHotkeys } from 'react-hotkeys-hook' | import { useHotkeys } from 'react-hotkeys-hook' | ||||||
| @ -21,6 +20,7 @@ import { | |||||||
| } from 'lib/toolbar' | } from 'lib/toolbar' | ||||||
| import { isDesktop } from 'lib/isDesktop' | import { isDesktop } from 'lib/isDesktop' | ||||||
| import { openExternalBrowserIfDesktop } from 'lib/openWindow' | import { openExternalBrowserIfDesktop } from 'lib/openWindow' | ||||||
|  | import { isCursorInFunctionDefinition } from 'lang/queryAst' | ||||||
| import { commandBarActor } from 'machines/commandBarMachine' | import { commandBarActor } from 'machines/commandBarMachine' | ||||||
| import { isArray } from 'lib/utils' | import { isArray } from 'lib/utils' | ||||||
|  |  | ||||||
| @ -37,7 +37,12 @@ export function Toolbar({ | |||||||
|   const buttonBorderClassName = '!border-transparent' |   const buttonBorderClassName = '!border-transparent' | ||||||
|  |  | ||||||
|   const sketchPathId = useMemo(() => { |   const sketchPathId = useMemo(() => { | ||||||
|     if (!isSingleCursorInPipe(context.selectionRanges, kclManager.ast)) |     if ( | ||||||
|  |       isCursorInFunctionDefinition( | ||||||
|  |         kclManager.ast, | ||||||
|  |         context.selectionRanges.graphSelections[0] | ||||||
|  |       ) | ||||||
|  |     ) | ||||||
|       return false |       return false | ||||||
|     return isCursorInSketchCommandRange( |     return isCursorInSketchCommandRange( | ||||||
|       engineCommandManager.artifactGraph, |       engineCommandManager.artifactGraph, | ||||||
|  | |||||||
| @ -125,14 +125,7 @@ export const ClientSideScene = ({ | |||||||
|         'mouseup', |         'mouseup', | ||||||
|         toSync(sceneInfra.onMouseUp, reportRejection) |         toSync(sceneInfra.onMouseUp, reportRejection) | ||||||
|       ) |       ) | ||||||
|       sceneEntitiesManager |       sceneEntitiesManager.tearDownSketch({ removeAxis: true }) | ||||||
|         .tearDownSketch() |  | ||||||
|         .then(() => { |  | ||||||
|           // no op |  | ||||||
|         }) |  | ||||||
|         .catch((e) => { |  | ||||||
|           console.error(e) |  | ||||||
|         }) |  | ||||||
|     } |     } | ||||||
|   }, []) |   }, []) | ||||||
|  |  | ||||||
| @ -191,12 +184,15 @@ const Overlays = () => { | |||||||
|       style={{ zIndex: '99999999' }} |       style={{ zIndex: '99999999' }} | ||||||
|     > |     > | ||||||
|       {Object.entries(context.segmentOverlays) |       {Object.entries(context.segmentOverlays) | ||||||
|         .filter((a) => a[1].visible) |         .flatMap((a) => | ||||||
|         .map(([pathToNodeString, overlay], index) => { |           a[1].map((b) => ({ pathToNodeString: a[0], overlay: b })) | ||||||
|  |         ) | ||||||
|  |         .filter((a) => a.overlay.visible) | ||||||
|  |         .map(({ pathToNodeString, overlay }, index) => { | ||||||
|           return ( |           return ( | ||||||
|             <Overlay |             <Overlay | ||||||
|               overlay={overlay} |               overlay={overlay} | ||||||
|               key={pathToNodeString} |               key={pathToNodeString + String(index)} | ||||||
|               pathToNodeString={pathToNodeString} |               pathToNodeString={pathToNodeString} | ||||||
|               overlayIndex={index} |               overlayIndex={index} | ||||||
|             /> |             /> | ||||||
| @ -237,11 +233,17 @@ const Overlay = ({ | |||||||
|  |  | ||||||
|   const constraints = |   const constraints = | ||||||
|     callExpression.type === 'CallExpression' |     callExpression.type === 'CallExpression' | ||||||
|       ? getConstraintInfo(callExpression, codeManager.code, overlay.pathToNode) |       ? getConstraintInfo( | ||||||
|  |           callExpression, | ||||||
|  |           codeManager.code, | ||||||
|  |           overlay.pathToNode, | ||||||
|  |           overlay.filterValue | ||||||
|  |         ) | ||||||
|       : getConstraintInfoKw( |       : getConstraintInfoKw( | ||||||
|           callExpression, |           callExpression, | ||||||
|           codeManager.code, |           codeManager.code, | ||||||
|           overlay.pathToNode |           overlay.pathToNode, | ||||||
|  |           overlay.filterValue | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|   const offset = 20 // px |   const offset = 20 // px | ||||||
| @ -261,7 +263,6 @@ const Overlay = ({ | |||||||
|       state.matches({ Sketch: 'Tangential arc to' }) || |       state.matches({ Sketch: 'Tangential arc to' }) || | ||||||
|       state.matches({ Sketch: 'Rectangle tool' }) |       state.matches({ Sketch: 'Rectangle tool' }) | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <div className={`absolute w-0 h-0`}> |     <div className={`absolute w-0 h-0`}> | ||||||
|       <div |       <div | ||||||
| @ -319,7 +320,8 @@ const Overlay = ({ | |||||||
|           this will likely change soon when we implement multi-profile so we'll leave it for now |           this will likely change soon when we implement multi-profile so we'll leave it for now | ||||||
|           issue: https://github.com/KittyCAD/modeling-app/issues/3910 |           issue: https://github.com/KittyCAD/modeling-app/issues/3910 | ||||||
|           */} |           */} | ||||||
|           {callExpression?.callee?.name !== 'circle' && ( |           {callExpression?.callee?.name !== 'circle' && | ||||||
|  |             callExpression?.callee?.name !== 'circleThreePoint' && ( | ||||||
|               <SegmentMenu |               <SegmentMenu | ||||||
|                 verticalPosition={ |                 verticalPosition={ | ||||||
|                   overlay.windowCoords[1] > window.innerHeight / 2 |                   overlay.windowCoords[1] > window.innerHeight / 2 | ||||||
| @ -450,6 +452,8 @@ export async function deleteSegment({ | |||||||
|   if (!sketchDetails) return |   if (!sketchDetails) return | ||||||
|   await sceneEntitiesManager.updateAstAndRejigSketch( |   await sceneEntitiesManager.updateAstAndRejigSketch( | ||||||
|     pathToNode, |     pathToNode, | ||||||
|  |     sketchDetails.sketchNodePaths, | ||||||
|  |     sketchDetails.planeNodePath, | ||||||
|     modifiedAst, |     modifiedAst, | ||||||
|     sketchDetails.zAxis, |     sketchDetails.zAxis, | ||||||
|     sketchDetails.yAxis, |     sketchDetails.yAxis, | ||||||
|  | |||||||
| @ -182,13 +182,15 @@ export class SceneInfra { | |||||||
|   callbacks: (() => SegmentOverlayPayload | null)[] = [] |   callbacks: (() => SegmentOverlayPayload | null)[] = [] | ||||||
|   _overlayCallbacks(callbacks: (() => SegmentOverlayPayload | null)[]) { |   _overlayCallbacks(callbacks: (() => SegmentOverlayPayload | null)[]) { | ||||||
|     const segmentOverlayPayload: SegmentOverlayPayload = { |     const segmentOverlayPayload: SegmentOverlayPayload = { | ||||||
|       type: 'set-many', |       type: 'add-many', | ||||||
|       overlays: {}, |       overlays: {}, | ||||||
|     } |     } | ||||||
|     callbacks.forEach((cb) => { |     callbacks.forEach((cb) => { | ||||||
|       const overlay = cb() |       const overlay = cb() | ||||||
|       if (overlay?.type === 'set-one') { |       if (overlay?.type === 'set-one') { | ||||||
|         segmentOverlayPayload.overlays[overlay.pathToNodeString] = overlay.seg |         segmentOverlayPayload.overlays[overlay.pathToNodeString] = overlay.seg | ||||||
|  |       } else if (overlay?.type === 'add-many') { | ||||||
|  |         Object.assign(segmentOverlayPayload.overlays, overlay.overlays) | ||||||
|       } |       } | ||||||
|     }) |     }) | ||||||
|     this.modelingSend({ |     this.modelingSend({ | ||||||
| @ -213,25 +215,27 @@ export class SceneInfra { | |||||||
|  |  | ||||||
|   overlayThrottleMap: { [pathToNodeString: string]: number } = {} |   overlayThrottleMap: { [pathToNodeString: string]: number } = {} | ||||||
|   updateOverlayDetails({ |   updateOverlayDetails({ | ||||||
|     arrowGroup, |     handle, | ||||||
|     group, |     group, | ||||||
|     isHandlesVisible, |     isHandlesVisible, | ||||||
|     from, |     from, | ||||||
|     to, |     to, | ||||||
|     angle, |     angle, | ||||||
|  |     hasThreeDotMenu, | ||||||
|   }: { |   }: { | ||||||
|     arrowGroup: Group |     handle: Group | ||||||
|     group: Group |     group: Group | ||||||
|     isHandlesVisible: boolean |     isHandlesVisible: boolean | ||||||
|     from: Coords2d |     from: Coords2d | ||||||
|     to: Coords2d |     to: Coords2d | ||||||
|  |     hasThreeDotMenu: boolean | ||||||
|     angle?: number |     angle?: number | ||||||
|   }): SegmentOverlayPayload | null { |   }): SegmentOverlayPayload | null { | ||||||
|     if (!group.userData.draft && group.userData.pathToNode && arrowGroup) { |     if (!group.userData.draft && group.userData.pathToNode && handle) { | ||||||
|       const vector = new Vector3(0, 0, 0) |       const vector = new Vector3(0, 0, 0) | ||||||
|  |  | ||||||
|       // Get the position of the object3D in world space |       // Get the position of the object3D in world space | ||||||
|       arrowGroup.getWorldPosition(vector) |       handle.getWorldPosition(vector) | ||||||
|  |  | ||||||
|       // Project that position to screen space |       // Project that position to screen space | ||||||
|       vector.project(this.camControls.camera) |       vector.project(this.camControls.camera) | ||||||
| @ -244,13 +248,16 @@ export class SceneInfra { | |||||||
|       return { |       return { | ||||||
|         type: 'set-one', |         type: 'set-one', | ||||||
|         pathToNodeString, |         pathToNodeString, | ||||||
|         seg: { |         seg: [ | ||||||
|  |           { | ||||||
|             windowCoords: [x, y], |             windowCoords: [x, y], | ||||||
|             angle: _angle, |             angle: _angle, | ||||||
|             group, |             group, | ||||||
|             pathToNode: group.userData.pathToNode, |             pathToNode: group.userData.pathToNode, | ||||||
|             visible: isHandlesVisible, |             visible: isHandlesVisible, | ||||||
|  |             hasThreeDotMenu, | ||||||
|           }, |           }, | ||||||
|  |         ], | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|     return null |     return null | ||||||
|  | |||||||
| @ -31,6 +31,12 @@ import { | |||||||
|   CIRCLE_SEGMENT, |   CIRCLE_SEGMENT, | ||||||
|   CIRCLE_SEGMENT_BODY, |   CIRCLE_SEGMENT_BODY, | ||||||
|   CIRCLE_SEGMENT_DASH, |   CIRCLE_SEGMENT_DASH, | ||||||
|  |   CIRCLE_THREE_POINT_HANDLE1, | ||||||
|  |   CIRCLE_THREE_POINT_HANDLE2, | ||||||
|  |   CIRCLE_THREE_POINT_HANDLE3, | ||||||
|  |   CIRCLE_THREE_POINT_SEGMENT, | ||||||
|  |   CIRCLE_THREE_POINT_SEGMENT_BODY, | ||||||
|  |   CIRCLE_THREE_POINT_SEGMENT_DASH, | ||||||
|   EXTRA_SEGMENT_HANDLE, |   EXTRA_SEGMENT_HANDLE, | ||||||
|   EXTRA_SEGMENT_OFFSET_PX, |   EXTRA_SEGMENT_OFFSET_PX, | ||||||
|   HIDE_HOVER_SEGMENT_LENGTH, |   HIDE_HOVER_SEGMENT_LENGTH, | ||||||
| @ -48,19 +54,26 @@ import { | |||||||
| import { getTangentPointFromPreviousArc } from 'lib/utils2d' | import { getTangentPointFromPreviousArc } from 'lib/utils2d' | ||||||
| import { | import { | ||||||
|   ARROWHEAD, |   ARROWHEAD, | ||||||
|  |   CIRCLE_3_POINT_DRAFT_CIRCLE, | ||||||
|   DRAFT_POINT, |   DRAFT_POINT, | ||||||
|   SceneInfra, |   SceneInfra, | ||||||
|   SEGMENT_LENGTH_LABEL, |   SEGMENT_LENGTH_LABEL, | ||||||
|   SEGMENT_LENGTH_LABEL_OFFSET_PX, |   SEGMENT_LENGTH_LABEL_OFFSET_PX, | ||||||
|   SEGMENT_LENGTH_LABEL_TEXT, |   SEGMENT_LENGTH_LABEL_TEXT, | ||||||
|  |   SKETCH_LAYER, | ||||||
| } from './sceneInfra' | } from './sceneInfra' | ||||||
| import { Themes, getThemeColorForThreeJs } from 'lib/theme' | import { Themes, getThemeColorForThreeJs } from 'lib/theme' | ||||||
| import { normaliseAngle, roundOff } from 'lib/utils' | import { normaliseAngle, roundOff } from 'lib/utils' | ||||||
| import { SegmentOverlayPayload } from 'machines/modelingMachine' | import { | ||||||
|  |   SegmentOverlay, | ||||||
|  |   SegmentOverlayPayload, | ||||||
|  |   SegmentOverlays, | ||||||
|  | } from 'machines/modelingMachine' | ||||||
| import { SegmentInputs } from 'lang/std/stdTypes' | import { SegmentInputs } from 'lang/std/stdTypes' | ||||||
| import { err } from 'lib/trap' | import { err } from 'lib/trap' | ||||||
| import { editorManager, sceneInfra } from 'lib/singletons' | import { editorManager, sceneInfra } from 'lib/singletons' | ||||||
| import { Selections } from 'lib/selections' | import { Selections } from 'lib/selections' | ||||||
|  | import { calculate_circle_from_3_points } from 'wasm-lib/pkg/wasm_lib' | ||||||
| import { commandBarActor } from 'machines/commandBarMachine' | import { commandBarActor } from 'machines/commandBarMachine' | ||||||
|  |  | ||||||
| interface CreateSegmentArgs { | interface CreateSegmentArgs { | ||||||
| @ -307,11 +320,12 @@ class StraightSegment implements SegmentUtils { | |||||||
|     } |     } | ||||||
|     return () => |     return () => | ||||||
|       sceneInfra.updateOverlayDetails({ |       sceneInfra.updateOverlayDetails({ | ||||||
|         arrowGroup, |         handle: arrowGroup, | ||||||
|         group, |         group, | ||||||
|         isHandlesVisible, |         isHandlesVisible, | ||||||
|         from, |         from, | ||||||
|         to, |         to, | ||||||
|  |         hasThreeDotMenu: true, | ||||||
|       }) |       }) | ||||||
|   } |   } | ||||||
| } | } | ||||||
| @ -483,12 +497,13 @@ class TangentialArcToSegment implements SegmentUtils { | |||||||
|     ) |     ) | ||||||
|     return () => |     return () => | ||||||
|       sceneInfra.updateOverlayDetails({ |       sceneInfra.updateOverlayDetails({ | ||||||
|         arrowGroup, |         handle: arrowGroup, | ||||||
|         group, |         group, | ||||||
|         isHandlesVisible, |         isHandlesVisible, | ||||||
|         from, |         from, | ||||||
|         to, |         to, | ||||||
|         angle, |         angle, | ||||||
|  |         hasThreeDotMenu: true, | ||||||
|       }) |       }) | ||||||
|   } |   } | ||||||
| } | } | ||||||
| @ -684,35 +699,255 @@ class CircleSegment implements SegmentUtils { | |||||||
|     } |     } | ||||||
|     return () => |     return () => | ||||||
|       sceneInfra.updateOverlayDetails({ |       sceneInfra.updateOverlayDetails({ | ||||||
|         arrowGroup, |         handle: arrowGroup, | ||||||
|         group, |         group, | ||||||
|         isHandlesVisible, |         isHandlesVisible, | ||||||
|         from: from, |         from: from, | ||||||
|         to: [center[0], center[1]], |         to: [center[0], center[1]], | ||||||
|         angle: Math.PI / 4, |         angle: Math.PI / 4, | ||||||
|  |         hasThreeDotMenu: true, | ||||||
|       }) |       }) | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | class CircleThreePointSegment implements SegmentUtils { | ||||||
|  |   init: SegmentUtils['init'] = ({ | ||||||
|  |     input, | ||||||
|  |     id, | ||||||
|  |     pathToNode, | ||||||
|  |     isDraftSegment, | ||||||
|  |     scale = 1, | ||||||
|  |     theme, | ||||||
|  |     isSelected = false, | ||||||
|  |     sceneInfra, | ||||||
|  |     prevSegment, | ||||||
|  |   }) => { | ||||||
|  |     if (input.type !== 'circle-three-point-segment') { | ||||||
|  |       return new Error('Invalid segment type') | ||||||
|  |     } | ||||||
|  |     const { p1, p2, p3 } = input | ||||||
|  |     const { center_x, center_y, radius } = calculate_circle_from_3_points( | ||||||
|  |       p1[0], | ||||||
|  |       p1[1], | ||||||
|  |       p2[0], | ||||||
|  |       p2[1], | ||||||
|  |       p3[0], | ||||||
|  |       p3[1] | ||||||
|  |     ) | ||||||
|  |     const center: [number, number] = [center_x, center_y] | ||||||
|  |     const baseColor = getThemeColorForThreeJs(theme) | ||||||
|  |     const color = isSelected ? 0x0000ff : baseColor | ||||||
|  |  | ||||||
|  |     const group = new Group() | ||||||
|  |     const geometry = createArcGeometry({ | ||||||
|  |       center, | ||||||
|  |       radius, | ||||||
|  |       startAngle: 0, | ||||||
|  |       endAngle: Math.PI * 2, | ||||||
|  |       ccw: true, | ||||||
|  |       isDashed: isDraftSegment, | ||||||
|  |       scale, | ||||||
|  |     }) | ||||||
|  |     const mat = new MeshBasicMaterial({ color }) | ||||||
|  |     const arcMesh = new Mesh(geometry, mat) | ||||||
|  |     const meshType = isDraftSegment | ||||||
|  |       ? CIRCLE_THREE_POINT_SEGMENT_DASH | ||||||
|  |       : CIRCLE_THREE_POINT_SEGMENT_BODY | ||||||
|  |     const handle1 = createCircleThreePointHandle( | ||||||
|  |       scale, | ||||||
|  |       theme, | ||||||
|  |       CIRCLE_THREE_POINT_HANDLE1, | ||||||
|  |       color | ||||||
|  |     ) | ||||||
|  |     const handle2 = createCircleThreePointHandle( | ||||||
|  |       scale, | ||||||
|  |       theme, | ||||||
|  |       CIRCLE_THREE_POINT_HANDLE2, | ||||||
|  |       color | ||||||
|  |     ) | ||||||
|  |     const handle3 = createCircleThreePointHandle( | ||||||
|  |       scale, | ||||||
|  |       theme, | ||||||
|  |       CIRCLE_THREE_POINT_HANDLE3, | ||||||
|  |       color | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     arcMesh.userData.type = meshType | ||||||
|  |     arcMesh.name = meshType | ||||||
|  |     group.userData = { | ||||||
|  |       type: CIRCLE_THREE_POINT_SEGMENT, | ||||||
|  |       draft: isDraftSegment, | ||||||
|  |       id, | ||||||
|  |       p1, | ||||||
|  |       p2, | ||||||
|  |       p3, | ||||||
|  |       ccw: true, | ||||||
|  |       prevSegment, | ||||||
|  |       pathToNode, | ||||||
|  |       isSelected, | ||||||
|  |       baseColor, | ||||||
|  |     } | ||||||
|  |     group.name = CIRCLE_THREE_POINT_SEGMENT | ||||||
|  |  | ||||||
|  |     group.add(arcMesh, handle1, handle2, handle3) | ||||||
|  |     const updateOverlaysCallback = this.update({ | ||||||
|  |       prevSegment, | ||||||
|  |       input, | ||||||
|  |       group, | ||||||
|  |       scale, | ||||||
|  |       sceneInfra, | ||||||
|  |     }) | ||||||
|  |     if (err(updateOverlaysCallback)) return updateOverlaysCallback | ||||||
|  |  | ||||||
|  |     return { | ||||||
|  |       group, | ||||||
|  |       updateOverlaysCallback, | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   update: SegmentUtils['update'] = ({ | ||||||
|  |     input, | ||||||
|  |     group, | ||||||
|  |     scale = 1, | ||||||
|  |     sceneInfra, | ||||||
|  |   }) => { | ||||||
|  |     if (input.type !== 'circle-three-point-segment') { | ||||||
|  |       return new Error('Invalid segment type') | ||||||
|  |     } | ||||||
|  |     const { p1, p2, p3 } = input | ||||||
|  |     group.userData.p1 = p1 | ||||||
|  |     group.userData.p2 = p2 | ||||||
|  |     group.userData.p3 = p3 | ||||||
|  |     const { center_x, center_y, radius } = calculate_circle_from_3_points( | ||||||
|  |       p1[0], | ||||||
|  |       p1[1], | ||||||
|  |       p2[0], | ||||||
|  |       p2[1], | ||||||
|  |       p3[0], | ||||||
|  |       p3[1] | ||||||
|  |     ) | ||||||
|  |     const center: [number, number] = [center_x, center_y] | ||||||
|  |     const points = [p1, p2, p3] | ||||||
|  |     const handles = [ | ||||||
|  |       CIRCLE_THREE_POINT_HANDLE1, | ||||||
|  |       CIRCLE_THREE_POINT_HANDLE2, | ||||||
|  |       CIRCLE_THREE_POINT_HANDLE3, | ||||||
|  |     ].map((handle) => group.getObjectByName(handle) as Group) | ||||||
|  |     handles.forEach((handle, i) => { | ||||||
|  |       const point = points[i] | ||||||
|  |       if (handle && point) { | ||||||
|  |         handle.position.set(point[0], point[1], 0) | ||||||
|  |         handle.scale.set(scale, scale, scale) | ||||||
|  |         handle.visible = true | ||||||
|  |       } | ||||||
|  |     }) | ||||||
|  |  | ||||||
|  |     const pxLength = (2 * radius * Math.PI) / scale | ||||||
|  |     const shouldHideIdle = pxLength < HIDE_SEGMENT_LENGTH | ||||||
|  |     const shouldHideHover = pxLength < HIDE_HOVER_SEGMENT_LENGTH | ||||||
|  |  | ||||||
|  |     const hoveredParent = | ||||||
|  |       sceneInfra.hoveredObject && | ||||||
|  |       getParentGroup(sceneInfra.hoveredObject, [CIRCLE_SEGMENT]) | ||||||
|  |     let isHandlesVisible = !shouldHideIdle | ||||||
|  |     if (hoveredParent && hoveredParent?.uuid === group?.uuid) { | ||||||
|  |       isHandlesVisible = !shouldHideHover | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const circleSegmentBody = group.children.find( | ||||||
|  |       (child) => child.userData.type === CIRCLE_THREE_POINT_SEGMENT_BODY | ||||||
|  |     ) as Mesh | ||||||
|  |  | ||||||
|  |     if (circleSegmentBody) { | ||||||
|  |       const newGeo = createArcGeometry({ | ||||||
|  |         radius, | ||||||
|  |         center, | ||||||
|  |         startAngle: 0, | ||||||
|  |         endAngle: Math.PI * 2, | ||||||
|  |         ccw: true, | ||||||
|  |         scale, | ||||||
|  |       }) | ||||||
|  |       circleSegmentBody.geometry = newGeo | ||||||
|  |     } | ||||||
|  |     const circleSegmentBodyDashed = group.getObjectByName( | ||||||
|  |       CIRCLE_THREE_POINT_SEGMENT_DASH | ||||||
|  |     ) | ||||||
|  |     if (circleSegmentBodyDashed instanceof Mesh) { | ||||||
|  |       // consider throttling the whole updateTangentialArcToSegment | ||||||
|  |       // if there are more perf considerations going forward | ||||||
|  |       circleSegmentBodyDashed.geometry = createArcGeometry({ | ||||||
|  |         center, | ||||||
|  |         radius, | ||||||
|  |         ccw: true, | ||||||
|  |         // make the start end where the handle is | ||||||
|  |         startAngle: Math.PI * 0.25, | ||||||
|  |         endAngle: Math.PI * 2.25, | ||||||
|  |         isDashed: true, | ||||||
|  |         scale, | ||||||
|  |       }) | ||||||
|  |     } | ||||||
|  |     return () => { | ||||||
|  |       const overlays: SegmentOverlays = {} | ||||||
|  |       const points = [p1, p2, p3] | ||||||
|  |       const overlayDetails = handles.map((handle, index) => { | ||||||
|  |         const currentPoint = points[index] | ||||||
|  |         const angle = Math.atan2( | ||||||
|  |           currentPoint[1] - center[1], | ||||||
|  |           currentPoint[0] - center[0] | ||||||
|  |         ) | ||||||
|  |         return sceneInfra.updateOverlayDetails({ | ||||||
|  |           handle, | ||||||
|  |           group, | ||||||
|  |           isHandlesVisible, | ||||||
|  |           from: [0, 0], | ||||||
|  |           to: [center[0], center[1]], | ||||||
|  |           angle: angle, | ||||||
|  |           hasThreeDotMenu: index === 0, | ||||||
|  |         }) | ||||||
|  |       }) | ||||||
|  |       const segmentOverlays: SegmentOverlay[] = [] | ||||||
|  |       overlayDetails.forEach((payload, index) => { | ||||||
|  |         if (payload?.type === 'set-one') { | ||||||
|  |           overlays[payload.pathToNodeString] = payload.seg | ||||||
|  |           segmentOverlays.push({ | ||||||
|  |             ...payload.seg[0], | ||||||
|  |             filterValue: index === 0 ? 'p1' : index === 1 ? 'p2' : 'p3', | ||||||
|  |           }) | ||||||
|  |         } | ||||||
|  |       }) | ||||||
|  |       const segmentOverlayPayload: SegmentOverlayPayload = { | ||||||
|  |         type: 'set-one', | ||||||
|  |         pathToNodeString: | ||||||
|  |           overlayDetails[0]?.type === 'set-one' | ||||||
|  |             ? overlayDetails[0].pathToNodeString | ||||||
|  |             : '', | ||||||
|  |         seg: segmentOverlays, | ||||||
|  |       } | ||||||
|  |       return segmentOverlayPayload | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
| export function createProfileStartHandle({ | export function createProfileStartHandle({ | ||||||
|   from, |   from, | ||||||
|   isDraft = false, |   isDraft = false, | ||||||
|   scale = 1, |   scale = 1, | ||||||
|   theme, |   theme, | ||||||
|   isSelected, |   isSelected, | ||||||
|  |   size = 12, | ||||||
|   ...rest |   ...rest | ||||||
| }: { | }: { | ||||||
|   from: Coords2d |   from: Coords2d | ||||||
|   scale?: number |   scale?: number | ||||||
|   theme: Themes |   theme: Themes | ||||||
|   isSelected?: boolean |   isSelected?: boolean | ||||||
|  |   size?: number | ||||||
| } & ( | } & ( | ||||||
|   | { isDraft: true } |   | { isDraft: true } | ||||||
|   | { isDraft: false; id: string; pathToNode: PathToNode } |   | { isDraft: false; id: string; pathToNode: PathToNode } | ||||||
| )) { | )) { | ||||||
|   const group = new Group() |   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 baseColor = getThemeColorForThreeJs(theme) | ||||||
|   const color = isSelected ? 0x0000ff : baseColor |   const color = isSelected ? 0x0000ff : baseColor | ||||||
|   const body = new MeshBasicMaterial({ color }) |   const body = new MeshBasicMaterial({ color }) | ||||||
| @ -774,6 +1009,32 @@ function createCircleCenterHandle( | |||||||
|   circleCenterGroup.scale.set(scale, scale, scale) |   circleCenterGroup.scale.set(scale, scale, scale) | ||||||
|   return circleCenterGroup |   return circleCenterGroup | ||||||
| } | } | ||||||
|  | function createCircleThreePointHandle( | ||||||
|  |   scale = 1, | ||||||
|  |   theme: Themes, | ||||||
|  |   name: | ||||||
|  |     | 'circle-three-point-handle1' | ||||||
|  |     | 'circle-three-point-handle2' | ||||||
|  |     | 'circle-three-point-handle3', | ||||||
|  |   color?: number | ||||||
|  | ): Group { | ||||||
|  |   const circleCenterGroup = new Group() | ||||||
|  |  | ||||||
|  |   const geometry = new BoxGeometry(12, 12, 12) // in pixels scaled later | ||||||
|  |   const baseColor = getThemeColorForThreeJs(theme) | ||||||
|  |   const body = new MeshBasicMaterial({ color }) | ||||||
|  |   const mesh = new Mesh(geometry, body) | ||||||
|  |  | ||||||
|  |   circleCenterGroup.add(mesh) | ||||||
|  |  | ||||||
|  |   circleCenterGroup.userData = { | ||||||
|  |     type: name, | ||||||
|  |     baseColor, | ||||||
|  |   } | ||||||
|  |   circleCenterGroup.name = name | ||||||
|  |   circleCenterGroup.scale.set(scale, scale, scale) | ||||||
|  |   return circleCenterGroup | ||||||
|  | } | ||||||
|  |  | ||||||
| function createExtraSegmentHandle( | function createExtraSegmentHandle( | ||||||
|   scale: number, |   scale: number, | ||||||
| @ -1100,4 +1361,5 @@ export const segmentUtils = { | |||||||
|   straight: new StraightSegment(), |   straight: new StraightSegment(), | ||||||
|   tangentialArcTo: new TangentialArcToSegment(), |   tangentialArcTo: new TangentialArcToSegment(), | ||||||
|   circle: new CircleSegment(), |   circle: new CircleSegment(), | ||||||
|  |   circleThreePoint: new CircleThreePointSegment(), | ||||||
| } as const | } as const | ||||||
|  | |||||||
| @ -329,7 +329,7 @@ export const FileMachineProvider = ({ | |||||||
|           onSubmit: async (data) => { |           onSubmit: async (data) => { | ||||||
|             if (data.method === 'overwrite') { |             if (data.method === 'overwrite') { | ||||||
|               codeManager.updateCodeStateEditor(data.code) |               codeManager.updateCodeStateEditor(data.code) | ||||||
|               await kclManager.executeCode(true) |               await kclManager.executeCode({ zoomToFit: true }) | ||||||
|               await codeManager.writeToFile() |               await codeManager.writeToFile() | ||||||
|             } else if (data.method === 'newFile' && isDesktop()) { |             } else if (data.method === 'newFile' && isDesktop()) { | ||||||
|               send({ |               send({ | ||||||
|  | |||||||
| @ -21,7 +21,6 @@ import { ContextMenu, ContextMenuItem } from './ContextMenu' | |||||||
| import usePlatform from 'hooks/usePlatform' | import usePlatform from 'hooks/usePlatform' | ||||||
| import { FileEntry } from 'lib/project' | import { FileEntry } from 'lib/project' | ||||||
| import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher' | import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher' | ||||||
| import { normalizeLineEndings } from 'lib/codeEditor' |  | ||||||
| import { reportRejection } from 'lib/trap' | import { reportRejection } from 'lib/trap' | ||||||
|  |  | ||||||
| function getIndentationCSS(level: number) { | function getIndentationCSS(level: number) { | ||||||
| @ -190,24 +189,25 @@ const FileTreeItem = ({ | |||||||
|   // Because subtrees only render when they are opened, that means this |   // Because subtrees only render when they are opened, that means this | ||||||
|   // only listens when they open. Because this acts like a useEffect, when |   // only listens when they open. Because this acts like a useEffect, when | ||||||
|   // the ReactNodes are destroyed, so is this listener :) |   // the ReactNodes are destroyed, so is this listener :) | ||||||
|   useFileSystemWatcher( |   /** Disabling this in favor of faster file writes until we fix file writing **/ | ||||||
|     async (eventType, path) => { |   /* useFileSystemWatcher( | ||||||
|       // Prevents a cyclic read / write causing editor problems such as |    *   async (eventType, path) => { | ||||||
|       // misplaced cursor positions. |    *     // Prevents a cyclic read / write causing editor problems such as | ||||||
|       if (codeManager.writeCausedByAppCheckedInFileTreeFileSystemWatcher) { |    *     // misplaced cursor positions. | ||||||
|         codeManager.writeCausedByAppCheckedInFileTreeFileSystemWatcher = false |    *     if (codeManager.writeCausedByAppCheckedInFileTreeFileSystemWatcher) { | ||||||
|         return |    *       codeManager.writeCausedByAppCheckedInFileTreeFileSystemWatcher = false | ||||||
|       } |    *       return | ||||||
|  |    *     } | ||||||
|  |  | ||||||
|       if (isCurrentFile && eventType === 'change') { |    *     if (isCurrentFile && eventType === 'change') { | ||||||
|         let code = await window.electron.readFile(path, { encoding: 'utf-8' }) |    *       let code = await window.electron.readFile(path, { encoding: 'utf-8' }) | ||||||
|         code = normalizeLineEndings(code) |    *       code = normalizeLineEndings(code) | ||||||
|         codeManager.updateCodeStateEditor(code) |    *       codeManager.updateCodeStateEditor(code) | ||||||
|       } |    *     } | ||||||
|       fileSend({ type: 'Refresh' }) |    *     fileSend({ type: 'Refresh' }) | ||||||
|     }, |    *   }, | ||||||
|     [fileOrDir.path] |    *   [fileOrDir.path] | ||||||
|   ) |    * ) */ | ||||||
|  |  | ||||||
|   const showNewTreeEntry = |   const showNewTreeEntry = | ||||||
|     newTreeEntry !== undefined && |     newTreeEntry !== undefined && | ||||||
| @ -263,7 +263,7 @@ const FileTreeItem = ({ | |||||||
|       await codeManager.writeToFile() |       await codeManager.writeToFile() | ||||||
|  |  | ||||||
|       // Prevent seeing the model built one piece at a time when changing files |       // Prevent seeing the model built one piece at a time when changing files | ||||||
|       await kclManager.executeCode(true) |       await kclManager.executeCode({ zoomToFit: true }) | ||||||
|     } else { |     } else { | ||||||
|       // Let the lsp servers know we closed a file. |       // Let the lsp servers know we closed a file. | ||||||
|       onFileClose(currentFile?.path || null, project?.path || null) |       onFileClose(currentFile?.path || null, project?.path || null) | ||||||
|  | |||||||
| @ -25,7 +25,7 @@ import { useSetupEngineManager } from 'hooks/useSetupEngineManager' | |||||||
| import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' | import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' | ||||||
| import { | import { | ||||||
|   isCursorInSketchCommandRange, |   isCursorInSketchCommandRange, | ||||||
|   updatePathToNodeFromMap, |   updateSketchDetailsNodePaths, | ||||||
| } from 'lang/util' | } from 'lang/util' | ||||||
| import { | import { | ||||||
|   kclManager, |   kclManager, | ||||||
| @ -65,17 +65,32 @@ import { | |||||||
|   replaceValueAtNodePath, |   replaceValueAtNodePath, | ||||||
|   sketchOnExtrudedFace, |   sketchOnExtrudedFace, | ||||||
|   sketchOnOffsetPlane, |   sketchOnOffsetPlane, | ||||||
|  |   splitPipedProfile, | ||||||
|   startSketchOnDefault, |   startSketchOnDefault, | ||||||
| } from 'lang/modifyAst' | } from 'lang/modifyAst' | ||||||
| import { PathToNode, Program, parse, recast, resultIsOk } from 'lang/wasm' | import { | ||||||
| import { artifactIsPlaneWithPaths, isSingleCursorInPipe } from 'lang/queryAst' |   CodeRef, | ||||||
|  |   PathToNode, | ||||||
|  |   Program, | ||||||
|  |   VariableDeclaration, | ||||||
|  |   parse, | ||||||
|  |   recast, | ||||||
|  |   resultIsOk, | ||||||
|  | } from 'lang/wasm' | ||||||
|  | import { | ||||||
|  |   artifactIsPlaneWithPaths, | ||||||
|  |   doesSketchPipeNeedSplitting, | ||||||
|  |   getNodeFromPath, | ||||||
|  |   isCursorInFunctionDefinition, | ||||||
|  |   traverse, | ||||||
|  | } from 'lang/queryAst' | ||||||
| import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils' | import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils' | ||||||
| import { exportFromEngine } from 'lib/exportFromEngine' | import { exportFromEngine } from 'lib/exportFromEngine' | ||||||
| import { Models } from '@kittycad/lib/dist/types/src' | import { Models } from '@kittycad/lib/dist/types/src' | ||||||
| import toast from 'react-hot-toast' | import toast from 'react-hot-toast' | ||||||
| import { useLoaderData, useNavigate, useSearchParams } from 'react-router-dom' | import { useLoaderData, useNavigate, useSearchParams } from 'react-router-dom' | ||||||
| import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls' | import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls' | ||||||
| import { err, reportRejection, trap } from 'lib/trap' | import { err, reportRejection, trap, reject } from 'lib/trap' | ||||||
| import { | import { | ||||||
|   ExportIntent, |   ExportIntent, | ||||||
|   EngineConnectionStateType, |   EngineConnectionStateType, | ||||||
| @ -86,6 +101,10 @@ import { useFileContext } from 'hooks/useFileContext' | |||||||
| import { uuidv4 } from 'lib/utils' | import { uuidv4 } from 'lib/utils' | ||||||
| import { IndexLoaderData } from 'lib/types' | import { IndexLoaderData } from 'lib/types' | ||||||
| import { Node } from 'wasm-lib/kcl/bindings/Node' | import { Node } from 'wasm-lib/kcl/bindings/Node' | ||||||
|  | import { | ||||||
|  |   getPathsFromArtifact, | ||||||
|  |   getPlaneFromArtifact, | ||||||
|  | } from 'lang/std/artifactGraph' | ||||||
| import { promptToEditFlow } from 'lib/promptToEdit' | import { promptToEditFlow } from 'lib/promptToEdit' | ||||||
| import { kclEditorActor } from 'machines/kclEditorMachine' | import { kclEditorActor } from 'machines/kclEditorMachine' | ||||||
| import { commandBarActor } from 'machines/commandBarMachine' | import { commandBarActor } from 'machines/commandBarMachine' | ||||||
| @ -163,10 +182,12 @@ export const ModelingMachineProvider = ({ | |||||||
|         'enable copilot': () => { |         'enable copilot': () => { | ||||||
|           editorManager.setCopilotEnabled(true) |           editorManager.setCopilotEnabled(true) | ||||||
|         }, |         }, | ||||||
|         'sketch exit execute': ({ context: { store } }) => { |         // tsc reports this typing as perfectly fine, but eslint is complaining. | ||||||
|           // TODO: Remove this async callback.  For some reason eslint wouldn't |         // It's actually nonsensical, so I'm quieting. | ||||||
|           // let me disable @typescript-eslint/no-misused-promises for the line. |         // eslint-disable-next-line @typescript-eslint/no-misused-promises | ||||||
|           ;(async () => { |         'sketch exit execute': async ({ | ||||||
|  |           context: { store }, | ||||||
|  |         }): Promise<void> => { | ||||||
|           // When cancelling the sketch mode we should disable sketch mode within the engine. |           // When cancelling the sketch mode we should disable sketch mode within the engine. | ||||||
|           await engineCommandManager.sendSceneCommand({ |           await engineCommandManager.sendSceneCommand({ | ||||||
|             type: 'modeling_cmd_req', |             type: 'modeling_cmd_req', | ||||||
| @ -184,6 +205,24 @@ export const ModelingMachineProvider = ({ | |||||||
|  |  | ||||||
|           store.videoElement?.pause() |           store.videoElement?.pause() | ||||||
|  |  | ||||||
|  |           return kclManager.executeCode().then(() => { | ||||||
|  |             if (engineCommandManager.engineConnection?.idleMode) return | ||||||
|  |  | ||||||
|  |             store.videoElement?.play().catch((e) => { | ||||||
|  |               console.warn('Video playing was prevented', e) | ||||||
|  |             }) | ||||||
|  |           }) | ||||||
|  |  | ||||||
|  |           sceneInfra.camControls.syncDirection = 'clientToEngine' | ||||||
|  |  | ||||||
|  |           if (cameraProjection.current === 'perspective') { | ||||||
|  |             await sceneInfra.camControls.snapToPerspectiveBeforeHandingBackControlToEngine() | ||||||
|  |           } | ||||||
|  |  | ||||||
|  |           sceneInfra.camControls.syncDirection = 'engineToClient' | ||||||
|  |  | ||||||
|  |           store.videoElement?.pause() | ||||||
|  |  | ||||||
|           return kclManager |           return kclManager | ||||||
|             .executeCode() |             .executeCode() | ||||||
|             .then(() => { |             .then(() => { | ||||||
| @ -194,7 +233,6 @@ export const ModelingMachineProvider = ({ | |||||||
|               }) |               }) | ||||||
|             }) |             }) | ||||||
|             .catch(reportRejection) |             .catch(reportRejection) | ||||||
|           })().catch(reportRejection) |  | ||||||
|         }, |         }, | ||||||
|         'Set mouse state': assign(({ context, event }) => { |         'Set mouse state': assign(({ context, event }) => { | ||||||
|           if (event.type !== 'Set mouse state') return {} |           if (event.type !== 'Set mouse state') return {} | ||||||
| @ -254,7 +292,11 @@ export const ModelingMachineProvider = ({ | |||||||
|         'Set Segment Overlays': assign({ |         'Set Segment Overlays': assign({ | ||||||
|           segmentOverlays: ({ context: { segmentOverlays }, event }) => { |           segmentOverlays: ({ context: { segmentOverlays }, event }) => { | ||||||
|             if (event.type !== 'Set Segment Overlays') return {} |             if (event.type !== 'Set Segment Overlays') return {} | ||||||
|             if (event.data.type === 'set-many') return event.data.overlays |             if (event.data.type === 'add-many') | ||||||
|  |               return { | ||||||
|  |                 ...segmentOverlays, | ||||||
|  |                 ...event.data.overlays, | ||||||
|  |               } | ||||||
|             if (event.data.type === 'set-one') |             if (event.data.type === 'set-one') | ||||||
|               return { |               return { | ||||||
|                 ...segmentOverlays, |                 ...segmentOverlays, | ||||||
| @ -287,7 +329,7 @@ export const ModelingMachineProvider = ({ | |||||||
|           return { |           return { | ||||||
|             sketchDetails: { |             sketchDetails: { | ||||||
|               ...sketchDetails, |               ...sketchDetails, | ||||||
|               sketchPathToNode: event.data, |               sketchEntryNodePath: event.data, | ||||||
|             }, |             }, | ||||||
|           } |           } | ||||||
|         }), |         }), | ||||||
| @ -411,9 +453,17 @@ export const ModelingMachineProvider = ({ | |||||||
|                 selectionRanges: setSelections.selection, |                 selectionRanges: setSelections.selection, | ||||||
|                 sketchDetails: { |                 sketchDetails: { | ||||||
|                   ...sketchDetails, |                   ...sketchDetails, | ||||||
|                   sketchPathToNode: |                   sketchEntryNodePath: | ||||||
|                     setSelections.updatedPathToNode || |                     setSelections.updatedSketchEntryNodePath || | ||||||
|                     sketchDetails?.sketchPathToNode || |                     sketchDetails?.sketchEntryNodePath || | ||||||
|  |                     [], | ||||||
|  |                   sketchNodePaths: | ||||||
|  |                     setSelections.updatedSketchNodePaths || | ||||||
|  |                     sketchDetails?.sketchNodePaths || | ||||||
|  |                     [], | ||||||
|  |                   planeNodePath: | ||||||
|  |                     setSelections.updatedPlaneNodePath || | ||||||
|  |                     sketchDetails?.planeNodePath || | ||||||
|                     [], |                     [], | ||||||
|                 }, |                 }, | ||||||
|               } |               } | ||||||
| @ -566,7 +616,12 @@ export const ModelingMachineProvider = ({ | |||||||
|           if (artifactIsPlaneWithPaths(selectionRanges)) { |           if (artifactIsPlaneWithPaths(selectionRanges)) { | ||||||
|             return true |             return true | ||||||
|           } |           } | ||||||
|           if (!isSingleCursorInPipe(selectionRanges, kclManager.ast)) |           if ( | ||||||
|  |             isCursorInFunctionDefinition( | ||||||
|  |               kclManager.ast, | ||||||
|  |               selectionRanges.graphSelections[0] | ||||||
|  |             ) | ||||||
|  |           ) | ||||||
|             return false |             return false | ||||||
|           return !!isCursorInSketchCommandRange( |           return !!isCursorInSketchCommandRange( | ||||||
|             engineCommandManager.artifactGraph, |             engineCommandManager.artifactGraph, | ||||||
| @ -597,10 +652,32 @@ export const ModelingMachineProvider = ({ | |||||||
|               // this assumes no changes have been made to the sketch besides what we did when entering the sketch |               // 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? |               // i.e. doesn't account for user's adding code themselves, maybe we need store a flag userEditedSinceSketchMode? | ||||||
|               const newAst = structuredClone(kclManager.ast) |               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 |               // remove body item at varDecIndex | ||||||
|               newAst.body = newAst.body.filter((_, i) => i !== varDecIndex) |               newAst.body = newAst.body.filter((_, i) => i !== varDecIndex) | ||||||
|               await kclManager.executeAstMock(newAst) |               await kclManager.executeAstMock(newAst) | ||||||
|  |               await codeManager.updateEditorWithAstAndWriteToFile(newAst) | ||||||
|             } |             } | ||||||
|             sceneInfra.setCallbacks({ |             sceneInfra.setCallbacks({ | ||||||
|               onClick: () => {}, |               onClick: () => {}, | ||||||
| @ -610,7 +687,7 @@ export const ModelingMachineProvider = ({ | |||||||
|           } |           } | ||||||
|         ), |         ), | ||||||
|         'animate-to-face': fromPromise(async ({ input }) => { |         'animate-to-face': fromPromise(async ({ input }) => { | ||||||
|           if (!input) return undefined |           if (!input) return null | ||||||
|           if (input.type === 'extrudeFace' || input.type === 'offsetPlane') { |           if (input.type === 'extrudeFace' || input.type === 'offsetPlane') { | ||||||
|             const sketched = |             const sketched = | ||||||
|               input.type === 'extrudeFace' |               input.type === 'extrudeFace' | ||||||
| @ -637,7 +714,9 @@ export const ModelingMachineProvider = ({ | |||||||
|             await letEngineAnimateAndSyncCamAfter(engineCommandManager, id) |             await letEngineAnimateAndSyncCamAfter(engineCommandManager, id) | ||||||
|             sceneInfra.camControls.syncDirection = 'clientToEngine' |             sceneInfra.camControls.syncDirection = 'clientToEngine' | ||||||
|             return { |             return { | ||||||
|               sketchPathToNode: pathToNewSketchNode, |               sketchEntryNodePath: [], | ||||||
|  |               planeNodePath: pathToNewSketchNode, | ||||||
|  |               sketchNodePaths: [], | ||||||
|               zAxis: input.zAxis, |               zAxis: input.zAxis, | ||||||
|               yAxis: input.yAxis, |               yAxis: input.yAxis, | ||||||
|               origin: input.position, |               origin: input.position, | ||||||
| @ -658,7 +737,9 @@ export const ModelingMachineProvider = ({ | |||||||
|           ) |           ) | ||||||
|  |  | ||||||
|           return { |           return { | ||||||
|             sketchPathToNode: pathToNode, |             sketchEntryNodePath: [], | ||||||
|  |             planeNodePath: pathToNode, | ||||||
|  |             sketchNodePaths: [], | ||||||
|             zAxis: input.zAxis, |             zAxis: input.zAxis, | ||||||
|             yAxis: input.yAxis, |             yAxis: input.yAxis, | ||||||
|             origin: [0, 0, 0], |             origin: [0, 0, 0], | ||||||
| @ -667,12 +748,14 @@ export const ModelingMachineProvider = ({ | |||||||
|         }), |         }), | ||||||
|         'animate-to-sketch': fromPromise( |         'animate-to-sketch': fromPromise( | ||||||
|           async ({ input: { selectionRanges } }) => { |           async ({ input: { selectionRanges } }) => { | ||||||
|             const sourceRange = |             const sketchPathToNode = | ||||||
|               selectionRanges.graphSelections[0]?.codeRef?.range |               selectionRanges.graphSelections[0]?.codeRef?.pathToNode | ||||||
|             const sketchPathToNode = getNodePathFromSourceRange( |             const plane = getPlaneFromArtifact( | ||||||
|               kclManager.ast, |               selectionRanges.graphSelections[0].artifact, | ||||||
|               sourceRange |               engineCommandManager.artifactGraph | ||||||
|             ) |             ) | ||||||
|  |             if (err(plane)) return Promise.reject(plane) | ||||||
|  |  | ||||||
|             const info = await getSketchOrientationDetails( |             const info = await getSketchOrientationDetails( | ||||||
|               sketchPathToNode || [] |               sketchPathToNode || [] | ||||||
|             ) |             ) | ||||||
| @ -680,8 +763,22 @@ export const ModelingMachineProvider = ({ | |||||||
|               engineCommandManager, |               engineCommandManager, | ||||||
|               info?.sketchDetails?.faceId || '' |               info?.sketchDetails?.faceId || '' | ||||||
|             ) |             ) | ||||||
|             return { |             const sketchPaths = getPathsFromArtifact({ | ||||||
|  |               artifact: selectionRanges.graphSelections[0].artifact, | ||||||
|               sketchPathToNode: sketchPathToNode || [], |               sketchPathToNode: sketchPathToNode || [], | ||||||
|  |             }) | ||||||
|  |             if (err(sketchPaths)) return Promise.reject(sketchPaths) | ||||||
|  |             let codeRef = | ||||||
|  |               'faceCodeRef' in plane && plane.faceCodeRef | ||||||
|  |                 ? plane.faceCodeRef | ||||||
|  |                 : 'codeRef' in plane && plane.codeRef | ||||||
|  |                 ? plane.codeRef | ||||||
|  |                 : null | ||||||
|  |             if (!codeRef) return Promise.reject(new Error('No plane codeRef')) | ||||||
|  |             return { | ||||||
|  |               sketchEntryNodePath: sketchPathToNode || [], | ||||||
|  |               sketchNodePaths: sketchPaths, | ||||||
|  |               planeNodePath: codeRef.pathToNode, | ||||||
|               zAxis: info.sketchDetails.zAxis || null, |               zAxis: info.sketchDetails.zAxis || null, | ||||||
|               yAxis: info.sketchDetails.yAxis || null, |               yAxis: info.sketchDetails.yAxis || null, | ||||||
|               origin: info.sketchDetails.origin.map( |               origin: info.sketchDetails.origin.map( | ||||||
| @ -694,7 +791,7 @@ export const ModelingMachineProvider = ({ | |||||||
|  |  | ||||||
|         'Get horizontal info': fromPromise( |         'Get horizontal info': fromPromise( | ||||||
|           async ({ input: { selectionRanges, sketchDetails } }) => { |           async ({ input: { selectionRanges, sketchDetails } }) => { | ||||||
|             const { modifiedAst, pathToNodeMap } = |             const { modifiedAst, pathToNodeMap, exprInsertIndex } = | ||||||
|               await applyConstraintHorzVertDistance({ |               await applyConstraintHorzVertDistance({ | ||||||
|                 constraint: 'setHorzDistance', |                 constraint: 'setHorzDistance', | ||||||
|                 selectionRanges, |                 selectionRanges, | ||||||
| @ -706,13 +803,23 @@ export const ModelingMachineProvider = ({ | |||||||
|  |  | ||||||
|             if (!sketchDetails) |             if (!sketchDetails) | ||||||
|               return Promise.reject(new Error('No sketch details')) |               return Promise.reject(new Error('No sketch details')) | ||||||
|             const updatedPathToNode = updatePathToNodeFromMap( |  | ||||||
|               sketchDetails.sketchPathToNode, |             const { | ||||||
|               pathToNodeMap |               updatedSketchEntryNodePath, | ||||||
|             ) |               updatedSketchNodePaths, | ||||||
|  |               updatedPlaneNodePath, | ||||||
|  |             } = updateSketchDetailsNodePaths({ | ||||||
|  |               sketchEntryNodePath: sketchDetails.sketchEntryNodePath, | ||||||
|  |               sketchNodePaths: sketchDetails.sketchNodePaths, | ||||||
|  |               planeNodePath: sketchDetails.planeNodePath, | ||||||
|  |               exprInsertIndex, | ||||||
|  |             }) | ||||||
|  |  | ||||||
|             const updatedAst = |             const updatedAst = | ||||||
|               await sceneEntitiesManager.updateAstAndRejigSketch( |               await sceneEntitiesManager.updateAstAndRejigSketch( | ||||||
|                 updatedPathToNode, |                 updatedSketchEntryNodePath, | ||||||
|  |                 updatedSketchNodePaths, | ||||||
|  |                 updatedPlaneNodePath, | ||||||
|                 _modifiedAst, |                 _modifiedAst, | ||||||
|                 sketchDetails.zAxis, |                 sketchDetails.zAxis, | ||||||
|                 sketchDetails.yAxis, |                 sketchDetails.yAxis, | ||||||
| @ -733,13 +840,15 @@ export const ModelingMachineProvider = ({ | |||||||
|             return { |             return { | ||||||
|               selectionType: 'completeSelection', |               selectionType: 'completeSelection', | ||||||
|               selection, |               selection, | ||||||
|               updatedPathToNode, |               updatedSketchEntryNodePath, | ||||||
|  |               updatedSketchNodePaths, | ||||||
|  |               updatedPlaneNodePath, | ||||||
|             } |             } | ||||||
|           } |           } | ||||||
|         ), |         ), | ||||||
|         'Get vertical info': fromPromise( |         'Get vertical info': fromPromise( | ||||||
|           async ({ input: { selectionRanges, sketchDetails } }) => { |           async ({ input: { selectionRanges, sketchDetails } }) => { | ||||||
|             const { modifiedAst, pathToNodeMap } = |             const { modifiedAst, pathToNodeMap, exprInsertIndex } = | ||||||
|               await applyConstraintHorzVertDistance({ |               await applyConstraintHorzVertDistance({ | ||||||
|                 constraint: 'setVertDistance', |                 constraint: 'setVertDistance', | ||||||
|                 selectionRanges, |                 selectionRanges, | ||||||
| @ -750,13 +859,23 @@ export const ModelingMachineProvider = ({ | |||||||
|             const _modifiedAst = pResult.program |             const _modifiedAst = pResult.program | ||||||
|             if (!sketchDetails) |             if (!sketchDetails) | ||||||
|               return Promise.reject(new Error('No sketch details')) |               return Promise.reject(new Error('No sketch details')) | ||||||
|             const updatedPathToNode = updatePathToNodeFromMap( |  | ||||||
|               sketchDetails.sketchPathToNode, |             const { | ||||||
|               pathToNodeMap |               updatedSketchEntryNodePath, | ||||||
|             ) |               updatedSketchNodePaths, | ||||||
|  |               updatedPlaneNodePath, | ||||||
|  |             } = updateSketchDetailsNodePaths({ | ||||||
|  |               sketchEntryNodePath: sketchDetails.sketchEntryNodePath, | ||||||
|  |               sketchNodePaths: sketchDetails.sketchNodePaths, | ||||||
|  |               planeNodePath: sketchDetails.planeNodePath, | ||||||
|  |               exprInsertIndex, | ||||||
|  |             }) | ||||||
|  |  | ||||||
|             const updatedAst = |             const updatedAst = | ||||||
|               await sceneEntitiesManager.updateAstAndRejigSketch( |               await sceneEntitiesManager.updateAstAndRejigSketch( | ||||||
|                 updatedPathToNode, |                 updatedSketchEntryNodePath, | ||||||
|  |                 updatedSketchNodePaths, | ||||||
|  |                 updatedPlaneNodePath, | ||||||
|                 _modifiedAst, |                 _modifiedAst, | ||||||
|                 sketchDetails.zAxis, |                 sketchDetails.zAxis, | ||||||
|                 sketchDetails.yAxis, |                 sketchDetails.yAxis, | ||||||
| @ -777,7 +896,9 @@ export const ModelingMachineProvider = ({ | |||||||
|             return { |             return { | ||||||
|               selectionType: 'completeSelection', |               selectionType: 'completeSelection', | ||||||
|               selection, |               selection, | ||||||
|               updatedPathToNode, |               updatedSketchEntryNodePath, | ||||||
|  |               updatedSketchNodePaths, | ||||||
|  |               updatedPlaneNodePath, | ||||||
|             } |             } | ||||||
|           } |           } | ||||||
|         ), |         ), | ||||||
| @ -787,7 +908,8 @@ export const ModelingMachineProvider = ({ | |||||||
|               selectionRanges, |               selectionRanges, | ||||||
|             }) |             }) | ||||||
|             if (err(info)) return Promise.reject(info) |             if (err(info)) return Promise.reject(info) | ||||||
|             const { modifiedAst, pathToNodeMap } = await (info.enabled |             const { modifiedAst, pathToNodeMap, exprInsertIndex } = | ||||||
|  |               await (info.enabled | ||||||
|                 ? applyConstraintAngleBetween({ |                 ? applyConstraintAngleBetween({ | ||||||
|                     selectionRanges, |                     selectionRanges, | ||||||
|                   }) |                   }) | ||||||
| @ -803,13 +925,23 @@ export const ModelingMachineProvider = ({ | |||||||
|  |  | ||||||
|             if (!sketchDetails) |             if (!sketchDetails) | ||||||
|               return Promise.reject(new Error('No sketch details')) |               return Promise.reject(new Error('No sketch details')) | ||||||
|             const updatedPathToNode = updatePathToNodeFromMap( |  | ||||||
|               sketchDetails.sketchPathToNode, |             const { | ||||||
|               pathToNodeMap |               updatedSketchEntryNodePath, | ||||||
|             ) |               updatedSketchNodePaths, | ||||||
|  |               updatedPlaneNodePath, | ||||||
|  |             } = updateSketchDetailsNodePaths({ | ||||||
|  |               sketchEntryNodePath: sketchDetails.sketchEntryNodePath, | ||||||
|  |               sketchNodePaths: sketchDetails.sketchNodePaths, | ||||||
|  |               planeNodePath: sketchDetails.planeNodePath, | ||||||
|  |               exprInsertIndex, | ||||||
|  |             }) | ||||||
|  |  | ||||||
|             const updatedAst = |             const updatedAst = | ||||||
|               await sceneEntitiesManager.updateAstAndRejigSketch( |               await sceneEntitiesManager.updateAstAndRejigSketch( | ||||||
|                 updatedPathToNode, |                 updatedSketchEntryNodePath, | ||||||
|  |                 updatedSketchNodePaths, | ||||||
|  |                 updatedPlaneNodePath, | ||||||
|                 _modifiedAst, |                 _modifiedAst, | ||||||
|                 sketchDetails.zAxis, |                 sketchDetails.zAxis, | ||||||
|                 sketchDetails.yAxis, |                 sketchDetails.yAxis, | ||||||
| @ -830,7 +962,9 @@ export const ModelingMachineProvider = ({ | |||||||
|             return { |             return { | ||||||
|               selectionType: 'completeSelection', |               selectionType: 'completeSelection', | ||||||
|               selection, |               selection, | ||||||
|               updatedPathToNode, |               updatedSketchEntryNodePath, | ||||||
|  |               updatedSketchNodePaths, | ||||||
|  |               updatedPlaneNodePath, | ||||||
|             } |             } | ||||||
|           } |           } | ||||||
|         ), |         ), | ||||||
| @ -845,20 +979,30 @@ export const ModelingMachineProvider = ({ | |||||||
|               length: lengthValue, |               length: lengthValue, | ||||||
|             }) |             }) | ||||||
|             if (err(constraintResult)) return Promise.reject(constraintResult) |             if (err(constraintResult)) return Promise.reject(constraintResult) | ||||||
|             const { modifiedAst, pathToNodeMap } = constraintResult |             const { modifiedAst, pathToNodeMap, exprInsertIndex } = | ||||||
|  |               constraintResult | ||||||
|             const pResult = parse(recast(modifiedAst)) |             const pResult = parse(recast(modifiedAst)) | ||||||
|             if (trap(pResult) || !resultIsOk(pResult)) |             if (trap(pResult) || !resultIsOk(pResult)) | ||||||
|               return Promise.reject(new Error('Unexpected compilation error')) |               return Promise.reject(new Error('Unexpected compilation error')) | ||||||
|             const _modifiedAst = pResult.program |             const _modifiedAst = pResult.program | ||||||
|             if (!sketchDetails) |             if (!sketchDetails) | ||||||
|               return Promise.reject(new Error('No sketch details')) |               return Promise.reject(new Error('No sketch details')) | ||||||
|             const updatedPathToNode = updatePathToNodeFromMap( |  | ||||||
|               sketchDetails.sketchPathToNode, |             const { | ||||||
|               pathToNodeMap |               updatedSketchEntryNodePath, | ||||||
|             ) |               updatedSketchNodePaths, | ||||||
|  |               updatedPlaneNodePath, | ||||||
|  |             } = updateSketchDetailsNodePaths({ | ||||||
|  |               sketchEntryNodePath: sketchDetails.sketchEntryNodePath, | ||||||
|  |               sketchNodePaths: sketchDetails.sketchNodePaths, | ||||||
|  |               planeNodePath: sketchDetails.planeNodePath, | ||||||
|  |               exprInsertIndex, | ||||||
|  |             }) | ||||||
|             const updatedAst = |             const updatedAst = | ||||||
|               await sceneEntitiesManager.updateAstAndRejigSketch( |               await sceneEntitiesManager.updateAstAndRejigSketch( | ||||||
|                 updatedPathToNode, |                 updatedSketchEntryNodePath, | ||||||
|  |                 updatedSketchNodePaths, | ||||||
|  |                 updatedPlaneNodePath, | ||||||
|                 _modifiedAst, |                 _modifiedAst, | ||||||
|                 sketchDetails.zAxis, |                 sketchDetails.zAxis, | ||||||
|                 sketchDetails.yAxis, |                 sketchDetails.yAxis, | ||||||
| @ -879,13 +1023,15 @@ export const ModelingMachineProvider = ({ | |||||||
|             return { |             return { | ||||||
|               selectionType: 'completeSelection', |               selectionType: 'completeSelection', | ||||||
|               selection, |               selection, | ||||||
|               updatedPathToNode, |               updatedSketchEntryNodePath, | ||||||
|  |               updatedSketchNodePaths, | ||||||
|  |               updatedPlaneNodePath, | ||||||
|             } |             } | ||||||
|           } |           } | ||||||
|         ), |         ), | ||||||
|         'Get perpendicular distance info': fromPromise( |         'Get perpendicular distance info': fromPromise( | ||||||
|           async ({ input: { selectionRanges, sketchDetails } }) => { |           async ({ input: { selectionRanges, sketchDetails } }) => { | ||||||
|             const { modifiedAst, pathToNodeMap } = |             const { modifiedAst, pathToNodeMap, exprInsertIndex } = | ||||||
|               await applyConstraintIntersect({ |               await applyConstraintIntersect({ | ||||||
|                 selectionRanges, |                 selectionRanges, | ||||||
|               }) |               }) | ||||||
| @ -895,13 +1041,22 @@ export const ModelingMachineProvider = ({ | |||||||
|             const _modifiedAst = pResult.program |             const _modifiedAst = pResult.program | ||||||
|             if (!sketchDetails) |             if (!sketchDetails) | ||||||
|               return Promise.reject(new Error('No sketch details')) |               return Promise.reject(new Error('No sketch details')) | ||||||
|             const updatedPathToNode = updatePathToNodeFromMap( |  | ||||||
|               sketchDetails.sketchPathToNode, |             const { | ||||||
|               pathToNodeMap |               updatedSketchEntryNodePath, | ||||||
|             ) |               updatedSketchNodePaths, | ||||||
|  |               updatedPlaneNodePath, | ||||||
|  |             } = updateSketchDetailsNodePaths({ | ||||||
|  |               sketchEntryNodePath: sketchDetails.sketchEntryNodePath, | ||||||
|  |               sketchNodePaths: sketchDetails.sketchNodePaths, | ||||||
|  |               planeNodePath: sketchDetails.planeNodePath, | ||||||
|  |               exprInsertIndex, | ||||||
|  |             }) | ||||||
|             const updatedAst = |             const updatedAst = | ||||||
|               await sceneEntitiesManager.updateAstAndRejigSketch( |               await sceneEntitiesManager.updateAstAndRejigSketch( | ||||||
|                 updatedPathToNode, |                 updatedSketchEntryNodePath, | ||||||
|  |                 updatedSketchNodePaths, | ||||||
|  |                 updatedPlaneNodePath, | ||||||
|                 _modifiedAst, |                 _modifiedAst, | ||||||
|                 sketchDetails.zAxis, |                 sketchDetails.zAxis, | ||||||
|                 sketchDetails.yAxis, |                 sketchDetails.yAxis, | ||||||
| @ -922,13 +1077,15 @@ export const ModelingMachineProvider = ({ | |||||||
|             return { |             return { | ||||||
|               selectionType: 'completeSelection', |               selectionType: 'completeSelection', | ||||||
|               selection, |               selection, | ||||||
|               updatedPathToNode, |               updatedSketchEntryNodePath, | ||||||
|  |               updatedSketchNodePaths, | ||||||
|  |               updatedPlaneNodePath, | ||||||
|             } |             } | ||||||
|           } |           } | ||||||
|         ), |         ), | ||||||
|         'Get ABS X info': fromPromise( |         'Get ABS X info': fromPromise( | ||||||
|           async ({ input: { selectionRanges, sketchDetails } }) => { |           async ({ input: { selectionRanges, sketchDetails } }) => { | ||||||
|             const { modifiedAst, pathToNodeMap } = |             const { modifiedAst, pathToNodeMap, exprInsertIndex } = | ||||||
|               await applyConstraintAbsDistance({ |               await applyConstraintAbsDistance({ | ||||||
|                 constraint: 'xAbs', |                 constraint: 'xAbs', | ||||||
|                 selectionRanges, |                 selectionRanges, | ||||||
| @ -939,13 +1096,22 @@ export const ModelingMachineProvider = ({ | |||||||
|             const _modifiedAst = pResult.program |             const _modifiedAst = pResult.program | ||||||
|             if (!sketchDetails) |             if (!sketchDetails) | ||||||
|               return Promise.reject(new Error('No sketch details')) |               return Promise.reject(new Error('No sketch details')) | ||||||
|             const updatedPathToNode = updatePathToNodeFromMap( |  | ||||||
|               sketchDetails.sketchPathToNode, |             const { | ||||||
|               pathToNodeMap |               updatedSketchEntryNodePath, | ||||||
|             ) |               updatedSketchNodePaths, | ||||||
|  |               updatedPlaneNodePath, | ||||||
|  |             } = updateSketchDetailsNodePaths({ | ||||||
|  |               sketchEntryNodePath: sketchDetails.sketchEntryNodePath, | ||||||
|  |               sketchNodePaths: sketchDetails.sketchNodePaths, | ||||||
|  |               planeNodePath: sketchDetails.planeNodePath, | ||||||
|  |               exprInsertIndex, | ||||||
|  |             }) | ||||||
|             const updatedAst = |             const updatedAst = | ||||||
|               await sceneEntitiesManager.updateAstAndRejigSketch( |               await sceneEntitiesManager.updateAstAndRejigSketch( | ||||||
|                 updatedPathToNode, |                 updatedSketchEntryNodePath, | ||||||
|  |                 updatedSketchNodePaths, | ||||||
|  |                 updatedPlaneNodePath, | ||||||
|                 _modifiedAst, |                 _modifiedAst, | ||||||
|                 sketchDetails.zAxis, |                 sketchDetails.zAxis, | ||||||
|                 sketchDetails.yAxis, |                 sketchDetails.yAxis, | ||||||
| @ -966,13 +1132,15 @@ export const ModelingMachineProvider = ({ | |||||||
|             return { |             return { | ||||||
|               selectionType: 'completeSelection', |               selectionType: 'completeSelection', | ||||||
|               selection, |               selection, | ||||||
|               updatedPathToNode, |               updatedSketchEntryNodePath, | ||||||
|  |               updatedSketchNodePaths, | ||||||
|  |               updatedPlaneNodePath, | ||||||
|             } |             } | ||||||
|           } |           } | ||||||
|         ), |         ), | ||||||
|         'Get ABS Y info': fromPromise( |         'Get ABS Y info': fromPromise( | ||||||
|           async ({ input: { selectionRanges, sketchDetails } }) => { |           async ({ input: { selectionRanges, sketchDetails } }) => { | ||||||
|             const { modifiedAst, pathToNodeMap } = |             const { modifiedAst, pathToNodeMap, exprInsertIndex } = | ||||||
|               await applyConstraintAbsDistance({ |               await applyConstraintAbsDistance({ | ||||||
|                 constraint: 'yAbs', |                 constraint: 'yAbs', | ||||||
|                 selectionRanges, |                 selectionRanges, | ||||||
| @ -983,13 +1151,22 @@ export const ModelingMachineProvider = ({ | |||||||
|             const _modifiedAst = pResult.program |             const _modifiedAst = pResult.program | ||||||
|             if (!sketchDetails) |             if (!sketchDetails) | ||||||
|               return Promise.reject(new Error('No sketch details')) |               return Promise.reject(new Error('No sketch details')) | ||||||
|             const updatedPathToNode = updatePathToNodeFromMap( |  | ||||||
|               sketchDetails.sketchPathToNode, |             const { | ||||||
|               pathToNodeMap |               updatedSketchEntryNodePath, | ||||||
|             ) |               updatedSketchNodePaths, | ||||||
|  |               updatedPlaneNodePath, | ||||||
|  |             } = updateSketchDetailsNodePaths({ | ||||||
|  |               sketchEntryNodePath: sketchDetails.sketchEntryNodePath, | ||||||
|  |               sketchNodePaths: sketchDetails.sketchNodePaths, | ||||||
|  |               planeNodePath: sketchDetails.planeNodePath, | ||||||
|  |               exprInsertIndex, | ||||||
|  |             }) | ||||||
|             const updatedAst = |             const updatedAst = | ||||||
|               await sceneEntitiesManager.updateAstAndRejigSketch( |               await sceneEntitiesManager.updateAstAndRejigSketch( | ||||||
|                 updatedPathToNode, |                 updatedSketchEntryNodePath, | ||||||
|  |                 updatedSketchNodePaths, | ||||||
|  |                 updatedPlaneNodePath, | ||||||
|                 _modifiedAst, |                 _modifiedAst, | ||||||
|                 sketchDetails.zAxis, |                 sketchDetails.zAxis, | ||||||
|                 sketchDetails.yAxis, |                 sketchDetails.yAxis, | ||||||
| @ -1010,7 +1187,9 @@ export const ModelingMachineProvider = ({ | |||||||
|             return { |             return { | ||||||
|               selectionType: 'completeSelection', |               selectionType: 'completeSelection', | ||||||
|               selection, |               selection, | ||||||
|               updatedPathToNode, |               updatedSketchEntryNodePath, | ||||||
|  |               updatedSketchNodePaths, | ||||||
|  |               updatedPlaneNodePath, | ||||||
|             } |             } | ||||||
|           } |           } | ||||||
|         ), |         ), | ||||||
| @ -1030,9 +1209,11 @@ export const ModelingMachineProvider = ({ | |||||||
|             let result: { |             let result: { | ||||||
|               modifiedAst: Node<Program> |               modifiedAst: Node<Program> | ||||||
|               pathToReplaced: PathToNode | null |               pathToReplaced: PathToNode | null | ||||||
|  |               exprInsertIndex: number | ||||||
|             } = { |             } = { | ||||||
|               modifiedAst: parsed, |               modifiedAst: parsed, | ||||||
|               pathToReplaced: null, |               pathToReplaced: null, | ||||||
|  |               exprInsertIndex: -1, | ||||||
|             } |             } | ||||||
|             // If the user provided a constant name, |             // If the user provided a constant name, | ||||||
|             // we need to insert the named constant |             // we need to insert the named constant | ||||||
| @ -1062,6 +1243,7 @@ export const ModelingMachineProvider = ({ | |||||||
|               result = { |               result = { | ||||||
|                 modifiedAst: parseResultAfterInsertion.program, |                 modifiedAst: parseResultAfterInsertion.program, | ||||||
|                 pathToReplaced: astAfterReplacement.pathToReplaced, |                 pathToReplaced: astAfterReplacement.pathToReplaced, | ||||||
|  |                 exprInsertIndex: astAfterReplacement.exprInsertIndex, | ||||||
|               } |               } | ||||||
|             } else if ('valueText' in data.namedValue) { |             } else if ('valueText' in data.namedValue) { | ||||||
|               // If they didn't provide a constant name, |               // If they didn't provide a constant name, | ||||||
| @ -1092,10 +1274,22 @@ export const ModelingMachineProvider = ({ | |||||||
|             parsed = parsed as Node<Program> |             parsed = parsed as Node<Program> | ||||||
|             if (!result.pathToReplaced) |             if (!result.pathToReplaced) | ||||||
|               return Promise.reject(new Error('No path to replaced node')) |               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 = |             const updatedAst = | ||||||
|               await sceneEntitiesManager.updateAstAndRejigSketch( |               await sceneEntitiesManager.updateAstAndRejigSketch( | ||||||
|                 result.pathToReplaced || [], |                 updatedSketchEntryNodePath, | ||||||
|  |                 updatedSketchNodePaths, | ||||||
|  |                 updatedPlaneNodePath, | ||||||
|                 parsed, |                 parsed, | ||||||
|                 sketchDetails.zAxis, |                 sketchDetails.zAxis, | ||||||
|                 sketchDetails.yAxis, |                 sketchDetails.yAxis, | ||||||
| @ -1116,7 +1310,168 @@ export const ModelingMachineProvider = ({ | |||||||
|             return { |             return { | ||||||
|               selectionType: 'completeSelection', |               selectionType: 'completeSelection', | ||||||
|               selection, |               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') | ||||||
|  |             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-circle-three-point': fromPromise( | ||||||
|  |           async ({ input: { sketchDetails, data } }) => { | ||||||
|  |             if (!sketchDetails || !data) | ||||||
|  |               return reject('No sketch details or data') | ||||||
|  |             sceneEntitiesManager.tearDownSketch({ removeAxis: false }) | ||||||
|  |  | ||||||
|  |             const result = | ||||||
|  |               await sceneEntitiesManager.setupDraftCircleThreePoint( | ||||||
|  |                 sketchDetails.sketchEntryNodePath, | ||||||
|  |                 sketchDetails.sketchNodePaths, | ||||||
|  |                 sketchDetails.planeNodePath, | ||||||
|  |                 sketchDetails.zAxis, | ||||||
|  |                 sketchDetails.yAxis, | ||||||
|  |                 sketchDetails.origin, | ||||||
|  |                 data.p1, | ||||||
|  |                 data.p2 | ||||||
|  |               ) | ||||||
|  |             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') | ||||||
|  |             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') | ||||||
|  |             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, | ||||||
|  |               // We will want to pass sketchTools here | ||||||
|  |               // to add their interactions | ||||||
|  |             }) | ||||||
|  |  | ||||||
|  |             // We will want to update the context with sketchTools. | ||||||
|  |             // They'll be used for their .destroy() in tearDownSketch | ||||||
|  |             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, | ||||||
|             } |             } | ||||||
|           } |           } | ||||||
|         ), |         ), | ||||||
|  | |||||||
| @ -187,7 +187,7 @@ export const SettingsAuthProviderBase = ({ | |||||||
|             ) { |             ) { | ||||||
|               // Unit changes requires a re-exec of code |               // Unit changes requires a re-exec of code | ||||||
|               // eslint-disable-next-line @typescript-eslint/no-floating-promises |               // eslint-disable-next-line @typescript-eslint/no-floating-promises | ||||||
|               kclManager.executeCode(true) |               kclManager.executeCode({ zoomToFit: true }) | ||||||
|             } else { |             } else { | ||||||
|               // For any future logging we'd like to do |               // For any future logging we'd like to do | ||||||
|               // console.log( |               // console.log( | ||||||
|  | |||||||
| @ -2,7 +2,12 @@ import { SVGProps } from 'react' | |||||||
|  |  | ||||||
| export const Spinner = (props: SVGProps<SVGSVGElement>) => { | export const Spinner = (props: SVGProps<SVGSVGElement>) => { | ||||||
|   return ( |   return ( | ||||||
|     <svg viewBox="0 0 10 10" className={'w-8 h-8'} {...props}> |     <svg | ||||||
|  |       data-testid="spinner" | ||||||
|  |       viewBox="0 0 10 10" | ||||||
|  |       className={'w-8 h-8'} | ||||||
|  |       {...props} | ||||||
|  |     > | ||||||
|       <circle |       <circle | ||||||
|         cx="5" |         cx="5" | ||||||
|         cy="5" |         cy="5" | ||||||
|  | |||||||
| @ -60,7 +60,7 @@ export const Stream = () => { | |||||||
|    */ |    */ | ||||||
|   function executeCodeAndPlayStream() { |   function executeCodeAndPlayStream() { | ||||||
|     // eslint-disable-next-line @typescript-eslint/no-floating-promises |     // eslint-disable-next-line @typescript-eslint/no-floating-promises | ||||||
|     kclManager.executeCode(true).then(async () => { |     kclManager.executeCode({ zoomToFit: true }).then(async () => { | ||||||
|       await videoRef.current?.play().catch((e) => { |       await videoRef.current?.play().catch((e) => { | ||||||
|         console.warn('Video playing was prevented', e, videoRef.current) |         console.warn('Video playing was prevented', e, videoRef.current) | ||||||
|       }) |       }) | ||||||
|  | |||||||
| @ -136,6 +136,7 @@ export async function applyConstraintIntersect({ | |||||||
| }): Promise<{ | }): Promise<{ | ||||||
|   modifiedAst: Node<Program> |   modifiedAst: Node<Program> | ||||||
|   pathToNodeMap: PathToNodeMap |   pathToNodeMap: PathToNodeMap | ||||||
|  |   exprInsertIndex: number | ||||||
| }> { | }> { | ||||||
|   const info = intersectInfo({ |   const info = intersectInfo({ | ||||||
|     selectionRanges, |     selectionRanges, | ||||||
| @ -174,6 +175,7 @@ export async function applyConstraintIntersect({ | |||||||
|     return { |     return { | ||||||
|       modifiedAst, |       modifiedAst, | ||||||
|       pathToNodeMap, |       pathToNodeMap, | ||||||
|  |       exprInsertIndex: -1, | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   // transform again but forcing certain values |   // transform again but forcing certain values | ||||||
| @ -192,6 +194,7 @@ export async function applyConstraintIntersect({ | |||||||
|   const { modifiedAst: _modifiedAst, pathToNodeMap: _pathToNodeMap } = |   const { modifiedAst: _modifiedAst, pathToNodeMap: _pathToNodeMap } = | ||||||
|     transform2 |     transform2 | ||||||
|  |  | ||||||
|  |   let exprInsertIndex = -1 | ||||||
|   if (variableName) { |   if (variableName) { | ||||||
|     const newBody = [..._modifiedAst.body] |     const newBody = [..._modifiedAst.body] | ||||||
|     newBody.splice( |     newBody.splice( | ||||||
| @ -204,9 +207,11 @@ export async function applyConstraintIntersect({ | |||||||
|       const index = pathToNode.findIndex((a) => a[0] === 'body') + 1 |       const index = pathToNode.findIndex((a) => a[0] === 'body') + 1 | ||||||
|       pathToNode[index][0] = Number(pathToNode[index][0]) + 1 |       pathToNode[index][0] = Number(pathToNode[index][0]) + 1 | ||||||
|     }) |     }) | ||||||
|  |     exprInsertIndex = newVariableInsertIndex | ||||||
|   } |   } | ||||||
|   return { |   return { | ||||||
|     modifiedAst: _modifiedAst, |     modifiedAst: _modifiedAst, | ||||||
|     pathToNodeMap: _pathToNodeMap, |     pathToNodeMap: _pathToNodeMap, | ||||||
|  |     exprInsertIndex, | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -28,7 +28,7 @@ export function removeConstrainingValuesInfo({ | |||||||
|   | Error { |   | Error { | ||||||
|   const _nodes = selectionRanges.graphSelections.map(({ codeRef }) => { |   const _nodes = selectionRanges.graphSelections.map(({ codeRef }) => { | ||||||
|     const tmp = getNodeFromPath<Expr>(kclManager.ast, codeRef.pathToNode) |     const tmp = getNodeFromPath<Expr>(kclManager.ast, codeRef.pathToNode) | ||||||
|     if (err(tmp)) return tmp |     if (tmp instanceof Error) return tmp | ||||||
|     return tmp.node |     return tmp.node | ||||||
|   }) |   }) | ||||||
|   const _err1 = _nodes.find(err) |   const _err1 = _nodes.find(err) | ||||||
|  | |||||||
| @ -92,6 +92,7 @@ export async function applyConstraintAbsDistance({ | |||||||
| }): Promise<{ | }): Promise<{ | ||||||
|   modifiedAst: Program |   modifiedAst: Program | ||||||
|   pathToNodeMap: PathToNodeMap |   pathToNodeMap: PathToNodeMap | ||||||
|  |   exprInsertIndex: number | ||||||
| }> { | }> { | ||||||
|   const info = absDistanceInfo({ |   const info = absDistanceInfo({ | ||||||
|     selectionRanges, |     selectionRanges, | ||||||
| @ -131,6 +132,7 @@ export async function applyConstraintAbsDistance({ | |||||||
|   if (err(transform2)) return Promise.reject(transform2) |   if (err(transform2)) return Promise.reject(transform2) | ||||||
|   const { modifiedAst: _modifiedAst, pathToNodeMap } = transform2 |   const { modifiedAst: _modifiedAst, pathToNodeMap } = transform2 | ||||||
|  |  | ||||||
|  |   let exprInsertIndex = -1 | ||||||
|   if (variableName) { |   if (variableName) { | ||||||
|     const newBody = [..._modifiedAst.body] |     const newBody = [..._modifiedAst.body] | ||||||
|     newBody.splice( |     newBody.splice( | ||||||
| @ -143,8 +145,9 @@ export async function applyConstraintAbsDistance({ | |||||||
|       const index = pathToNode.findIndex((a) => a[0] === 'body') + 1 |       const index = pathToNode.findIndex((a) => a[0] === 'body') + 1 | ||||||
|       pathToNode[index][0] = Number(pathToNode[index][0]) + 1 |       pathToNode[index][0] = Number(pathToNode[index][0]) + 1 | ||||||
|     }) |     }) | ||||||
|  |     exprInsertIndex = newVariableInsertIndex | ||||||
|   } |   } | ||||||
|   return { modifiedAst: _modifiedAst, pathToNodeMap } |   return { modifiedAst: _modifiedAst, pathToNodeMap, exprInsertIndex } | ||||||
| } | } | ||||||
|  |  | ||||||
| export function applyConstraintAxisAlign({ | export function applyConstraintAxisAlign({ | ||||||
|  | |||||||
| @ -86,6 +86,7 @@ export async function applyConstraintAngleBetween({ | |||||||
| }): Promise<{ | }): Promise<{ | ||||||
|   modifiedAst: Program |   modifiedAst: Program | ||||||
|   pathToNodeMap: PathToNodeMap |   pathToNodeMap: PathToNodeMap | ||||||
|  |   exprInsertIndex: number | ||||||
| }> { | }> { | ||||||
|   const info = angleBetweenInfo({ selectionRanges }) |   const info = angleBetweenInfo({ selectionRanges }) | ||||||
|   if (err(info)) return Promise.reject(info) |   if (err(info)) return Promise.reject(info) | ||||||
| @ -122,6 +123,7 @@ export async function applyConstraintAngleBetween({ | |||||||
|     return { |     return { | ||||||
|       modifiedAst, |       modifiedAst, | ||||||
|       pathToNodeMap, |       pathToNodeMap, | ||||||
|  |       exprInsertIndex: -1, | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @ -141,6 +143,7 @@ export async function applyConstraintAngleBetween({ | |||||||
|   const { modifiedAst: _modifiedAst, pathToNodeMap: _pathToNodeMap } = |   const { modifiedAst: _modifiedAst, pathToNodeMap: _pathToNodeMap } = | ||||||
|     transformed2 |     transformed2 | ||||||
|  |  | ||||||
|  |   let exprInsertIndex = -1 | ||||||
|   if (variableName) { |   if (variableName) { | ||||||
|     const newBody = [..._modifiedAst.body] |     const newBody = [..._modifiedAst.body] | ||||||
|     newBody.splice( |     newBody.splice( | ||||||
| @ -153,9 +156,11 @@ export async function applyConstraintAngleBetween({ | |||||||
|       const index = pathToNode.findIndex((a) => a[0] === 'body') + 1 |       const index = pathToNode.findIndex((a) => a[0] === 'body') + 1 | ||||||
|       pathToNode[index][0] = Number(pathToNode[index][0]) + 1 |       pathToNode[index][0] = Number(pathToNode[index][0]) + 1 | ||||||
|     }) |     }) | ||||||
|  |     exprInsertIndex = newVariableInsertIndex | ||||||
|   } |   } | ||||||
|   return { |   return { | ||||||
|     modifiedAst: _modifiedAst, |     modifiedAst: _modifiedAst, | ||||||
|     pathToNodeMap: _pathToNodeMap, |     pathToNodeMap: _pathToNodeMap, | ||||||
|  |     exprInsertIndex, | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -87,15 +87,13 @@ export function horzVertDistanceInfo({ | |||||||
| export async function applyConstraintHorzVertDistance({ | export async function applyConstraintHorzVertDistance({ | ||||||
|   selectionRanges, |   selectionRanges, | ||||||
|   constraint, |   constraint, | ||||||
|   // TODO align will always be false (covered by synconous applyConstraintHorzVertAlign), remove it |  | ||||||
|   isAlign = false, |  | ||||||
| }: { | }: { | ||||||
|   selectionRanges: Selections |   selectionRanges: Selections | ||||||
|   constraint: 'setHorzDistance' | 'setVertDistance' |   constraint: 'setHorzDistance' | 'setVertDistance' | ||||||
|   isAlign?: false |  | ||||||
| }): Promise<{ | }): Promise<{ | ||||||
|   modifiedAst: Program |   modifiedAst: Program | ||||||
|   pathToNodeMap: PathToNodeMap |   pathToNodeMap: PathToNodeMap | ||||||
|  |   exprInsertIndex: number | ||||||
| }> { | }> { | ||||||
|   const info = horzVertDistanceInfo({ |   const info = horzVertDistanceInfo({ | ||||||
|     selectionRanges: selectionRanges, |     selectionRanges: selectionRanges, | ||||||
| @ -133,13 +131,12 @@ export async function applyConstraintHorzVertDistance({ | |||||||
|     return { |     return { | ||||||
|       modifiedAst, |       modifiedAst, | ||||||
|       pathToNodeMap, |       pathToNodeMap, | ||||||
|  |       exprInsertIndex: -1, | ||||||
|     } |     } | ||||||
|   } else { |   } else { | ||||||
|     if (!isExprBinaryPart(valueNode)) |     if (!isExprBinaryPart(valueNode)) | ||||||
|       return Promise.reject('Invalid valueNode, is not a BinaryPart') |       return Promise.reject('Invalid valueNode, is not a BinaryPart') | ||||||
|     let finalValue = isAlign |     let finalValue = removeDoubleNegatives(valueNode, sign, variableName) | ||||||
|       ? createLiteral(0) |  | ||||||
|       : removeDoubleNegatives(valueNode, sign, variableName) |  | ||||||
|     // transform again but forcing certain values |     // transform again but forcing certain values | ||||||
|     const transformed = transformSecondarySketchLinesTagFirst({ |     const transformed = transformSecondarySketchLinesTagFirst({ | ||||||
|       ast: kclManager.ast, |       ast: kclManager.ast, | ||||||
| @ -152,6 +149,7 @@ export async function applyConstraintHorzVertDistance({ | |||||||
|  |  | ||||||
|     if (err(transformed)) return Promise.reject(transformed) |     if (err(transformed)) return Promise.reject(transformed) | ||||||
|     const { modifiedAst: _modifiedAst, pathToNodeMap } = transformed |     const { modifiedAst: _modifiedAst, pathToNodeMap } = transformed | ||||||
|  |     let exprInsertIndex = -1 | ||||||
|     if (variableName) { |     if (variableName) { | ||||||
|       const newBody = [..._modifiedAst.body] |       const newBody = [..._modifiedAst.body] | ||||||
|       newBody.splice( |       newBody.splice( | ||||||
| @ -164,10 +162,12 @@ export async function applyConstraintHorzVertDistance({ | |||||||
|         const index = pathToNode.findIndex((a) => a[0] === 'body') + 1 |         const index = pathToNode.findIndex((a) => a[0] === 'body') + 1 | ||||||
|         pathToNode[index][0] = Number(pathToNode[index][0]) + 1 |         pathToNode[index][0] = Number(pathToNode[index][0]) + 1 | ||||||
|       }) |       }) | ||||||
|  |       exprInsertIndex = newVariableInsertIndex | ||||||
|     } |     } | ||||||
|     return { |     return { | ||||||
|       modifiedAst: _modifiedAst, |       modifiedAst: _modifiedAst, | ||||||
|       pathToNodeMap, |       pathToNodeMap, | ||||||
|  |       exprInsertIndex, | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -74,10 +74,14 @@ export async function applyConstraintLength({ | |||||||
| }: { | }: { | ||||||
|   length: KclCommandValue |   length: KclCommandValue | ||||||
|   selectionRanges: Selections |   selectionRanges: Selections | ||||||
| }) { | }): Promise<{ | ||||||
|  |   modifiedAst: Program | ||||||
|  |   pathToNodeMap: PathToNodeMap | ||||||
|  |   exprInsertIndex: number | ||||||
|  | }> { | ||||||
|   const ast = kclManager.ast |   const ast = kclManager.ast | ||||||
|   const angleLength = angleLengthInfo({ selectionRanges }) |   const angleLength = angleLengthInfo({ selectionRanges }) | ||||||
|   if (err(angleLength)) return angleLength |   if (err(angleLength)) return Promise.reject(angleLength) | ||||||
|   const { transforms } = angleLength |   const { transforms } = angleLength | ||||||
|  |  | ||||||
|   let distanceExpression: Expr = length.valueAst |   let distanceExpression: Expr = length.valueAst | ||||||
| @ -98,7 +102,7 @@ export async function applyConstraintLength({ | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   if (!isExprBinaryPart(distanceExpression)) { |   if (!isExprBinaryPart(distanceExpression)) { | ||||||
|     return new Error('Invalid valueNode, is not a BinaryPart') |     return Promise.reject('Invalid valueNode, is not a BinaryPart') | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   const retval = transformAstSketchLines({ |   const retval = transformAstSketchLines({ | ||||||
| @ -116,6 +120,12 @@ export async function applyConstraintLength({ | |||||||
|   return { |   return { | ||||||
|     modifiedAst: _modifiedAst, |     modifiedAst: _modifiedAst, | ||||||
|     pathToNodeMap, |     pathToNodeMap, | ||||||
|  |     exprInsertIndex: | ||||||
|  |       'variableName' in length && | ||||||
|  |       length.variableName && | ||||||
|  |       length.insertIndex !== undefined | ||||||
|  |         ? length.insertIndex | ||||||
|  |         : -1, | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| @ -128,6 +138,7 @@ export async function applyConstraintAngleLength({ | |||||||
| }): Promise<{ | }): Promise<{ | ||||||
|   modifiedAst: Program |   modifiedAst: Program | ||||||
|   pathToNodeMap: PathToNodeMap |   pathToNodeMap: PathToNodeMap | ||||||
|  |   exprInsertIndex: number | ||||||
| }> { | }> { | ||||||
|   const angleLength = angleLengthInfo({ selectionRanges, angleOrLength }) |   const angleLength = angleLengthInfo({ selectionRanges, angleOrLength }) | ||||||
|   if (err(angleLength)) return Promise.reject(angleLength) |   if (err(angleLength)) return Promise.reject(angleLength) | ||||||
| @ -212,5 +223,6 @@ export async function applyConstraintAngleLength({ | |||||||
|   return { |   return { | ||||||
|     modifiedAst: _modifiedAst, |     modifiedAst: _modifiedAst, | ||||||
|     pathToNodeMap, |     pathToNodeMap, | ||||||
|  |     exprInsertIndex: variableName ? newVariableInsertIndex : -1, | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -39,6 +39,7 @@ import { Operation } from 'wasm-lib/kcl/bindings/Operation' | |||||||
| interface ExecuteArgs { | interface ExecuteArgs { | ||||||
|   ast?: Node<Program> |   ast?: Node<Program> | ||||||
|   zoomToFit?: boolean |   zoomToFit?: boolean | ||||||
|  |   isPartialExecution?: boolean | ||||||
|   executionId?: number |   executionId?: number | ||||||
|   zoomOnRangeAndType?: { |   zoomOnRangeAndType?: { | ||||||
|     range: SourceRange |     range: SourceRange | ||||||
| @ -379,12 +380,10 @@ export class KclManager { | |||||||
|     } |     } | ||||||
|     this.ast = { ...ast } |     this.ast = { ...ast } | ||||||
|     // updateArtifactGraph relies on updated executeState/programMemory |     // updateArtifactGraph relies on updated executeState/programMemory | ||||||
|     this.engineCommandManager.updateArtifactGraph(execState.artifactGraph) |     await this.engineCommandManager.updateArtifactGraph(execState.artifactGraph) | ||||||
|     this._executeCallback() |     this._executeCallback() | ||||||
|     if (!isInterrupted) { |     if (!isInterrupted) | ||||||
|       sceneInfra.modelingSend({ type: 'code edit during sketch' }) |       sceneInfra.modelingSend({ type: 'code edit during sketch' }) | ||||||
|     } |  | ||||||
|  |  | ||||||
|     this.engineCommandManager.addCommandLog({ |     this.engineCommandManager.addCommandLog({ | ||||||
|       type: 'execution-done', |       type: 'execution-done', | ||||||
|       data: null, |       data: null, | ||||||
| @ -444,6 +443,7 @@ export class KclManager { | |||||||
|  |  | ||||||
|     this._logs = logs |     this._logs = logs | ||||||
|     this.addDiagnostics(kclErrorsToDiagnostics(errors)) |     this.addDiagnostics(kclErrorsToDiagnostics(errors)) | ||||||
|  |  | ||||||
|     this._execState = execState |     this._execState = execState | ||||||
|     this._programMemory = execState.memory |     this._programMemory = execState.memory | ||||||
|     if (!errors.length) { |     if (!errors.length) { | ||||||
| @ -484,7 +484,10 @@ export class KclManager { | |||||||
|       this._cancelTokens.set(key, true) |       this._cancelTokens.set(key, true) | ||||||
|     }) |     }) | ||||||
|   } |   } | ||||||
|   async executeCode(zoomToFit?: boolean): Promise<void> { |   async executeCode(opts?: { | ||||||
|  |     zoomToFit?: true | ||||||
|  |     isPartialExecution?: true | ||||||
|  |   }): Promise<void> { | ||||||
|     const ast = await this.safeParse(codeManager.code) |     const ast = await this.safeParse(codeManager.code) | ||||||
|  |  | ||||||
|     if (!ast) { |     if (!ast) { | ||||||
| @ -492,10 +495,10 @@ export class KclManager { | |||||||
|       return |       return | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     zoomToFit = this.tryToZoomToFitOnCodeUpdate(ast, zoomToFit) |     // zoomToFit = this.tryToZoomToFitOnCodeUpdate(ast, opts?.zoomToFit) | ||||||
|  |  | ||||||
|     this.ast = { ...ast } |     this.ast = { ...ast } | ||||||
|     return this.executeAst({ zoomToFit }) |     return this.executeAst(opts) | ||||||
|   } |   } | ||||||
|   /** |   /** | ||||||
|    * This will override the zoom to fit to zoom into the model if the previous AST was empty. |    * This will override the zoom to fit to zoom into the model if the previous AST was empty. | ||||||
|  | |||||||
| @ -157,7 +157,7 @@ export default class CodeManager { | |||||||
|               toast.error('Error saving file, please check file permissions') |               toast.error('Error saving file, please check file permissions') | ||||||
|               reject(err) |               reject(err) | ||||||
|             }) |             }) | ||||||
|         }, 1000) |         }, 10) | ||||||
|       }) |       }) | ||||||
|     } else { |     } else { | ||||||
|       safeLSSetItem(PERSIST_CODE_KEY, this.code) |       safeLSSetItem(PERSIST_CODE_KEY, this.code) | ||||||
|  | |||||||
| @ -27,6 +27,7 @@ export type ToolTip = | |||||||
|   | 'angledLineThatIntersects' |   | 'angledLineThatIntersects' | ||||||
|   | 'tangentialArcTo' |   | 'tangentialArcTo' | ||||||
|   | 'circle' |   | 'circle' | ||||||
|  |   | 'circleThreePoint' | ||||||
|  |  | ||||||
| export const toolTips: Array<ToolTip> = [ | export const toolTips: Array<ToolTip> = [ | ||||||
|   'line', |   'line', | ||||||
| @ -42,6 +43,7 @@ export const toolTips: Array<ToolTip> = [ | |||||||
|   'yLineTo', |   'yLineTo', | ||||||
|   'angledLineThatIntersects', |   'angledLineThatIntersects', | ||||||
|   'tangentialArcTo', |   'tangentialArcTo', | ||||||
|  |   'circleThreePoint', | ||||||
| ] | ] | ||||||
|  |  | ||||||
| export async function executeAst({ | export async function executeAst({ | ||||||
| @ -69,7 +71,6 @@ export async function executeAst({ | |||||||
|       : executor(ast, engineCommandManager, path)) |       : executor(ast, engineCommandManager, path)) | ||||||
|  |  | ||||||
|     await engineCommandManager.waitForAllCommands() |     await engineCommandManager.waitForAllCommands() | ||||||
|  |  | ||||||
|     return { |     return { | ||||||
|       logs: [], |       logs: [], | ||||||
|       errors: [], |       errors: [], | ||||||
|  | |||||||
| @ -25,6 +25,7 @@ import { | |||||||
|   deleteSegmentFromPipeExpression, |   deleteSegmentFromPipeExpression, | ||||||
|   removeSingleConstraintInfo, |   removeSingleConstraintInfo, | ||||||
|   deleteFromSelection, |   deleteFromSelection, | ||||||
|  |   splitPipedProfile, | ||||||
| } from './modifyAst' | } from './modifyAst' | ||||||
| import { enginelessExecutor } from '../lib/testHelpers' | import { enginelessExecutor } from '../lib/testHelpers' | ||||||
| import { findUsesOfTagInPipe } from './queryAst' | import { findUsesOfTagInPipe } from './queryAst' | ||||||
| @ -931,3 +932,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, number] = [ | ||||||
|  |       codeBefore.indexOf(codeOfInterest), | ||||||
|  |       codeBefore.indexOf(codeOfInterest) + codeOfInterest.length, | ||||||
|  |       0, | ||||||
|  |     ] | ||||||
|  |     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, number] = [ | ||||||
|  |       codeBefore.indexOf(codeOfInterest), | ||||||
|  |       codeBefore.indexOf(codeOfInterest) + codeOfInterest.length, | ||||||
|  |       0, | ||||||
|  |     ] | ||||||
|  |     const pathToPipe = getNodePathFromSourceRange(ast, range) | ||||||
|  |  | ||||||
|  |     const result = splitPipedProfile(ast, pathToPipe) | ||||||
|  |     expect(result instanceof Error).toBe(true) | ||||||
|  |   }) | ||||||
|  | }) | ||||||
|  | |||||||
| @ -22,6 +22,7 @@ import { | |||||||
|   SourceRange, |   SourceRange, | ||||||
|   sketchFromKclValue, |   sketchFromKclValue, | ||||||
|   isPathToNodeNumber, |   isPathToNodeNumber, | ||||||
|  |   parse, | ||||||
|   formatNumber, |   formatNumber, | ||||||
| } from './wasm' | } from './wasm' | ||||||
| import { | import { | ||||||
| @ -31,6 +32,8 @@ import { | |||||||
|   getNodeFromPath, |   getNodeFromPath, | ||||||
|   isNodeSafeToReplace, |   isNodeSafeToReplace, | ||||||
|   traverse, |   traverse, | ||||||
|  |   getBodyIndex, | ||||||
|  |   isCallExprWithName, | ||||||
|   ARG_INDEX_FIELD, |   ARG_INDEX_FIELD, | ||||||
|   LABELED_ARG_FIELD, |   LABELED_ARG_FIELD, | ||||||
| } from './queryAst' | } from './queryAst' | ||||||
| @ -56,6 +59,8 @@ import { Models } from '@kittycad/lib' | |||||||
| import { ExtrudeFacePlane } from 'machines/modelingMachine' | import { ExtrudeFacePlane } from 'machines/modelingMachine' | ||||||
| import { Node } from 'wasm-lib/kcl/bindings/Node' | import { Node } from 'wasm-lib/kcl/bindings/Node' | ||||||
| import { KclExpressionWithVariable } from 'lib/commandTypes' | import { KclExpressionWithVariable } from 'lib/commandTypes' | ||||||
|  | import { Artifact, getPathsFromArtifact } from './std/artifactGraph' | ||||||
|  | import { BodyItem } from 'wasm-lib/kcl/bindings/BodyItem' | ||||||
| import { findKwArg } from './util' | import { findKwArg } from './util' | ||||||
| import { deleteEdgeTreatment } from './modifyAst/addEdgeTreatment' | import { deleteEdgeTreatment } from './modifyAst/addEdgeTreatment' | ||||||
|  |  | ||||||
| @ -90,41 +95,54 @@ export function startSketchOnDefault( | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| export function addStartProfileAt( | export function insertNewStartProfileAt( | ||||||
|   node: Node<Program>, |   node: Node<Program>, | ||||||
|   pathToNode: PathToNode, |   sketchEntryNodePath: PathToNode, | ||||||
|   at: [number, number] |   sketchNodePaths: PathToNode[], | ||||||
| ): { modifiedAst: Node<Program>; pathToNode: PathToNode } | Error { |   planeNodePath: PathToNode, | ||||||
|   const _node1 = getNodeFromPath<VariableDeclaration>( |   at: [number, number], | ||||||
|     node, |   insertType: 'start' | 'end' = 'end' | ||||||
|     pathToNode, | ): | ||||||
|     'VariableDeclaration' |   | { | ||||||
|   ) |       modifiedAst: Node<Program> | ||||||
|   if (err(_node1)) return _node1 |       updatedSketchNodePaths: PathToNode[] | ||||||
|   const variableDeclaration = _node1.node |       updatedEntryNodePath: PathToNode | ||||||
|   if (variableDeclaration.type !== 'VariableDeclaration') { |  | ||||||
|     return new Error('variableDeclaration.init.type !== PipeExpression') |  | ||||||
|     } |     } | ||||||
|   const _node = { ...node } |   | Error { | ||||||
|   const init = variableDeclaration.declaration.init |   const varDec = getNodeFromPath<VariableDeclarator>( | ||||||
|   const startProfileAt = createCallExpressionStdLib('startProfileAt', [ |     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([ |       createArrayExpression([ | ||||||
|         createLiteral(roundOff(at[0])), |         createLiteral(roundOff(at[0])), | ||||||
|         createLiteral(roundOff(at[1])), |         createLiteral(roundOff(at[1])), | ||||||
|       ]), |       ]), | ||||||
|     createPipeSubstitution(), |       createIdentifier(varDec.node.id.name), | ||||||
|     ]) |     ]) | ||||||
|   if (init.type === 'PipeExpression') { |   ) | ||||||
|     init.body.splice(1, 0, startProfileAt) |   const insertIndex = getInsertIndex(sketchNodePaths, planeNodePath, insertType) | ||||||
|   } else { |  | ||||||
|     variableDeclaration.declaration.init = createPipeExpression([ |   const _node = structuredClone(node) | ||||||
|       init, |   // TODO the rest of this function will not be robust to work for sketches defined within a function declaration | ||||||
|       startProfileAt, |   _node.body.splice(insertIndex, 0, newExpression) | ||||||
|     ]) |  | ||||||
|   } |   const { updatedEntryNodePath, updatedSketchNodePaths } = | ||||||
|  |     updateSketchNodePathsWithInsertIndex({ | ||||||
|  |       insertIndex, | ||||||
|  |       insertType, | ||||||
|  |       sketchNodePaths, | ||||||
|  |     }) | ||||||
|   return { |   return { | ||||||
|     modifiedAst: _node, |     modifiedAst: _node, | ||||||
|     pathToNode, |     updatedSketchNodePaths, | ||||||
|  |     updatedEntryNodePath, | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| @ -224,8 +242,21 @@ export function mutateKwArg( | |||||||
|   for (let i = 0; i < node.arguments.length; i++) { |   for (let i = 0; i < node.arguments.length; i++) { | ||||||
|     const arg = node.arguments[i] |     const arg = node.arguments[i] | ||||||
|     if (arg.label.name === label) { |     if (arg.label.name === label) { | ||||||
|  |       if (isLiteralArrayOrStatic(val) && isLiteralArrayOrStatic(arg.arg)) { | ||||||
|         node.arguments[i].arg = val |         node.arguments[i].arg = val | ||||||
|         return true |         return true | ||||||
|  |       } else if ( | ||||||
|  |         arg.arg.type === 'ArrayExpression' && | ||||||
|  |         val.type === 'ArrayExpression' | ||||||
|  |       ) { | ||||||
|  |         const arrExp = arg.arg | ||||||
|  |         arrExp.elements.forEach((element, i) => { | ||||||
|  |           if (isLiteralArrayOrStatic(element)) { | ||||||
|  |             arrExp.elements[i] = val.elements[i] | ||||||
|  |           } | ||||||
|  |         }) | ||||||
|  |         return true | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   node.arguments.push(createLabeledArg(label, val)) |   node.arguments.push(createLabeledArg(label, val)) | ||||||
| @ -287,15 +318,16 @@ export function mutateObjExpProp( | |||||||
| export function extrudeSketch({ | export function extrudeSketch({ | ||||||
|   node, |   node, | ||||||
|   pathToNode, |   pathToNode, | ||||||
|   shouldPipe = false, |  | ||||||
|   distance = createLiteral(4), |   distance = createLiteral(4), | ||||||
|   extrudeName, |   extrudeName, | ||||||
|  |   artifact | ||||||
| }: { | }: { | ||||||
|   node: Node<Program> |   node: Node<Program> | ||||||
|   pathToNode: PathToNode |   pathToNode: PathToNode | ||||||
|   shouldPipe?: boolean |   shouldPipe?: boolean | ||||||
|   distance: Expr |   distance: Expr | ||||||
|   extrudeName?: string |   extrudeName?: string | ||||||
|  |   artifact?: Artifact, | ||||||
| }): | }): | ||||||
|   | { |   | { | ||||||
|       modifiedAst: Node<Program> |       modifiedAst: Node<Program> | ||||||
| @ -303,10 +335,14 @@ export function extrudeSketch({ | |||||||
|       pathToExtrudeArg: PathToNode |       pathToExtrudeArg: PathToNode | ||||||
|     } |     } | ||||||
|   | Error { |   | Error { | ||||||
|  |   const orderedSketchNodePaths = getPathsFromArtifact({ | ||||||
|  |     artifact: artifact, | ||||||
|  |     sketchPathToNode: pathToNode, | ||||||
|  |   }) | ||||||
|  |   if (err(orderedSketchNodePaths)) return orderedSketchNodePaths | ||||||
|   const _node = structuredClone(node) |   const _node = structuredClone(node) | ||||||
|   const _node1 = getNodeFromPath(_node, pathToNode) |   const _node1 = getNodeFromPath(_node, pathToNode) | ||||||
|   if (err(_node1)) return _node1 |   if (err(_node1)) return _node1 | ||||||
|   const { node: sketchExpression } = _node1 |  | ||||||
|  |  | ||||||
|   // determine if sketchExpression is in a pipeExpression or not |   // determine if sketchExpression is in a pipeExpression or not | ||||||
|   const _node2 = getNodeFromPath<PipeExpression>( |   const _node2 = getNodeFromPath<PipeExpression>( | ||||||
| @ -315,9 +351,6 @@ export function extrudeSketch({ | |||||||
|     'PipeExpression' |     'PipeExpression' | ||||||
|   ) |   ) | ||||||
|   if (err(_node2)) return _node2 |   if (err(_node2)) return _node2 | ||||||
|   const { node: pipeExpression } = _node2 |  | ||||||
|  |  | ||||||
|   const isInPipeExpression = pipeExpression.type === 'PipeExpression' |  | ||||||
|  |  | ||||||
|   const _node3 = getNodeFromPath<VariableDeclarator>( |   const _node3 = getNodeFromPath<VariableDeclarator>( | ||||||
|     _node, |     _node, | ||||||
| @ -325,54 +358,27 @@ export function extrudeSketch({ | |||||||
|     'VariableDeclarator' |     'VariableDeclarator' | ||||||
|   ) |   ) | ||||||
|   if (err(_node3)) return _node3 |   if (err(_node3)) return _node3 | ||||||
|   const { node: variableDeclarator, shallowPath: pathToDecleration } = _node3 |   const { node: variableDeclarator } = _node3 | ||||||
|  |  | ||||||
|   const sketchToExtrude = shouldPipe |   const extrudeCall = createCallExpressionStdLibKw( | ||||||
|     ? createPipeSubstitution() |     'extrude', | ||||||
|     : createIdentifier(variableDeclarator.id.name) |     createIdentifier(variableDeclarator.id.name), | ||||||
|   const extrudeCall = createCallExpressionStdLibKw('extrude', sketchToExtrude, [ |     [createLabeledArg('length', distance)] | ||||||
|     createLabeledArg('length', distance), |   ) | ||||||
|   ]) |  | ||||||
|   // index of the 'length' arg above. If you reorder the labeled args above, |   // index of the 'length' arg above. If you reorder the labeled args above, | ||||||
|   // make sure to update this too. |   // make sure to update this too. | ||||||
|   const argIndex = 0 |   const argIndex = 0 | ||||||
|  |  | ||||||
|   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', 'CallExpressionKw'], |  | ||||||
|       [argIndex, ARG_INDEX_FIELD], |  | ||||||
|       ['arg', LABELED_ARG_FIELD], |  | ||||||
|     ] |  | ||||||
|  |  | ||||||
|     return { |  | ||||||
|       modifiedAst: _node, |  | ||||||
|       pathToNode, |  | ||||||
|       pathToExtrudeArg, |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   // We're not creating a pipe expression, |   // We're not creating a pipe expression, | ||||||
|   // but rather a separate constant for the extrusion |   // but rather a separate constant for the extrusion | ||||||
|   const name = |   const name = | ||||||
|     extrudeName ?? findUniqueName(node, KCL_DEFAULT_CONSTANT_PREFIXES.EXTRUDE) |     extrudeName ?? findUniqueName(node, KCL_DEFAULT_CONSTANT_PREFIXES.EXTRUDE) | ||||||
|   const VariableDeclaration = createVariableDeclaration(name, extrudeCall) |   const VariableDeclaration = createVariableDeclaration(name, extrudeCall) | ||||||
|  |  | ||||||
|   const sketchIndexInPathToNode = |   const lastSketchNodePath = | ||||||
|     pathToDecleration.findIndex((a) => a[0] === 'body') + 1 |     orderedSketchNodePaths[orderedSketchNodePaths.length - 1] | ||||||
|   const sketchIndexInBody = pathToDecleration[ |  | ||||||
|     sketchIndexInPathToNode |   const sketchIndexInBody = Number(lastSketchNodePath[1][0]) | ||||||
|   ][0] as number |  | ||||||
|   _node.body.splice(sketchIndexInBody + 1, 0, VariableDeclaration) |   _node.body.splice(sketchIndexInBody + 1, 0, VariableDeclaration) | ||||||
|  |  | ||||||
|   const pathToExtrudeArg: PathToNode = [ |   const pathToExtrudeArg: PathToNode = [ | ||||||
| @ -1496,13 +1502,21 @@ export async function deleteFromSelection( | |||||||
|     const pipeBody = varDec.node.init.body |     const pipeBody = varDec.node.init.body | ||||||
|     if ( |     if ( | ||||||
|       pipeBody[0].type === 'CallExpression' && |       pipeBody[0].type === 'CallExpression' && | ||||||
|       pipeBody[0].callee.name === 'startSketchOn' |       (pipeBody[0].callee.name === 'startSketchOn' || | ||||||
|  |         pipeBody[0].callee.name === 'startProfileAt') | ||||||
|     ) { |     ) { | ||||||
|       // remove varDec |       // remove varDec | ||||||
|       const varDecIndex = varDec.shallowPath[1][0] as number |       const varDecIndex = varDec.shallowPath[1][0] as number | ||||||
|       astClone.body.splice(varDecIndex, 1) |       astClone.body.splice(varDecIndex, 1) | ||||||
|       return astClone |       return astClone | ||||||
|     } |     } | ||||||
|  |   } else if ( | ||||||
|  |     varDec.node.init.type === 'CallExpressionKw' && | ||||||
|  |     varDec.node.init.callee.name === 'circleThreePoint' | ||||||
|  |   ) { | ||||||
|  |     const varDecIndex = varDec.shallowPath[1][0] as number | ||||||
|  |     astClone.body.splice(varDecIndex, 1) | ||||||
|  |     return astClone | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   return new Error('Selection not recognised, could not delete') |   return new Error('Selection not recognised, could not delete') | ||||||
| @ -1512,6 +1526,167 @@ const nonCodeMetaEmpty = () => { | |||||||
|   return { nonCodeNodes: {}, startNodes: [], start: 0, end: 0 } |   return { nonCodeNodes: {}, startNodes: [], start: 0, end: 0 } | ||||||
| } | } | ||||||
|  |  | ||||||
| export const createLabeledArg = (name: string, arg: Expr): LabeledArg => { | export function getInsertIndex( | ||||||
|   return { label: createIdentifier(name), arg, type: 'LabeledArg' } |   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, | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function createNodeFromExprSnippet( | ||||||
|  |   strings: TemplateStringsArray, | ||||||
|  |   ...expressions: any[] | ||||||
|  | ): Node<BodyItem> | Error { | ||||||
|  |   const code = strings.reduce( | ||||||
|  |     (acc, str, i) => acc + str + (expressions[i] || ''), | ||||||
|  |     '' | ||||||
|  |   ) | ||||||
|  |   let program = parse(code) | ||||||
|  |   if (err(program)) return program | ||||||
|  |   const node = program.program?.body[0] | ||||||
|  |   if (!node) return new Error('No node found') | ||||||
|  |   return node | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export const createLabeledArg = (label: string, arg: Expr): LabeledArg => { | ||||||
|  |   return { label: createIdentifier(label), arg, type: 'LabeledArg' } | ||||||
| } | } | ||||||
|  | |||||||
| @ -5,7 +5,6 @@ import { | |||||||
|   PathToNode, |   PathToNode, | ||||||
|   Expr, |   Expr, | ||||||
|   CallExpression, |   CallExpression, | ||||||
|   PipeExpression, |  | ||||||
|   VariableDeclarator, |   VariableDeclarator, | ||||||
|   CallExpressionKw, |   CallExpressionKw, | ||||||
| } from 'lang/wasm' | } from 'lang/wasm' | ||||||
| @ -16,7 +15,6 @@ import { | |||||||
|   createCallExpressionStdLib, |   createCallExpressionStdLib, | ||||||
|   createObjectExpression, |   createObjectExpression, | ||||||
|   createIdentifier, |   createIdentifier, | ||||||
|   createPipeExpression, |  | ||||||
|   findUniqueName, |   findUniqueName, | ||||||
|   createVariableDeclaration, |   createVariableDeclaration, | ||||||
| } from 'lang/modifyAst' | } from 'lang/modifyAst' | ||||||
| @ -26,14 +24,15 @@ import { | |||||||
|   mutateAstWithTagForSketchSegment, |   mutateAstWithTagForSketchSegment, | ||||||
|   getEdgeTagCall, |   getEdgeTagCall, | ||||||
| } from 'lang/modifyAst/addEdgeTreatment' | } from 'lang/modifyAst/addEdgeTreatment' | ||||||
|  | import { Artifact, getPathsFromArtifact } from 'lang/std/artifactGraph' | ||||||
| export function revolveSketch( | export function revolveSketch( | ||||||
|   ast: Node<Program>, |   ast: Node<Program>, | ||||||
|   pathToSketchNode: PathToNode, |   pathToSketchNode: PathToNode, | ||||||
|   shouldPipe = false, |  | ||||||
|   angle: Expr = createLiteral(4), |   angle: Expr = createLiteral(4), | ||||||
|   axisOrEdge: string, |   axisOrEdge: string, | ||||||
|   axis: string, |   axis: string, | ||||||
|   edge: Selections |   edge: Selections, | ||||||
|  |   artifact?: Artifact | ||||||
| ): | ): | ||||||
|   | { |   | { | ||||||
|       modifiedAst: Node<Program> |       modifiedAst: Node<Program> | ||||||
| @ -41,6 +40,11 @@ export function revolveSketch( | |||||||
|       pathToRevolveArg: PathToNode |       pathToRevolveArg: PathToNode | ||||||
|     } |     } | ||||||
|   | Error { |   | Error { | ||||||
|  |   const orderedSketchNodePaths = getPathsFromArtifact({ | ||||||
|  |     artifact: artifact, | ||||||
|  |     sketchPathToNode: pathToSketchNode, | ||||||
|  |   }) | ||||||
|  |   if (err(orderedSketchNodePaths)) return orderedSketchNodePaths | ||||||
|   const clonedAst = structuredClone(ast) |   const clonedAst = structuredClone(ast) | ||||||
|   const sketchNode = getNodeFromPath(clonedAst, pathToSketchNode) |   const sketchNode = getNodeFromPath(clonedAst, pathToSketchNode) | ||||||
|   if (err(sketchNode)) return sketchNode |   if (err(sketchNode)) return sketchNode | ||||||
| @ -74,29 +78,13 @@ export function revolveSketch( | |||||||
|     generatedAxis = createLiteral(axis) |     generatedAxis = createLiteral(axis) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   /* Original Code */ |  | ||||||
|   const { node: sketchExpression } = sketchNode |  | ||||||
|  |  | ||||||
|   // determine if sketchExpression is in a pipeExpression or not |  | ||||||
|   const sketchPipeExpressionNode = getNodeFromPath<PipeExpression>( |  | ||||||
|     clonedAst, |  | ||||||
|     pathToSketchNode, |  | ||||||
|     'PipeExpression' |  | ||||||
|   ) |  | ||||||
|   if (err(sketchPipeExpressionNode)) return sketchPipeExpressionNode |  | ||||||
|   const { node: sketchPipeExpression } = sketchPipeExpressionNode |  | ||||||
|   const isInPipeExpression = sketchPipeExpression.type === 'PipeExpression' |  | ||||||
|  |  | ||||||
|   const sketchVariableDeclaratorNode = getNodeFromPath<VariableDeclarator>( |   const sketchVariableDeclaratorNode = getNodeFromPath<VariableDeclarator>( | ||||||
|     clonedAst, |     clonedAst, | ||||||
|     pathToSketchNode, |     pathToSketchNode, | ||||||
|     'VariableDeclarator' |     'VariableDeclarator' | ||||||
|   ) |   ) | ||||||
|   if (err(sketchVariableDeclaratorNode)) return sketchVariableDeclaratorNode |   if (err(sketchVariableDeclaratorNode)) return sketchVariableDeclaratorNode | ||||||
|   const { |   const { node: sketchVariableDeclarator } = sketchVariableDeclaratorNode | ||||||
|     node: sketchVariableDeclarator, |  | ||||||
|     shallowPath: sketchPathToDecleration, |  | ||||||
|   } = sketchVariableDeclaratorNode |  | ||||||
|  |  | ||||||
|   if (!generatedAxis) return new Error('Generated axis selection is missing.') |   if (!generatedAxis) return new Error('Generated axis selection is missing.') | ||||||
|  |  | ||||||
| @ -108,37 +96,13 @@ export function revolveSketch( | |||||||
|     createIdentifier(sketchVariableDeclarator.id.name), |     createIdentifier(sketchVariableDeclarator.id.name), | ||||||
|   ]) |   ]) | ||||||
|  |  | ||||||
|   if (shouldPipe) { |  | ||||||
|     const pipeChain = createPipeExpression( |  | ||||||
|       isInPipeExpression |  | ||||||
|         ? [...sketchPipeExpression.body, revolveCall] |  | ||||||
|         : [sketchExpression as any, revolveCall] |  | ||||||
|     ) |  | ||||||
|  |  | ||||||
|     sketchVariableDeclarator.init = pipeChain |  | ||||||
|     const pathToRevolveArg: PathToNode = [ |  | ||||||
|       ...sketchPathToDecleration, |  | ||||||
|       ['init', 'VariableDeclarator'], |  | ||||||
|       ['body', ''], |  | ||||||
|       [pipeChain.body.length - 1, 'index'], |  | ||||||
|       ['arguments', 'CallExpression'], |  | ||||||
|       [0, 'index'], |  | ||||||
|     ] |  | ||||||
|  |  | ||||||
|     return { |  | ||||||
|       modifiedAst: clonedAst, |  | ||||||
|       pathToSketchNode, |  | ||||||
|       pathToRevolveArg, |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   // We're not creating a pipe expression, |   // We're not creating a pipe expression, | ||||||
|   // but rather a separate constant for the extrusion |   // but rather a separate constant for the extrusion | ||||||
|   const name = findUniqueName(clonedAst, KCL_DEFAULT_CONSTANT_PREFIXES.REVOLVE) |   const name = findUniqueName(clonedAst, KCL_DEFAULT_CONSTANT_PREFIXES.REVOLVE) | ||||||
|   const VariableDeclaration = createVariableDeclaration(name, revolveCall) |   const VariableDeclaration = createVariableDeclaration(name, revolveCall) | ||||||
|   const sketchIndexInPathToNode = |   const lastSketchNodePath = | ||||||
|     sketchPathToDecleration.findIndex((a) => a[0] === 'body') + 1 |     orderedSketchNodePaths[orderedSketchNodePaths.length - 1] | ||||||
|   const sketchIndexInBody = sketchPathToDecleration[sketchIndexInPathToNode][0] |   const sketchIndexInBody = Number(lastSketchNodePath[1][0]) | ||||||
|   if (typeof sketchIndexInBody !== 'number') |   if (typeof sketchIndexInBody !== 'number') | ||||||
|     return new Error('expected sketchIndexInBody to be a number') |     return new Error('expected sketchIndexInBody to be a number') | ||||||
|   clonedAst.body.splice(sketchIndexInBody + 1, 0, VariableDeclaration) |   clonedAst.body.splice(sketchIndexInBody + 1, 0, VariableDeclaration) | ||||||
|  | |||||||
| @ -2,7 +2,6 @@ import { ToolTip } from 'lang/langHelpers' | |||||||
| import { Selection, Selections } from 'lib/selections' | import { Selection, Selections } from 'lib/selections' | ||||||
| import { | import { | ||||||
|   ArrayExpression, |   ArrayExpression, | ||||||
|   ArtifactGraph, |  | ||||||
|   BinaryExpression, |   BinaryExpression, | ||||||
|   CallExpression, |   CallExpression, | ||||||
|   CallExpressionKw, |   CallExpressionKw, | ||||||
| @ -23,6 +22,7 @@ import { | |||||||
|   VariableDeclaration, |   VariableDeclaration, | ||||||
|   VariableDeclarator, |   VariableDeclarator, | ||||||
|   recast, |   recast, | ||||||
|  |   ArtifactGraph, | ||||||
| } from './wasm' | } from './wasm' | ||||||
| import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils' | import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils' | ||||||
| import { createIdentifier, splitPathAtLastIndex } from './modifyAst' | import { createIdentifier, splitPathAtLastIndex } from './modifyAst' | ||||||
| @ -34,10 +34,10 @@ import { | |||||||
|   getConstraintType, |   getConstraintType, | ||||||
| } from './std/sketchcombos' | } from './std/sketchcombos' | ||||||
| import { err, Reason } from 'lib/trap' | import { err, Reason } from 'lib/trap' | ||||||
| import { ImportStatement } from 'wasm-lib/kcl/bindings/ImportStatement' |  | ||||||
| import { Node } from 'wasm-lib/kcl/bindings/Node' | import { Node } from 'wasm-lib/kcl/bindings/Node' | ||||||
| import { findKwArg } from './util' | import { findKwArg } from './util' | ||||||
| import { codeRefFromRange } from './std/artifactGraph' | import { codeRefFromRange } from './std/artifactGraph' | ||||||
|  | import { FunctionExpression } from 'wasm-lib/kcl/bindings/FunctionExpression' | ||||||
|  |  | ||||||
| export const LABELED_ARG_FIELD = 'LabeledArg -> Arg' | export const LABELED_ARG_FIELD = 'LabeledArg -> Arg' | ||||||
| export const ARG_INDEX_FIELD = 'arg index' | export const ARG_INDEX_FIELD = 'arg index' | ||||||
| @ -353,7 +353,13 @@ export function findAllPreviousVariables( | |||||||
| type ReplacerFn = ( | type ReplacerFn = ( | ||||||
|   _ast: Node<Program>, |   _ast: Node<Program>, | ||||||
|   varName: string |   varName: string | ||||||
| ) => { modifiedAst: Node<Program>; pathToReplaced: PathToNode } | Error | ) => | ||||||
|  |   | { | ||||||
|  |       modifiedAst: Node<Program> | ||||||
|  |       pathToReplaced: PathToNode | ||||||
|  |       exprInsertIndex: number | ||||||
|  |     } | ||||||
|  |   | Error | ||||||
|  |  | ||||||
| export function isNodeSafeToReplacePath( | export function isNodeSafeToReplacePath( | ||||||
|   ast: Program, |   ast: Program, | ||||||
| @ -405,7 +411,7 @@ export function isNodeSafeToReplacePath( | |||||||
|     if (err(_nodeToReplace)) return _nodeToReplace |     if (err(_nodeToReplace)) return _nodeToReplace | ||||||
|     const nodeToReplace = _nodeToReplace.node as any |     const nodeToReplace = _nodeToReplace.node as any | ||||||
|     nodeToReplace[last[0]] = identifier |     nodeToReplace[last[0]] = identifier | ||||||
|     return { modifiedAst: _ast, pathToReplaced } |     return { modifiedAst: _ast, pathToReplaced, exprInsertIndex: index } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   const hasPipeSub = isTypeInValue(finVal as Expr, 'PipeSubstitution') |   const hasPipeSub = isTypeInValue(finVal as Expr, 'PipeSubstitution') | ||||||
| @ -514,8 +520,15 @@ export function isLinesParallelAndConstrained( | |||||||
|     if (err(_primarySegment)) return _primarySegment |     if (err(_primarySegment)) return _primarySegment | ||||||
|     const primarySegment = _primarySegment.segment |     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( |     const _segment = getSketchSegmentFromSourceRange( | ||||||
|       sg, |       sg2, | ||||||
|       secondaryLine?.codeRef?.range |       secondaryLine?.codeRef?.range | ||||||
|     ) |     ) | ||||||
|     if (err(_segment)) return _segment |     if (err(_segment)) return _segment | ||||||
| @ -866,3 +879,57 @@ export function getObjExprProperty( | |||||||
|   if (index === -1) return null |   if (index === -1) return null | ||||||
|   return { expr: node.properties[index].value, index } |   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 | ||||||
|  | } | ||||||
|  | |||||||
| @ -82,6 +82,7 @@ function moreNodePathFromSourceRange( | |||||||
|           return moreNodePathFromSourceRange(arg, sourceRange, path) |           return moreNodePathFromSourceRange(arg, sourceRange, path) | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|  |       return path | ||||||
|     } |     } | ||||||
|     return path |     return path | ||||||
|   } |   } | ||||||
|  | |||||||
| @ -1,4 +1,5 @@ | |||||||
| import { | import { | ||||||
|  |   Expr, | ||||||
|   Artifact, |   Artifact, | ||||||
|   ArtifactGraph, |   ArtifactGraph, | ||||||
|   ArtifactId, |   ArtifactId, | ||||||
| @ -18,7 +19,7 @@ import { | |||||||
| import { Models } from '@kittycad/lib' | import { Models } from '@kittycad/lib' | ||||||
| import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils' | import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils' | ||||||
| import { err } from 'lib/trap' | import { err } from 'lib/trap' | ||||||
| import { codeManager } from 'lib/singletons' | import { engineCommandManager, kclManager } from 'lib/singletons' | ||||||
|  |  | ||||||
| export type { Artifact, ArtifactId, SegmentArtifact } from 'lang/wasm' | export type { Artifact, ArtifactId, SegmentArtifact } from 'lang/wasm' | ||||||
|  |  | ||||||
| @ -40,7 +41,7 @@ export interface PlaneArtifactRich extends BaseArtifact { | |||||||
| export interface PathArtifactRich extends BaseArtifact { | export interface PathArtifactRich extends BaseArtifact { | ||||||
|   type: 'path' |   type: 'path' | ||||||
|   /** A path must always lie on a plane */ |   /** A path must always lie on a plane */ | ||||||
|   plane: PlaneArtifact | WallArtifact |   plane: PlaneArtifact | WallArtifact | CapArtifact | ||||||
|   /** A path must always contain 0 or more segments */ |   /** A path must always contain 0 or more segments */ | ||||||
|   segments: Array<SegmentArtifact> |   segments: Array<SegmentArtifact> | ||||||
|   /** A path may not result in a sweep artifact */ |   /** A path may not result in a sweep artifact */ | ||||||
| @ -51,7 +52,7 @@ export interface PathArtifactRich extends BaseArtifact { | |||||||
| interface SegmentArtifactRich extends BaseArtifact { | interface SegmentArtifactRich extends BaseArtifact { | ||||||
|   type: 'segment' |   type: 'segment' | ||||||
|   path: PathArtifact |   path: PathArtifact | ||||||
|   surf?: WallArtifact |   surf: WallArtifact | ||||||
|   edges: Array<SweepEdge> |   edges: Array<SweepEdge> | ||||||
|   edgeCut?: EdgeCut |   edgeCut?: EdgeCut | ||||||
|   codeRef: CodeRef |   codeRef: CodeRef | ||||||
| @ -239,6 +240,7 @@ export function expandSegment( | |||||||
|   if (err(path)) return path |   if (err(path)) return path | ||||||
|   if (err(surf)) return surf |   if (err(surf)) return surf | ||||||
|   if (err(edgeCut)) return edgeCut |   if (err(edgeCut)) return edgeCut | ||||||
|  |   if (!surf) return new Error('Segment does not have a surface') | ||||||
|  |  | ||||||
|   return { |   return { | ||||||
|     type: 'segment', |     type: 'segment', | ||||||
| @ -410,6 +412,205 @@ export function codeRefFromRange(range: SourceRange, ast: Program): CodeRef { | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | function getPlaneFromPath( | ||||||
|  |   path: PathArtifact, | ||||||
|  |   graph: ArtifactGraph | ||||||
|  | ): PlaneArtifact | WallArtifact | CapArtifact | Error { | ||||||
|  |   const plane = getArtifactOfTypes( | ||||||
|  |     { key: path.planeId, types: ['plane', 'wall', 'cap'] }, | ||||||
|  |     graph | ||||||
|  |   ) | ||||||
|  |   if (err(plane)) return plane | ||||||
|  |   return plane | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getPlaneFromSegment( | ||||||
|  |   segment: SegmentArtifact, | ||||||
|  |   graph: ArtifactGraph | ||||||
|  | ): PlaneArtifact | WallArtifact | CapArtifact | Error { | ||||||
|  |   const path = getArtifactOfTypes( | ||||||
|  |     { key: segment.pathId, types: ['path'] }, | ||||||
|  |     graph | ||||||
|  |   ) | ||||||
|  |   if (err(path)) return path | ||||||
|  |   return getPlaneFromPath(path, graph) | ||||||
|  | } | ||||||
|  | function getPlaneFromSolid2D( | ||||||
|  |   solid2D: Solid2D, | ||||||
|  |   graph: ArtifactGraph | ||||||
|  | ): PlaneArtifact | WallArtifact | CapArtifact | Error { | ||||||
|  |   const path = getArtifactOfTypes( | ||||||
|  |     { key: solid2D.pathId, types: ['path'] }, | ||||||
|  |     graph | ||||||
|  |   ) | ||||||
|  |   if (err(path)) return path | ||||||
|  |   return getPlaneFromPath(path, graph) | ||||||
|  | } | ||||||
|  | function getPlaneFromCap( | ||||||
|  |   cap: CapArtifact, | ||||||
|  |   graph: ArtifactGraph | ||||||
|  | ): PlaneArtifact | WallArtifact | CapArtifact | Error { | ||||||
|  |   const sweep = getArtifactOfTypes( | ||||||
|  |     { key: cap.sweepId, types: ['sweep'] }, | ||||||
|  |     graph | ||||||
|  |   ) | ||||||
|  |   if (err(sweep)) return sweep | ||||||
|  |   const path = getArtifactOfTypes({ key: sweep.pathId, types: ['path'] }, graph) | ||||||
|  |   if (err(path)) return path | ||||||
|  |   return getPlaneFromPath(path, graph) | ||||||
|  | } | ||||||
|  | function getPlaneFromWall( | ||||||
|  |   wall: WallArtifact, | ||||||
|  |   graph: ArtifactGraph | ||||||
|  | ): PlaneArtifact | WallArtifact | CapArtifact | Error { | ||||||
|  |   const sweep = getArtifactOfTypes( | ||||||
|  |     { key: wall.sweepId, types: ['sweep'] }, | ||||||
|  |     graph | ||||||
|  |   ) | ||||||
|  |   if (err(sweep)) return sweep | ||||||
|  |   const path = getArtifactOfTypes({ key: sweep.pathId, types: ['path'] }, graph) | ||||||
|  |   if (err(path)) return path | ||||||
|  |   return getPlaneFromPath(path, graph) | ||||||
|  | } | ||||||
|  | function getPlaneFromSweepEdge(edge: SweepEdge, graph: ArtifactGraph) { | ||||||
|  |   const sweep = getArtifactOfTypes( | ||||||
|  |     { key: edge.sweepId, types: ['sweep'] }, | ||||||
|  |     graph | ||||||
|  |   ) | ||||||
|  |   if (err(sweep)) return sweep | ||||||
|  |   const path = getArtifactOfTypes({ key: sweep.pathId, types: ['path'] }, graph) | ||||||
|  |   if (err(path)) return path | ||||||
|  |   return getPlaneFromPath(path, graph) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function getPlaneFromArtifact( | ||||||
|  |   artifact: Artifact | undefined, | ||||||
|  |   graph: ArtifactGraph | ||||||
|  | ): PlaneArtifact | WallArtifact | CapArtifact | Error { | ||||||
|  |   if (!artifact) return new Error(`Artifact is undefined`) | ||||||
|  |   if (artifact.type === 'plane') return artifact | ||||||
|  |   if (artifact.type === 'path') return getPlaneFromPath(artifact, graph) | ||||||
|  |   if (artifact.type === 'segment') return getPlaneFromSegment(artifact, graph) | ||||||
|  |   if (artifact.type === 'solid2d') return getPlaneFromSolid2D(artifact, graph) | ||||||
|  |   if (artifact.type === 'cap') return getPlaneFromCap(artifact, graph) | ||||||
|  |   if (artifact.type === 'wall') return getPlaneFromWall(artifact, graph) | ||||||
|  |   if (artifact.type === 'sweepEdge') | ||||||
|  |     return getPlaneFromSweepEdge(artifact, graph) | ||||||
|  |   return new Error(`Artifact type ${artifact.type} does not have a plane`) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const isExprSafe = (index: number): boolean => { | ||||||
|  |   const expr = kclManager.ast.body?.[index] | ||||||
|  |   if (!expr) { | ||||||
|  |     return false | ||||||
|  |   } | ||||||
|  |   if (expr.type === 'ImportStatement' || expr.type === 'ReturnStatement') { | ||||||
|  |     return false | ||||||
|  |   } | ||||||
|  |   if (expr.type === 'VariableDeclaration') { | ||||||
|  |     const init = expr.declaration?.init | ||||||
|  |     if (!init) return false | ||||||
|  |     if (init.type === 'CallExpression') { | ||||||
|  |       return false | ||||||
|  |     } | ||||||
|  |     if (init.type === 'BinaryExpression' && isNodeSafe(init)) { | ||||||
|  |       return true | ||||||
|  |     } | ||||||
|  |     if (init.type === 'Literal' || init.type === 'MemberExpression') { | ||||||
|  |       return true | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   return false | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const onlyConsecutivePaths = ( | ||||||
|  |   orderedNodePaths: PathToNode[], | ||||||
|  |   originalPath: PathToNode | ||||||
|  | ): PathToNode[] => { | ||||||
|  |   const originalIndex = Number( | ||||||
|  |     orderedNodePaths.find( | ||||||
|  |       (path) => path[1][0] === originalPath[1][0] | ||||||
|  |     )?.[1]?.[0] || 0 | ||||||
|  |   ) | ||||||
|  |  | ||||||
|  |   const minIndex = Number(orderedNodePaths[0][1][0]) | ||||||
|  |   const maxIndex = Number(orderedNodePaths[orderedNodePaths.length - 1][1][0]) | ||||||
|  |   const pathIndexMap: any = {} | ||||||
|  |   orderedNodePaths.forEach((path) => { | ||||||
|  |     const bodyIndex = Number(path[1][0]) | ||||||
|  |     pathIndexMap[bodyIndex] = path | ||||||
|  |   }) | ||||||
|  |   const safePaths: PathToNode[] = [] | ||||||
|  |  | ||||||
|  |   // traverse expressions in either direction from the profile selected | ||||||
|  |   // when the user entered sketch mode | ||||||
|  |   for (let i = originalIndex; i <= maxIndex; i++) { | ||||||
|  |     if (pathIndexMap[i]) { | ||||||
|  |       safePaths.push(pathIndexMap[i]) | ||||||
|  |     } else if (!isExprSafe(i)) { | ||||||
|  |       break | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   for (let i = originalIndex - 1; i >= minIndex; i--) { | ||||||
|  |     if (pathIndexMap[i]) { | ||||||
|  |       safePaths.unshift(pathIndexMap[i]) | ||||||
|  |     } else if (!isExprSafe(i)) { | ||||||
|  |       break | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   return safePaths | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function getPathsFromPlaneArtifact(planeArtifact: PlaneArtifact) { | ||||||
|  |   const nodePaths: PathToNode[] = [] | ||||||
|  |   for (const pathId of planeArtifact.pathIds) { | ||||||
|  |     const path = engineCommandManager.artifactGraph.get(pathId) | ||||||
|  |     if (!path) continue | ||||||
|  |     if ('codeRef' in path && path.codeRef) { | ||||||
|  |       // TODO should figure out why upstream the path is bad | ||||||
|  |       const isNodePathBad = path.codeRef.pathToNode.length < 2 | ||||||
|  |       nodePaths.push( | ||||||
|  |         isNodePathBad | ||||||
|  |           ? getNodePathFromSourceRange(kclManager.ast, path.codeRef.range) | ||||||
|  |           : path.codeRef.pathToNode | ||||||
|  |       ) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   return onlyConsecutivePaths(nodePaths, nodePaths[0]) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function getPathsFromArtifact({ | ||||||
|  |   sketchPathToNode, | ||||||
|  |   artifact, | ||||||
|  | }: { | ||||||
|  |   sketchPathToNode: PathToNode | ||||||
|  |   artifact?: Artifact | ||||||
|  | }): PathToNode[] | Error { | ||||||
|  |   const plane = getPlaneFromArtifact( | ||||||
|  |     artifact, | ||||||
|  |     engineCommandManager.artifactGraph | ||||||
|  |   ) | ||||||
|  |   if (err(plane)) return plane | ||||||
|  |   const paths = getArtifactsOfTypes( | ||||||
|  |     { keys: plane.pathIds, types: ['path'] }, | ||||||
|  |     engineCommandManager.artifactGraph | ||||||
|  |   ) | ||||||
|  |   let nodePaths = [...paths.values()] | ||||||
|  |     .map((path) => path.codeRef.pathToNode) | ||||||
|  |     .sort((a, b) => Number(a[1][0]) - Number(b[1][0])) | ||||||
|  |   return onlyConsecutivePaths(nodePaths, sketchPathToNode) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function isNodeSafe(node: Expr): boolean { | ||||||
|  |   if (node.type === 'Literal' || node.type === 'MemberExpression') { | ||||||
|  |     return true | ||||||
|  |   } | ||||||
|  |   if (node.type === 'BinaryExpression') { | ||||||
|  |     return isNodeSafe(node.left) && isNodeSafe(node.right) | ||||||
|  |   } | ||||||
|  |   return false | ||||||
|  | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Get an artifact from a code source range |  * Get an artifact from a code source range | ||||||
|  */ |  */ | ||||||
| @ -418,7 +619,7 @@ export function getArtifactFromRange( | |||||||
|   artifactGraph: ArtifactGraph |   artifactGraph: ArtifactGraph | ||||||
| ): Artifact | null { | ): Artifact | null { | ||||||
|   for (const artifact of artifactGraph.values()) { |   for (const artifact of artifactGraph.values()) { | ||||||
|     if ('codeRef' in artifact) { |     if ('codeRef' in artifact && artifact.codeRef) { | ||||||
|       const match = |       const match = | ||||||
|         artifact.codeRef?.range[0] === range[0] && |         artifact.codeRef?.range[0] === range[0] && | ||||||
|         artifact.codeRef.range[1] === range[1] |         artifact.codeRef.range[1] === range[1] | ||||||
|  | |||||||
| Before Width: | Height: | Size: 569 KiB After Width: | Height: | Size: 560 KiB | 
| @ -66,7 +66,12 @@ import { perpendicularDistance } from 'sketch-helpers' | |||||||
| import { TagDeclarator } from 'wasm-lib/kcl/bindings/TagDeclarator' | import { TagDeclarator } from 'wasm-lib/kcl/bindings/TagDeclarator' | ||||||
| import { EdgeCutInfo } from 'machines/modelingMachine' | import { EdgeCutInfo } from 'machines/modelingMachine' | ||||||
| import { Node } from 'wasm-lib/kcl/bindings/Node' | import { Node } from 'wasm-lib/kcl/bindings/Node' | ||||||
| import { findKwArg, findKwArgAny, findKwArgAnyIndex } from 'lang/util' | import { | ||||||
|  |   findKwArg, | ||||||
|  |   findKwArgWithIndex, | ||||||
|  |   findKwArgAny, | ||||||
|  |   findKwArgAnyIndex, | ||||||
|  | } from 'lang/util' | ||||||
|  |  | ||||||
| export const ARG_TAG = 'tag' | export const ARG_TAG = 'tag' | ||||||
| export const ARG_END = 'end' | export const ARG_END = 'end' | ||||||
| @ -76,6 +81,9 @@ const STRAIGHT_SEGMENT_ERR = new Error( | |||||||
|   'Invalid input, expected "straight-segment"' |   'Invalid input, expected "straight-segment"' | ||||||
| ) | ) | ||||||
| const ARC_SEGMENT_ERR = new Error('Invalid input, expected "arc-segment"') | const ARC_SEGMENT_ERR = new Error('Invalid input, expected "arc-segment"') | ||||||
|  | const CIRCLE_THREE_POINT_SEGMENT_ERR = new Error( | ||||||
|  |   'Invalid input, expected "circle-three-point-segment"' | ||||||
|  | ) | ||||||
|  |  | ||||||
| export type Coords2d = [number, number] | export type Coords2d = [number, number] | ||||||
|  |  | ||||||
| @ -171,7 +179,8 @@ const commonConstraintInfoHelper = ( | |||||||
|     } |     } | ||||||
|   ], |   ], | ||||||
|   code: string, |   code: string, | ||||||
|   pathToNode: PathToNode |   pathToNode: PathToNode, | ||||||
|  |   filterValue?: string | ||||||
| ) => { | ) => { | ||||||
|   if (callExp.type !== 'CallExpression' && callExp.type !== 'CallExpressionKw') |   if (callExp.type !== 'CallExpression' && callExp.type !== 'CallExpressionKw') | ||||||
|     return [] |     return [] | ||||||
| @ -295,7 +304,8 @@ const horzVertConstraintInfoHelper = ( | |||||||
|   stdLibFnName: ConstrainInfo['stdLibFnName'], |   stdLibFnName: ConstrainInfo['stdLibFnName'], | ||||||
|   abbreviatedInput: AbbreviatedInput, |   abbreviatedInput: AbbreviatedInput, | ||||||
|   code: string, |   code: string, | ||||||
|   pathToNode: PathToNode |   pathToNode: PathToNode, | ||||||
|  |   filterValue?: string | ||||||
| ) => { | ) => { | ||||||
|   if (callExp.type !== 'CallExpression') return [] |   if (callExp.type !== 'CallExpression') return [] | ||||||
|   const firstArg = callExp.arguments?.[0] |   const firstArg = callExp.arguments?.[0] | ||||||
| @ -502,13 +512,14 @@ export const lineTo: SketchLineHelperKw = { | |||||||
|   }) => { |   }) => { | ||||||
|     if (segmentInput.type !== 'straight-segment') return STRAIGHT_SEGMENT_ERR |     if (segmentInput.type !== 'straight-segment') return STRAIGHT_SEGMENT_ERR | ||||||
|     const to = segmentInput.to |     const to = segmentInput.to | ||||||
|     const _node = { ...node } |     const _node = structuredClone(node) | ||||||
|     const nodeMeta = getNodeFromPath<PipeExpression | CallExpressionKw>( |     const nodeMeta = getNodeFromPath<PipeExpression | CallExpressionKw>( | ||||||
|       _node, |       _node, | ||||||
|       pathToNode, |       pathToNode, | ||||||
|       'PipeExpression' |       'PipeExpression' | ||||||
|     ) |     ) | ||||||
|     if (err(nodeMeta)) return nodeMeta |     if (err(nodeMeta)) return nodeMeta | ||||||
|  |  | ||||||
|     const { node: pipe } = nodeMeta |     const { node: pipe } = nodeMeta | ||||||
|     const nodeMeta2 = getNodeFromPath<VariableDeclarator>( |     const nodeMeta2 = getNodeFromPath<VariableDeclarator>( | ||||||
|       _node, |       _node, | ||||||
| @ -783,11 +794,11 @@ export const xLine: SketchLineHelper = { | |||||||
|   add: ({ node, pathToNode, segmentInput, replaceExistingCallback }) => { |   add: ({ node, pathToNode, segmentInput, replaceExistingCallback }) => { | ||||||
|     if (segmentInput.type !== 'straight-segment') return STRAIGHT_SEGMENT_ERR |     if (segmentInput.type !== 'straight-segment') return STRAIGHT_SEGMENT_ERR | ||||||
|     const { from, to } = segmentInput |     const { from, to } = segmentInput | ||||||
|     const _node = { ...node } |     const _node = structuredClone(node) | ||||||
|     const getNode = getNodeFromPathCurry(_node, pathToNode) |     const getNode = getNodeFromPathCurry(_node, pathToNode) | ||||||
|     const _node1 = getNode<PipeExpression>('PipeExpression') |     const varDec = getNode<VariableDeclaration>('VariableDeclaration') | ||||||
|     if (err(_node1)) return _node1 |     if (err(varDec)) return varDec | ||||||
|     const { node: pipe } = _node1 |     const dec = varDec.node.declaration | ||||||
|  |  | ||||||
|     const newVal = createLiteral(roundOff(to[0] - from[0], 2)) |     const newVal = createLiteral(roundOff(to[0] - from[0], 2)) | ||||||
|  |  | ||||||
| @ -802,7 +813,11 @@ export const xLine: SketchLineHelper = { | |||||||
|       ]) |       ]) | ||||||
|       if (err(result)) return result |       if (err(result)) return result | ||||||
|       const { callExp, valueUsedInTransform } = result |       const { callExp, valueUsedInTransform } = result | ||||||
|       pipe.body[callIndex] = callExp |       if (dec.init.type === 'PipeExpression') { | ||||||
|  |         dec.init.body[callIndex] = callExp | ||||||
|  |       } else { | ||||||
|  |         dec.init = callExp | ||||||
|  |       } | ||||||
|       return { |       return { | ||||||
|         modifiedAst: _node, |         modifiedAst: _node, | ||||||
|         pathToNode, |         pathToNode, | ||||||
| @ -814,7 +829,11 @@ export const xLine: SketchLineHelper = { | |||||||
|       newVal, |       newVal, | ||||||
|       createPipeSubstitution(), |       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 } |     return { modifiedAst: _node, pathToNode } | ||||||
|   }, |   }, | ||||||
|   updateArgs: ({ node, pathToNode, input }) => { |   updateArgs: ({ node, pathToNode, input }) => { | ||||||
| @ -851,11 +870,11 @@ export const yLine: SketchLineHelper = { | |||||||
|   add: ({ node, pathToNode, segmentInput, replaceExistingCallback }) => { |   add: ({ node, pathToNode, segmentInput, replaceExistingCallback }) => { | ||||||
|     if (segmentInput.type !== 'straight-segment') return STRAIGHT_SEGMENT_ERR |     if (segmentInput.type !== 'straight-segment') return STRAIGHT_SEGMENT_ERR | ||||||
|     const { from, to } = segmentInput |     const { from, to } = segmentInput | ||||||
|     const _node = { ...node } |     const _node = structuredClone(node) | ||||||
|     const getNode = getNodeFromPathCurry(_node, pathToNode) |     const getNode = getNodeFromPathCurry(_node, pathToNode) | ||||||
|     const _node1 = getNode<PipeExpression>('PipeExpression') |     const varDec = getNode<VariableDeclaration>('VariableDeclaration') | ||||||
|     if (err(_node1)) return _node1 |     if (err(varDec)) return varDec | ||||||
|     const { node: pipe } = _node1 |     const dec = varDec.node.declaration | ||||||
|     const newVal = createLiteral(roundOff(to[1] - from[1], 2)) |     const newVal = createLiteral(roundOff(to[1] - from[1], 2)) | ||||||
|     if (replaceExistingCallback) { |     if (replaceExistingCallback) { | ||||||
|       const { index: callIndex } = splitPathAtPipeExpression(pathToNode) |       const { index: callIndex } = splitPathAtPipeExpression(pathToNode) | ||||||
| @ -868,7 +887,11 @@ export const yLine: SketchLineHelper = { | |||||||
|       ]) |       ]) | ||||||
|       if (err(result)) return result |       if (err(result)) return result | ||||||
|       const { callExp, valueUsedInTransform } = result |       const { callExp, valueUsedInTransform } = result | ||||||
|       pipe.body[callIndex] = callExp |       if (dec.init.type === 'PipeExpression') { | ||||||
|  |         dec.init.body[callIndex] = callExp | ||||||
|  |       } else { | ||||||
|  |         dec.init = callExp | ||||||
|  |       } | ||||||
|       return { |       return { | ||||||
|         modifiedAst: _node, |         modifiedAst: _node, | ||||||
|         pathToNode, |         pathToNode, | ||||||
| @ -880,7 +903,11 @@ export const yLine: SketchLineHelper = { | |||||||
|       newVal, |       newVal, | ||||||
|       createPipeSubstitution(), |       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 } |     return { modifiedAst: _node, pathToNode } | ||||||
|   }, |   }, | ||||||
|   updateArgs: ({ node, pathToNode, input }) => { |   updateArgs: ({ node, pathToNode, input }) => { | ||||||
| @ -1220,6 +1247,295 @@ export const circle: SketchLineHelper = { | |||||||
|     ] |     ] | ||||||
|   }, |   }, | ||||||
| } | } | ||||||
|  | export const circleThreePoint: SketchLineHelperKw = { | ||||||
|  |   add: ({ node, pathToNode, segmentInput, replaceExistingCallback }) => { | ||||||
|  |     if (segmentInput.type !== 'circle-three-point-segment') { | ||||||
|  |       return CIRCLE_THREE_POINT_SEGMENT_ERR | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const { p1, p2, p3 } = segmentInput | ||||||
|  |     const _node = structuredClone(node) | ||||||
|  |     const nodeMeta = getNodeFromPath<VariableDeclaration>( | ||||||
|  |       _node, | ||||||
|  |       pathToNode, | ||||||
|  |       'VariableDeclaration' | ||||||
|  |     ) | ||||||
|  |     if (err(nodeMeta)) return nodeMeta | ||||||
|  |  | ||||||
|  |     const { node: varDec } = nodeMeta | ||||||
|  |  | ||||||
|  |     const createRoundedLiteral = (val: number) => | ||||||
|  |       createLiteral(roundOff(val, 2)) | ||||||
|  |     if (replaceExistingCallback) { | ||||||
|  |       const result = replaceExistingCallback([ | ||||||
|  |         { | ||||||
|  |           type: 'arrayInObject', | ||||||
|  |           index: 0, | ||||||
|  |           key: 'p1', | ||||||
|  |           argType: 'xAbsolute', | ||||||
|  |           expr: createRoundedLiteral(p1[0]), | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           type: 'arrayInObject', | ||||||
|  |           index: 1, | ||||||
|  |           key: 'p1', | ||||||
|  |           argType: 'yAbsolute', | ||||||
|  |           expr: createRoundedLiteral(p1[1]), | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           type: 'arrayInObject', | ||||||
|  |           index: 0, | ||||||
|  |           key: 'p2', | ||||||
|  |           argType: 'xAbsolute', | ||||||
|  |           expr: createRoundedLiteral(p2[0]), | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           type: 'arrayInObject', | ||||||
|  |           index: 1, | ||||||
|  |           key: 'p2', | ||||||
|  |           argType: 'yAbsolute', | ||||||
|  |           expr: createRoundedLiteral(p2[1]), | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           type: 'arrayInObject', | ||||||
|  |           index: 0, | ||||||
|  |           key: 'p3', | ||||||
|  |           argType: 'xAbsolute', | ||||||
|  |           expr: createRoundedLiteral(p3[0]), | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           type: 'arrayInObject', | ||||||
|  |           index: 1, | ||||||
|  |           key: 'p3', | ||||||
|  |           argType: 'yAbsolute', | ||||||
|  |           expr: createRoundedLiteral(p3[1]), | ||||||
|  |         }, | ||||||
|  |       ]) | ||||||
|  |       if (err(result)) return result | ||||||
|  |       const { callExp, valueUsedInTransform } = result | ||||||
|  |  | ||||||
|  |       varDec.declaration.init = callExp | ||||||
|  |  | ||||||
|  |       return { | ||||||
|  |         modifiedAst: _node, | ||||||
|  |         pathToNode, | ||||||
|  |         valueUsedInTransform, | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     return new Error('not implemented') | ||||||
|  |   }, | ||||||
|  |   updateArgs: ({ node, pathToNode, input }) => { | ||||||
|  |     if (input.type !== 'circle-three-point-segment') { | ||||||
|  |       return CIRCLE_THREE_POINT_SEGMENT_ERR | ||||||
|  |     } | ||||||
|  |     const { p1, p2, p3 } = input | ||||||
|  |     const _node = { ...node } | ||||||
|  |     const nodeMeta = getNodeFromPath<CallExpressionKw>(_node, pathToNode) | ||||||
|  |     if (err(nodeMeta)) return nodeMeta | ||||||
|  |  | ||||||
|  |     const { node: callExpression, shallowPath } = nodeMeta | ||||||
|  |     const createRounded2DPointArr = (point: [number, number]) => | ||||||
|  |       createArrayExpression([ | ||||||
|  |         createLiteral(roundOff(point[0], 2)), | ||||||
|  |         createLiteral(roundOff(point[1], 2)), | ||||||
|  |       ]) | ||||||
|  |  | ||||||
|  |     const newP1 = createRounded2DPointArr(p1) | ||||||
|  |     const newP2 = createRounded2DPointArr(p2) | ||||||
|  |     const newP3 = createRounded2DPointArr(p3) | ||||||
|  |     mutateKwArg('p1', callExpression, newP1) | ||||||
|  |     mutateKwArg('p2', callExpression, newP2) | ||||||
|  |     mutateKwArg('p3', callExpression, newP3) | ||||||
|  |  | ||||||
|  |     return { | ||||||
|  |       modifiedAst: _node, | ||||||
|  |       pathToNode: shallowPath, | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   getTag: getTagKwArg(), | ||||||
|  |   addTag: addTagKw(), | ||||||
|  |   getConstraintInfo: (callExp, code, pathToNode, filterValue) => { | ||||||
|  |     if (callExp.type !== 'CallExpressionKw') return [] | ||||||
|  |     const p1Details = findKwArgWithIndex('p1', callExp) | ||||||
|  |     const p2Details = findKwArgWithIndex('p2', callExp) | ||||||
|  |     const p3Details = findKwArgWithIndex('p3', callExp) | ||||||
|  |     if (!p1Details || !p2Details || !p3Details) return [] | ||||||
|  |     if ( | ||||||
|  |       p1Details.expr.type !== 'ArrayExpression' || | ||||||
|  |       p2Details.expr.type !== 'ArrayExpression' || | ||||||
|  |       p3Details.expr.type !== 'ArrayExpression' | ||||||
|  |     ) | ||||||
|  |       return [] | ||||||
|  |  | ||||||
|  |     const pathToP1ArrayExpression: PathToNode = [ | ||||||
|  |       ...pathToNode, | ||||||
|  |       ['arguments', 'CallExpressionKw'], | ||||||
|  |       [p1Details.argIndex, 'arg index'], | ||||||
|  |       ['arg', 'labeledArg -> Arg'], | ||||||
|  |       ['elements', 'ArrayExpression'], | ||||||
|  |     ] | ||||||
|  |     const pathToP2ArrayExpression: PathToNode = [ | ||||||
|  |       ...pathToNode, | ||||||
|  |       ['arguments', 'CallExpressionKw'], | ||||||
|  |       [p2Details.argIndex, 'arg index'], | ||||||
|  |       ['arg', 'labeledArg -> Arg'], | ||||||
|  |       ['elements', 'ArrayExpression'], | ||||||
|  |     ] | ||||||
|  |     const pathToP3ArrayExpression: PathToNode = [ | ||||||
|  |       ...pathToNode, | ||||||
|  |       ['arguments', 'CallExpressionKw'], | ||||||
|  |       [p3Details.argIndex, 'arg index'], | ||||||
|  |       ['arg', 'labeledArg -> Arg'], | ||||||
|  |       ['elements', 'ArrayExpression'], | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     const pathToP1XArg: PathToNode = [...pathToP1ArrayExpression, [0, 'index']] | ||||||
|  |     const pathToP1YArg: PathToNode = [...pathToP1ArrayExpression, [1, 'index']] | ||||||
|  |     const pathToP2XArg: PathToNode = [...pathToP2ArrayExpression, [0, 'index']] | ||||||
|  |     const pathToP2YArg: PathToNode = [...pathToP2ArrayExpression, [1, 'index']] | ||||||
|  |     const pathToP3XArg: PathToNode = [...pathToP3ArrayExpression, [0, 'index']] | ||||||
|  |     const pathToP3YArg: PathToNode = [...pathToP3ArrayExpression, [1, 'index']] | ||||||
|  |  | ||||||
|  |     const constraints: (ConstrainInfo & { filterValue: string })[] = [ | ||||||
|  |       { | ||||||
|  |         stdLibFnName: 'circleThreePoint', | ||||||
|  |         type: 'xAbsolute', | ||||||
|  |         isConstrained: isNotLiteralArrayOrStatic(p1Details.expr.elements[0]), | ||||||
|  |         sourceRange: [ | ||||||
|  |           p1Details.expr.elements[0].start, | ||||||
|  |           p1Details.expr.elements[0].end, | ||||||
|  |           0, | ||||||
|  |         ], | ||||||
|  |         pathToNode: pathToP1XArg, | ||||||
|  |         value: code.slice( | ||||||
|  |           p1Details.expr.elements[0].start, | ||||||
|  |           p1Details.expr.elements[0].end | ||||||
|  |         ), | ||||||
|  |         argPosition: { | ||||||
|  |           type: 'arrayInObject', | ||||||
|  |           index: 0, | ||||||
|  |           key: 'p1', | ||||||
|  |         }, | ||||||
|  |         filterValue: 'p1', | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         stdLibFnName: 'circleThreePoint', | ||||||
|  |         type: 'yAbsolute', | ||||||
|  |         isConstrained: isNotLiteralArrayOrStatic(p1Details.expr.elements[1]), | ||||||
|  |         sourceRange: [ | ||||||
|  |           p1Details.expr.elements[1].start, | ||||||
|  |           p1Details.expr.elements[1].end, | ||||||
|  |           0, | ||||||
|  |         ], | ||||||
|  |         pathToNode: pathToP1YArg, | ||||||
|  |         value: code.slice( | ||||||
|  |           p1Details.expr.elements[1].start, | ||||||
|  |           p1Details.expr.elements[1].end | ||||||
|  |         ), | ||||||
|  |         argPosition: { | ||||||
|  |           type: 'arrayInObject', | ||||||
|  |           index: 1, | ||||||
|  |           key: 'p1', | ||||||
|  |         }, | ||||||
|  |         filterValue: 'p1', | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         stdLibFnName: 'circleThreePoint', | ||||||
|  |         type: 'xAbsolute', | ||||||
|  |         isConstrained: isNotLiteralArrayOrStatic(p2Details.expr.elements[0]), | ||||||
|  |         sourceRange: [ | ||||||
|  |           p2Details.expr.elements[0].start, | ||||||
|  |           p2Details.expr.elements[0].end, | ||||||
|  |           0, | ||||||
|  |         ], | ||||||
|  |         pathToNode: pathToP2XArg, | ||||||
|  |         value: code.slice( | ||||||
|  |           p2Details.expr.elements[0].start, | ||||||
|  |           p2Details.expr.elements[0].end | ||||||
|  |         ), | ||||||
|  |         argPosition: { | ||||||
|  |           type: 'arrayInObject', | ||||||
|  |           index: 0, | ||||||
|  |           key: 'p2', | ||||||
|  |         }, | ||||||
|  |         filterValue: 'p2', | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         stdLibFnName: 'circleThreePoint', | ||||||
|  |         type: 'yAbsolute', | ||||||
|  |         isConstrained: isNotLiteralArrayOrStatic(p2Details.expr.elements[1]), | ||||||
|  |         sourceRange: [ | ||||||
|  |           p2Details.expr.elements[1].start, | ||||||
|  |           p2Details.expr.elements[1].end, | ||||||
|  |           0, | ||||||
|  |         ], | ||||||
|  |         pathToNode: pathToP2YArg, | ||||||
|  |         value: code.slice( | ||||||
|  |           p2Details.expr.elements[1].start, | ||||||
|  |           p2Details.expr.elements[1].end | ||||||
|  |         ), | ||||||
|  |         argPosition: { | ||||||
|  |           type: 'arrayInObject', | ||||||
|  |           index: 1, | ||||||
|  |           key: 'p2', | ||||||
|  |         }, | ||||||
|  |         filterValue: 'p2', | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         stdLibFnName: 'circleThreePoint', | ||||||
|  |         type: 'xAbsolute', | ||||||
|  |         isConstrained: isNotLiteralArrayOrStatic(p3Details.expr.elements[0]), | ||||||
|  |         sourceRange: [ | ||||||
|  |           p3Details.expr.elements[0].start, | ||||||
|  |           p3Details.expr.elements[0].end, | ||||||
|  |           0, | ||||||
|  |         ], | ||||||
|  |         pathToNode: pathToP3XArg, | ||||||
|  |         value: code.slice( | ||||||
|  |           p3Details.expr.elements[0].start, | ||||||
|  |           p3Details.expr.elements[0].end | ||||||
|  |         ), | ||||||
|  |         argPosition: { | ||||||
|  |           type: 'arrayInObject', | ||||||
|  |           index: 0, | ||||||
|  |           key: 'p3', | ||||||
|  |         }, | ||||||
|  |         filterValue: 'p3', | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         stdLibFnName: 'circleThreePoint', | ||||||
|  |         type: 'yAbsolute', | ||||||
|  |         isConstrained: isNotLiteralArrayOrStatic(p3Details.expr.elements[1]), | ||||||
|  |         sourceRange: [ | ||||||
|  |           p3Details.expr.elements[1].start, | ||||||
|  |           p3Details.expr.elements[1].end, | ||||||
|  |           0, | ||||||
|  |         ], | ||||||
|  |         pathToNode: pathToP3YArg, | ||||||
|  |         value: code.slice( | ||||||
|  |           p3Details.expr.elements[1].start, | ||||||
|  |           p3Details.expr.elements[1].end | ||||||
|  |         ), | ||||||
|  |         argPosition: { | ||||||
|  |           type: 'arrayInObject', | ||||||
|  |           index: 1, | ||||||
|  |           key: 'p3', | ||||||
|  |         }, | ||||||
|  |         filterValue: 'p3', | ||||||
|  |       }, | ||||||
|  |     ] | ||||||
|  |     const finalConstraints: ConstrainInfo[] = [] | ||||||
|  |     constraints.forEach((constraint) => { | ||||||
|  |       if (!filterValue) { | ||||||
|  |         finalConstraints.push(constraint) | ||||||
|  |       } | ||||||
|  |       if (filterValue && constraint.filterValue === filterValue) { | ||||||
|  |         finalConstraints.push(constraint) | ||||||
|  |       } | ||||||
|  |     }) | ||||||
|  |     return finalConstraints | ||||||
|  |   }, | ||||||
|  | } | ||||||
| export const angledLine: SketchLineHelper = { | export const angledLine: SketchLineHelper = { | ||||||
|   add: ({ node, pathToNode, segmentInput, replaceExistingCallback }) => { |   add: ({ node, pathToNode, segmentInput, replaceExistingCallback }) => { | ||||||
|     if (segmentInput.type !== 'straight-segment') return STRAIGHT_SEGMENT_ERR |     if (segmentInput.type !== 'straight-segment') return STRAIGHT_SEGMENT_ERR | ||||||
| @ -1991,6 +2307,7 @@ export const sketchLineHelperMap: { [key: string]: SketchLineHelper } = { | |||||||
| export const sketchLineHelperMapKw: { [key: string]: SketchLineHelperKw } = { | export const sketchLineHelperMapKw: { [key: string]: SketchLineHelperKw } = { | ||||||
|   line, |   line, | ||||||
|   lineTo, |   lineTo, | ||||||
|  |   circleThreePoint, | ||||||
| } as const | } as const | ||||||
|  |  | ||||||
| export function changeSketchArguments( | export function changeSketchArguments( | ||||||
| @ -2058,30 +2375,36 @@ export function changeSketchArguments( | |||||||
| export function getConstraintInfo( | export function getConstraintInfo( | ||||||
|   callExpression: Node<CallExpression>, |   callExpression: Node<CallExpression>, | ||||||
|   code: string, |   code: string, | ||||||
|   pathToNode: PathToNode |   pathToNode: PathToNode, | ||||||
|  |   filterValue?: string | ||||||
| ): ConstrainInfo[] { | ): ConstrainInfo[] { | ||||||
|   const fnName = callExpression?.callee?.name || '' |   const fnName = callExpression?.callee?.name || '' | ||||||
|   if (!(fnName in sketchLineHelperMap)) return [] |   if (!(fnName in sketchLineHelperMap)) return [] | ||||||
|   return sketchLineHelperMap[fnName].getConstraintInfo( |   return sketchLineHelperMap[fnName].getConstraintInfo( | ||||||
|     callExpression, |     callExpression, | ||||||
|     code, |     code, | ||||||
|     pathToNode |     pathToNode, | ||||||
|  |     filterValue | ||||||
|   ) |   ) | ||||||
| } | } | ||||||
|  |  | ||||||
| export function getConstraintInfoKw( | export function getConstraintInfoKw( | ||||||
|   callExpression: Node<CallExpressionKw>, |   callExpression: Node<CallExpressionKw>, | ||||||
|   code: string, |   code: string, | ||||||
|   pathToNode: PathToNode |   pathToNode: PathToNode, | ||||||
|  |   filterValue?: string | ||||||
| ): ConstrainInfo[] { | ): ConstrainInfo[] { | ||||||
|   const fnName = callExpression?.callee?.name || '' |   const fnName = callExpression?.callee?.name || '' | ||||||
|   const isAbsolute = findKwArg('endAbsolute', callExpression) !== undefined |   const isAbsolute = | ||||||
|  |     fnName === 'circleThreePoint' || | ||||||
|  |     findKwArg('endAbsolute', callExpression) !== undefined | ||||||
|   if (!(fnName in sketchLineHelperMapKw)) return [] |   if (!(fnName in sketchLineHelperMapKw)) return [] | ||||||
|   const correctFnName = fnName === 'line' && isAbsolute ? 'lineTo' : fnName |   const correctFnName = fnName === 'line' && isAbsolute ? 'lineTo' : fnName | ||||||
|   return sketchLineHelperMapKw[correctFnName].getConstraintInfo( |   return sketchLineHelperMapKw[correctFnName].getConstraintInfo( | ||||||
|     callExpression, |     callExpression, | ||||||
|     code, |     code, | ||||||
|     pathToNode |     pathToNode, | ||||||
|  |     filterValue | ||||||
|   ) |   ) | ||||||
| } | } | ||||||
|  |  | ||||||
| @ -2305,8 +2628,6 @@ function addTagToChamfer( | |||||||
|   if (err(variableDec)) return variableDec |   if (err(variableDec)) return variableDec | ||||||
|   const isPipeExpression = pipeExpr.node.type === 'PipeExpression' |   const isPipeExpression = pipeExpr.node.type === 'PipeExpression' | ||||||
|  |  | ||||||
|   console.log('pipeExpr', pipeExpr, variableDec) |  | ||||||
|   // const callExpr = isPipeExpression ? pipeExpr.node.body[pipeIndex] : variableDec.node.init |  | ||||||
|   const callExpr = isPipeExpression |   const callExpr = isPipeExpression | ||||||
|     ? pipeExpr.node.body[pipeIndex] |     ? pipeExpr.node.body[pipeIndex] | ||||||
|     : variableDec.node.init |     : variableDec.node.init | ||||||
| @ -2387,7 +2708,6 @@ function addTagToChamfer( | |||||||
|   if (isPipeExpression) { |   if (isPipeExpression) { | ||||||
|     pipeExpr.node.body.splice(pipeIndex, 0, newExpressionToInsert) |     pipeExpr.node.body.splice(pipeIndex, 0, newExpressionToInsert) | ||||||
|   } else { |   } else { | ||||||
|     console.log('yo', createPipeExpression([newExpressionToInsert, callExpr])) |  | ||||||
|     callExpr.arguments[1] = createPipeSubstitution() |     callExpr.arguments[1] = createPipeSubstitution() | ||||||
|     variableDec.node.init = createPipeExpression([ |     variableDec.node.init = createPipeExpression([ | ||||||
|       newExpressionToInsert, |       newExpressionToInsert, | ||||||
| @ -2724,6 +3044,8 @@ export function isAbsoluteLine(lineCall: CallExpressionKw): boolean | Error { | |||||||
|       return new Error( |       return new Error( | ||||||
|         `line call has neither ${ARG_END} nor ${ARG_END_ABSOLUTE} params` |         `line call has neither ${ARG_END} nor ${ARG_END_ABSOLUTE} params` | ||||||
|       ) |       ) | ||||||
|  |     case 'circleThreePoint': | ||||||
|  |       return false | ||||||
|   } |   } | ||||||
|   return new Error(`Unknown sketch function ${name}`) |   return new Error(`Unknown sketch function ${name}`) | ||||||
| } | } | ||||||
|  | |||||||
| @ -22,7 +22,6 @@ import { | |||||||
|   Literal, |   Literal, | ||||||
|   SourceRange, |   SourceRange, | ||||||
|   LiteralValue, |   LiteralValue, | ||||||
|   recast, |  | ||||||
|   LabeledArg, |   LabeledArg, | ||||||
| } from '../wasm' | } from '../wasm' | ||||||
| import { getNodeFromPath, getNodeFromPathCurry } from '../queryAst' | import { getNodeFromPath, getNodeFromPathCurry } from '../queryAst' | ||||||
| @ -217,14 +216,19 @@ function createStdlibCallExpressionKw( | |||||||
|   tool: ToolTip, |   tool: ToolTip, | ||||||
|   labeled: LabeledArg[], |   labeled: LabeledArg[], | ||||||
|   tag?: Expr, |   tag?: Expr, | ||||||
|   valueUsedInTransform?: number |   valueUsedInTransform?: number, | ||||||
|  |   unlabeled?: Expr | ||||||
| ): CreatedSketchExprResult { | ): CreatedSketchExprResult { | ||||||
|   const args = labeled |   const args = labeled | ||||||
|   if (tag) { |   if (tag) { | ||||||
|     args.push(createLabeledArg(ARG_TAG, tag)) |     args.push(createLabeledArg(ARG_TAG, tag)) | ||||||
|   } |   } | ||||||
|   return { |   return { | ||||||
|     callExp: createCallExpressionStdLibKw(tool, null, args), |     callExp: createCallExpressionStdLibKw( | ||||||
|  |       tool, | ||||||
|  |       unlabeled ? unlabeled : null, | ||||||
|  |       args | ||||||
|  |     ), | ||||||
|     valueUsedInTransform, |     valueUsedInTransform, | ||||||
|   } |   } | ||||||
| } | } | ||||||
| @ -1306,6 +1310,12 @@ export function getRemoveConstraintsTransform( | |||||||
|     }, |     }, | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   if ( | ||||||
|  |     sketchFnExp.type === 'CallExpressionKw' && | ||||||
|  |     sketchFnExp.callee.name === 'circleThreePoint' | ||||||
|  |   ) { | ||||||
|  |     return false | ||||||
|  |   } | ||||||
|   const isAbsolute = |   const isAbsolute = | ||||||
|     // isAbsolute doesn't matter if the call is positional. |     // isAbsolute doesn't matter if the call is positional. | ||||||
|     sketchFnExp.type === 'CallExpression' ? false : isAbsoluteLine(sketchFnExp) |     sketchFnExp.type === 'CallExpression' ? false : isAbsoluteLine(sketchFnExp) | ||||||
| @ -1320,7 +1330,6 @@ export function getRemoveConstraintsTransform( | |||||||
|       ? getFirstArg(sketchFnExp) |       ? getFirstArg(sketchFnExp) | ||||||
|       : getArgForEnd(sketchFnExp) |       : getArgForEnd(sketchFnExp) | ||||||
|   if (err(firstArg)) { |   if (err(firstArg)) { | ||||||
|     console.error(firstArg) |  | ||||||
|     return false |     return false | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @ -1351,7 +1360,7 @@ export function getRemoveConstraintsTransform( | |||||||
|  |  | ||||||
| export function removeSingleConstraint({ | export function removeSingleConstraint({ | ||||||
|   pathToCallExp, |   pathToCallExp, | ||||||
|   inputDetails, |   inputDetails: inputToReplace, | ||||||
|   ast, |   ast, | ||||||
| }: { | }: { | ||||||
|   pathToCallExp: PathToNode |   pathToCallExp: PathToNode | ||||||
| @ -1384,12 +1393,12 @@ export function removeSingleConstraint({ | |||||||
|       // So we should update the call expression to use the inputs, except for |       // So we should update the call expression to use the inputs, except for | ||||||
|       // the inputDetails, input where we should use the rawValue(s) |       // the inputDetails, input where we should use the rawValue(s) | ||||||
|  |  | ||||||
|       if (inputDetails.type === 'arrayItem') { |       if (inputToReplace.type === 'arrayItem') { | ||||||
|         const values = inputs.map((arg) => { |         const values = inputs.map((arg) => { | ||||||
|           if ( |           if ( | ||||||
|             !( |             !( | ||||||
|               (arg.type === 'arrayItem' || arg.type === 'arrayOrObjItem') && |               (arg.type === 'arrayItem' || arg.type === 'arrayOrObjItem') && | ||||||
|               arg.index === inputDetails.index |               arg.index === inputToReplace.index | ||||||
|             ) |             ) | ||||||
|           ) |           ) | ||||||
|             return arg.expr |             return arg.expr | ||||||
| @ -1397,9 +1406,9 @@ export function removeSingleConstraint({ | |||||||
|             (rawValue) => |             (rawValue) => | ||||||
|               (rawValue.type === 'arrayItem' || |               (rawValue.type === 'arrayItem' || | ||||||
|                 rawValue.type === 'arrayOrObjItem') && |                 rawValue.type === 'arrayOrObjItem') && | ||||||
|               rawValue.index === inputDetails.index |               rawValue.index === inputToReplace.index | ||||||
|           )?.expr |           )?.expr | ||||||
|           return (arg.index === inputDetails.index && literal) || arg.expr |           return (arg.index === inputToReplace.index && literal) || arg.expr | ||||||
|         }) |         }) | ||||||
|         if (callExp.node.type === 'CallExpression') { |         if (callExp.node.type === 'CallExpression') { | ||||||
|           return createStdlibCallExpression( |           return createStdlibCallExpression( | ||||||
| @ -1428,66 +1437,110 @@ export function removeSingleConstraint({ | |||||||
|         } |         } | ||||||
|       } |       } | ||||||
|       if ( |       if ( | ||||||
|         inputDetails.type === 'arrayInObject' || |         inputToReplace.type === 'arrayInObject' || | ||||||
|         inputDetails.type === 'objectProperty' |         inputToReplace.type === 'objectProperty' | ||||||
|       ) { |       ) { | ||||||
|         const arrayDetailsNameBetterLater: { |         const arrayInput: { | ||||||
|           [key: string]: Parameters<typeof createArrayExpression>[0] |           [key: string]: Parameters<typeof createArrayExpression>[0] | ||||||
|         } = {} |         } = {} | ||||||
|         const otherThing: Parameters<typeof createObjectExpression>[0] = {} |         const objInput: Parameters<typeof createObjectExpression>[0] = {} | ||||||
|         inputs.forEach((arg) => { |         const kwArgInput: ReturnType<typeof createLabeledArg>[] = [] | ||||||
|  |         inputs.forEach((currentArg) => { | ||||||
|           if ( |           if ( | ||||||
|             arg.type !== 'objectProperty' && |             // should be one of these, return early to make TS happy. | ||||||
|             arg.type !== 'arrayOrObjItem' && |             currentArg.type !== 'objectProperty' && | ||||||
|             arg.type !== 'arrayInObject' |             currentArg.type !== 'arrayOrObjItem' && | ||||||
|  |             currentArg.type !== 'arrayInObject' | ||||||
|           ) |           ) | ||||||
|             return |             return | ||||||
|           const rawLiteralArrayInObject = rawArgs.find( |           const rawLiteralArrayInObject = rawArgs.find( | ||||||
|             (rawValue) => |             (rawValue) => | ||||||
|               rawValue.type === 'arrayInObject' && |               rawValue.type === 'arrayInObject' && | ||||||
|               rawValue.key === inputDetails.key && |               rawValue.key === currentArg.key && | ||||||
|               rawValue.index === (arg.type === 'arrayInObject' ? arg.index : -1) |               rawValue.index === | ||||||
|  |                 (currentArg.type === 'arrayInObject' ? currentArg.index : -1) | ||||||
|           ) |           ) | ||||||
|           const rawLiteralObjProp = rawArgs.find( |           const rawLiteralObjProp = rawArgs.find( | ||||||
|             (rawValue) => |             (rawValue) => | ||||||
|               (rawValue.type === 'objectProperty' || |               (rawValue.type === 'objectProperty' || | ||||||
|                 rawValue.type === 'arrayOrObjItem' || |                 rawValue.type === 'arrayOrObjItem' || | ||||||
|                 rawValue.type === 'arrayInObject') && |                 rawValue.type === 'arrayInObject') && | ||||||
|               rawValue.key === inputDetails.key |               rawValue.key === inputToReplace.key | ||||||
|           ) |           ) | ||||||
|           if ( |           if ( | ||||||
|             inputDetails.type === 'arrayInObject' && |             inputToReplace.type === 'arrayInObject' && | ||||||
|             rawLiteralArrayInObject?.type === 'arrayInObject' && |             rawLiteralArrayInObject?.type === 'arrayInObject' && | ||||||
|             rawLiteralArrayInObject?.index === inputDetails.index && |             rawLiteralArrayInObject?.index === inputToReplace.index && | ||||||
|             rawLiteralArrayInObject?.key === inputDetails.key |             rawLiteralArrayInObject?.key === inputToReplace.key | ||||||
|           ) { |           ) { | ||||||
|             if (!arrayDetailsNameBetterLater[arg.key]) |             if (!arrayInput[currentArg.key]) { | ||||||
|               arrayDetailsNameBetterLater[arg.key] = [] |               arrayInput[currentArg.key] = [] | ||||||
|             arrayDetailsNameBetterLater[inputDetails.key][inputDetails.index] = |             } | ||||||
|  |             arrayInput[inputToReplace.key][inputToReplace.index] = | ||||||
|               rawLiteralArrayInObject.expr |               rawLiteralArrayInObject.expr | ||||||
|  |             let existingKwgForKey = kwArgInput.find( | ||||||
|  |               (kwArg) => kwArg.label.name === currentArg.key | ||||||
|  |             ) | ||||||
|  |             if (!existingKwgForKey) { | ||||||
|  |               existingKwgForKey = createLabeledArg( | ||||||
|  |                 currentArg.key, | ||||||
|  |                 createArrayExpression([]) | ||||||
|  |               ) | ||||||
|  |               kwArgInput.push(existingKwgForKey) | ||||||
|  |             } | ||||||
|  |             if (existingKwgForKey.arg.type === 'ArrayExpression') { | ||||||
|  |               existingKwgForKey.arg.elements[inputToReplace.index] = | ||||||
|  |                 rawLiteralArrayInObject.expr | ||||||
|  |             } | ||||||
|           } else if ( |           } else if ( | ||||||
|             inputDetails.type === 'objectProperty' && |             inputToReplace.type === 'objectProperty' && | ||||||
|             (rawLiteralObjProp?.type === 'objectProperty' || |             (rawLiteralObjProp?.type === 'objectProperty' || | ||||||
|               rawLiteralObjProp?.type === 'arrayOrObjItem') && |               rawLiteralObjProp?.type === 'arrayOrObjItem') && | ||||||
|             rawLiteralObjProp?.key === inputDetails.key && |             rawLiteralObjProp?.key === inputToReplace.key && | ||||||
|             arg.key === inputDetails.key |             currentArg.key === inputToReplace.key | ||||||
|           ) { |           ) { | ||||||
|             otherThing[inputDetails.key] = rawLiteralObjProp.expr |             objInput[inputToReplace.key] = rawLiteralObjProp.expr | ||||||
|           } else if (arg.type === 'arrayInObject') { |           } else if (currentArg.type === 'arrayInObject') { | ||||||
|             if (!arrayDetailsNameBetterLater[arg.key]) |             if (!arrayInput[currentArg.key]) arrayInput[currentArg.key] = [] | ||||||
|               arrayDetailsNameBetterLater[arg.key] = [] |             arrayInput[currentArg.key][currentArg.index] = currentArg.expr | ||||||
|             arrayDetailsNameBetterLater[arg.key][arg.index] = arg.expr |             let existingKwgForKey = kwArgInput.find( | ||||||
|           } else if (arg.type === 'objectProperty') { |               (kwArg) => kwArg.label.name === currentArg.key | ||||||
|             otherThing[arg.key] = arg.expr |             ) | ||||||
|  |             if (!existingKwgForKey) { | ||||||
|  |               existingKwgForKey = createLabeledArg( | ||||||
|  |                 currentArg.key, | ||||||
|  |                 createArrayExpression([]) | ||||||
|  |               ) | ||||||
|  |               kwArgInput.push(existingKwgForKey) | ||||||
|  |             } | ||||||
|  |             if (existingKwgForKey.arg.type === 'ArrayExpression') { | ||||||
|  |               existingKwgForKey.arg.elements[currentArg.index] = currentArg.expr | ||||||
|  |             } | ||||||
|  |           } else if (currentArg.type === 'objectProperty') { | ||||||
|  |             objInput[currentArg.key] = currentArg.expr | ||||||
|           } |           } | ||||||
|         }) |         }) | ||||||
|         const createObjParam: Parameters<typeof createObjectExpression>[0] = {} |         const createObjParam: Parameters<typeof createObjectExpression>[0] = {} | ||||||
|         Object.entries(arrayDetailsNameBetterLater).forEach(([key, value]) => { |         Object.entries(arrayInput).forEach(([key, value]) => { | ||||||
|           createObjParam[key] = createArrayExpression(value) |           createObjParam[key] = createArrayExpression(value) | ||||||
|         }) |         }) | ||||||
|  |         if ( | ||||||
|  |           callExp.node.callee.name === 'circleThreePoint' && | ||||||
|  |           callExp.node.type === 'CallExpressionKw' | ||||||
|  |         ) { | ||||||
|  |           // it's kwarg | ||||||
|  |           const inputPlane = callExp.node.unlabeled as Expr | ||||||
|  |           return createStdlibCallExpressionKw( | ||||||
|  |             callExp.node.callee.name as any, | ||||||
|  |             kwArgInput, | ||||||
|  |             tag, | ||||||
|  |             undefined, | ||||||
|  |             inputPlane | ||||||
|  |           ) | ||||||
|  |         } | ||||||
|         const objExp = createObjectExpression({ |         const objExp = createObjectExpression({ | ||||||
|           ...createObjParam, |           ...createObjParam, | ||||||
|           ...otherThing, |           ...objInput, | ||||||
|         }) |         }) | ||||||
|         return createStdlibCallExpression( |         return createStdlibCallExpression( | ||||||
|           callExp.node.callee.name as any, |           callExp.node.callee.name as any, | ||||||
| @ -1571,6 +1624,16 @@ function getTransformMapPathKw( | |||||||
|     } |     } | ||||||
|   | false { |   | false { | ||||||
|   const name = sketchFnExp.callee.name as ToolTip |   const name = sketchFnExp.callee.name as ToolTip | ||||||
|  |   if (name === 'circleThreePoint') { | ||||||
|  |     const info = transformMap?.circleThreePoint?.free?.[constraintType] | ||||||
|  |     if (info) | ||||||
|  |       return { | ||||||
|  |         toolTip: 'circleThreePoint', | ||||||
|  |         lineInputType: 'free', | ||||||
|  |         constraintType, | ||||||
|  |       } | ||||||
|  |     return false | ||||||
|  |   } | ||||||
|   const isAbsolute = findKwArg(ARG_END_ABSOLUTE, sketchFnExp) !== undefined |   const isAbsolute = findKwArg(ARG_END_ABSOLUTE, sketchFnExp) !== undefined | ||||||
|   const nameAbsolute = name === 'line' ? 'lineTo' : name |   const nameAbsolute = name === 'line' ? 'lineTo' : name | ||||||
|   if (!toolTips.includes(name)) { |   if (!toolTips.includes(name)) { | ||||||
| @ -1989,6 +2052,13 @@ export function transformAstSketchLines({ | |||||||
|               radius: seg.radius, |               radius: seg.radius, | ||||||
|               from, |               from, | ||||||
|             } |             } | ||||||
|  |           : seg.type === 'CircleThreePoint' | ||||||
|  |           ? { | ||||||
|  |               type: 'circle-three-point-segment', | ||||||
|  |               p1: seg.p1, | ||||||
|  |               p2: seg.p2, | ||||||
|  |               p3: seg.p3, | ||||||
|  |             } | ||||||
|           : { |           : { | ||||||
|               type: 'straight-segment', |               type: 'straight-segment', | ||||||
|               to, |               to, | ||||||
|  | |||||||
| @ -45,6 +45,13 @@ interface ArcSegmentInput { | |||||||
|   center: [number, number] |   center: [number, number] | ||||||
|   radius: number |   radius: number | ||||||
| } | } | ||||||
|  | /** Inputs for three point circle */ | ||||||
|  | interface CircleThreePointSegmentInput { | ||||||
|  |   type: 'circle-three-point-segment' | ||||||
|  |   p1: [number, number] | ||||||
|  |   p2: [number, number] | ||||||
|  |   p3: [number, number] | ||||||
|  | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * SegmentInputs is a union type that can be either a StraightSegmentInput or an ArcSegmentInput. |  * SegmentInputs is a union type that can be either a StraightSegmentInput or an ArcSegmentInput. | ||||||
| @ -52,7 +59,10 @@ interface ArcSegmentInput { | |||||||
|  * - StraightSegmentInput: Represents a straight segment with a starting point (from) and an ending point (to). |  * - StraightSegmentInput: Represents a straight segment with a starting point (from) and an ending point (to). | ||||||
|  * - ArcSegmentInput: Represents an arc segment with a starting point (from), a center point, and a radius. |  * - ArcSegmentInput: Represents an arc segment with a starting point (from), a center point, and a radius. | ||||||
|  */ |  */ | ||||||
| export type SegmentInputs = StraightSegmentInput | ArcSegmentInput | export type SegmentInputs = | ||||||
|  |   | StraightSegmentInput | ||||||
|  |   | ArcSegmentInput | ||||||
|  |   | CircleThreePointSegmentInput | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Interface for adding or replacing a sketch stblib call expression to a sketch. |  * Interface for adding or replacing a sketch stblib call expression to a sketch. | ||||||
| @ -85,6 +95,9 @@ export type InputArgKeys = | |||||||
|   | 'intersectTag' |   | 'intersectTag' | ||||||
|   | 'radius' |   | 'radius' | ||||||
|   | 'center' |   | 'center' | ||||||
|  |   | 'p1' | ||||||
|  |   | 'p2' | ||||||
|  |   | 'p3' | ||||||
| export interface SingleValueInput<T> { | export interface SingleValueInput<T> { | ||||||
|   type: 'singleValue' |   type: 'singleValue' | ||||||
|   argType: LineInputsType |   argType: LineInputsType | ||||||
| @ -239,7 +252,8 @@ export interface SketchLineHelper { | |||||||
|   getConstraintInfo: ( |   getConstraintInfo: ( | ||||||
|     callExp: Node<CallExpression>, |     callExp: Node<CallExpression>, | ||||||
|     code: string, |     code: string, | ||||||
|     pathToNode: PathToNode |     pathToNode: PathToNode, | ||||||
|  |     filterValue?: string | ||||||
|   ) => ConstrainInfo[] |   ) => ConstrainInfo[] | ||||||
| } | } | ||||||
|  |  | ||||||
| @ -267,6 +281,7 @@ export interface SketchLineHelperKw { | |||||||
|   getConstraintInfo: ( |   getConstraintInfo: ( | ||||||
|     callExp: Node<CallExpressionKw>, |     callExp: Node<CallExpressionKw>, | ||||||
|     code: string, |     code: string, | ||||||
|     pathToNode: PathToNode |     pathToNode: PathToNode, | ||||||
|  |     filterValue?: string | ||||||
|   ) => ConstrainInfo[] |   ) => ConstrainInfo[] | ||||||
| } | } | ||||||
|  | |||||||
| @ -14,20 +14,47 @@ import { | |||||||
| import { filterArtifacts } from 'lang/std/artifactGraph' | import { filterArtifacts } from 'lang/std/artifactGraph' | ||||||
| import { isArray, isOverlap } from 'lib/utils' | import { isArray, isOverlap } from 'lib/utils' | ||||||
|  |  | ||||||
| export function updatePathToNodeFromMap( | /** | ||||||
|   oldPath: PathToNode, |  * Updates pathToNode body indices to account for the insertion of an expression | ||||||
|   pathToNodeMap: { [key: number]: PathToNode } |  * 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 { | ): PathToNode { | ||||||
|   const updatedPathToNode = structuredClone(oldPath) |   if (exprInsertIndex < 0) return pathToNode | ||||||
|   let max = 0 |   const bodyIndex = Number(pathToNode[1][0]) | ||||||
|   Object.values(pathToNodeMap).forEach((path) => { |   if (bodyIndex < exprInsertIndex) return pathToNode | ||||||
|     const index = Number(path[1][0]) |   const clone = structuredClone(pathToNode) | ||||||
|     if (index > max) { |   clone[1][0] = bodyIndex + 1 | ||||||
|       max = index |   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( | export function isCursorInSketchCommandRange( | ||||||
| @ -36,7 +63,7 @@ export function isCursorInSketchCommandRange( | |||||||
| ): string | false { | ): string | false { | ||||||
|   const overlappingEntries = filterArtifacts( |   const overlappingEntries = filterArtifacts( | ||||||
|     { |     { | ||||||
|       types: ['segment', 'path'], |       types: ['segment', 'path', 'plane'], | ||||||
|       predicate: (artifact) => { |       predicate: (artifact) => { | ||||||
|         return selectionRanges.graphSelections.some( |         return selectionRanges.graphSelections.some( | ||||||
|           (selection) => |           (selection) => | ||||||
| @ -81,11 +108,27 @@ export function findKwArg( | |||||||
|   label: string, |   label: string, | ||||||
|   call: CallExpressionKw |   call: CallExpressionKw | ||||||
| ): Expr | undefined { | ): Expr | undefined { | ||||||
|   return call.arguments.find((arg) => { |   return call?.arguments?.find((arg) => { | ||||||
|     return arg.label.name === label |     return arg.label.name === label | ||||||
|   })?.arg |   })?.arg | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  | Search the keyword arguments from a call for an argument with this label, | ||||||
|  | returns the index of the argument as well. | ||||||
|  | */ | ||||||
|  | export function findKwArgWithIndex( | ||||||
|  |   label: string, | ||||||
|  |   call: CallExpressionKw | ||||||
|  | ): { expr: Expr; argIndex: number } | undefined { | ||||||
|  |   const index = call.arguments.findIndex((arg) => { | ||||||
|  |     return arg.label.name === label | ||||||
|  |   }) | ||||||
|  |   return index >= 0 | ||||||
|  |     ? { expr: call.arguments[index].arg, argIndex: index } | ||||||
|  |     : undefined | ||||||
|  | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
| Search the keyword arguments from a call for an argument with one of these labels. | Search the keyword arguments from a call for an argument with one of these labels. | ||||||
| */ | */ | ||||||
|  | |||||||
| @ -599,10 +599,6 @@ export const executor = async ( | |||||||
|   if (programMemoryOverride !== null && err(programMemoryOverride)) |   if (programMemoryOverride !== null && err(programMemoryOverride)) | ||||||
|     return Promise.reject(programMemoryOverride) |     return Promise.reject(programMemoryOverride) | ||||||
|  |  | ||||||
|   // eslint-disable-next-line @typescript-eslint/no-floating-promises |  | ||||||
|   if (programMemoryOverride !== null && err(programMemoryOverride)) |  | ||||||
|     return Promise.reject(programMemoryOverride) |  | ||||||
|  |  | ||||||
|   try { |   try { | ||||||
|     let jsAppSettings = default_app_settings() |     let jsAppSettings = default_app_settings() | ||||||
|     if (!TEST) { |     if (!TEST) { | ||||||
|  | |||||||
| @ -58,7 +58,7 @@ export type ModelingCommandSchema = { | |||||||
|   Revolve: { |   Revolve: { | ||||||
|     selection: Selections |     selection: Selections | ||||||
|     angle: KclCommandValue |     angle: KclCommandValue | ||||||
|     axisOrEdge: string |     axisOrEdge: 'Axis' | 'Edge' | ||||||
|     axis: string |     axis: string | ||||||
|     edge: Selections |     edge: Selections | ||||||
|   } |   } | ||||||
|  | |||||||
| @ -37,7 +37,7 @@ import { | |||||||
|  */ |  */ | ||||||
| export const getRectangleCallExpressions = ( | export const getRectangleCallExpressions = ( | ||||||
|   rectangleOrigin: [number, number], |   rectangleOrigin: [number, number], | ||||||
|   tags: [string, string, string] |   tag: string | ||||||
| ) => [ | ) => [ | ||||||
|   createCallExpressionStdLib('angledLine', [ |   createCallExpressionStdLib('angledLine', [ | ||||||
|     createArrayExpression([ |     createArrayExpression([ | ||||||
| @ -45,30 +45,28 @@ export const getRectangleCallExpressions = ( | |||||||
|       createLiteral(0), // This will be the width of the rectangle |       createLiteral(0), // This will be the width of the rectangle | ||||||
|     ]), |     ]), | ||||||
|     createPipeSubstitution(), |     createPipeSubstitution(), | ||||||
|     createTagDeclarator(tags[0]), |     createTagDeclarator(tag), | ||||||
|   ]), |   ]), | ||||||
|   createCallExpressionStdLib('angledLine', [ |   createCallExpressionStdLib('angledLine', [ | ||||||
|     createArrayExpression([ |     createArrayExpression([ | ||||||
|       createBinaryExpression([ |       createBinaryExpression([ | ||||||
|         createCallExpressionStdLib('segAng', [createIdentifier(tags[0])]), |         createCallExpressionStdLib('segAng', [createIdentifier(tag)]), | ||||||
|         '+', |         '+', | ||||||
|         createLiteral(90), |         createLiteral(90), | ||||||
|       ]), // 90 offset from the previous line |       ]), // 90 offset from the previous line | ||||||
|       createLiteral(0), // This will be the height of the rectangle |       createLiteral(0), // This will be the height of the rectangle | ||||||
|     ]), |     ]), | ||||||
|     createPipeSubstitution(), |     createPipeSubstitution(), | ||||||
|     createTagDeclarator(tags[1]), |  | ||||||
|   ]), |   ]), | ||||||
|   createCallExpressionStdLib('angledLine', [ |   createCallExpressionStdLib('angledLine', [ | ||||||
|     createArrayExpression([ |     createArrayExpression([ | ||||||
|       createCallExpressionStdLib('segAng', [createIdentifier(tags[0])]), // same angle as the first line |       createCallExpressionStdLib('segAng', [createIdentifier(tag)]), // same angle as the first line | ||||||
|       createUnaryExpression( |       createUnaryExpression( | ||||||
|         createCallExpressionStdLib('segLen', [createIdentifier(tags[0])]), |         createCallExpressionStdLib('segLen', [createIdentifier(tag)]), | ||||||
|         '-' |         '-' | ||||||
|       ), // negative height |       ), // negative height | ||||||
|     ]), |     ]), | ||||||
|     createPipeSubstitution(), |     createPipeSubstitution(), | ||||||
|     createTagDeclarator(tags[2]), |  | ||||||
|   ]), |   ]), | ||||||
|   createCallExpressionStdLibKw('line', null, [ |   createCallExpressionStdLibKw('line', null, [ | ||||||
|     createLabeledArg( |     createLabeledArg( | ||||||
| @ -95,12 +93,12 @@ export function updateRectangleSketch( | |||||||
|   y: number, |   y: number, | ||||||
|   tag: string |   tag: string | ||||||
| ) { | ) { | ||||||
|   ;((pipeExpression.body[2] as CallExpression) |   ;((pipeExpression.body[1] as CallExpression) | ||||||
|     .arguments[0] as ArrayExpression) = createArrayExpression([ |     .arguments[0] as ArrayExpression) = createArrayExpression([ | ||||||
|     createLiteral(x >= 0 ? 0 : 180), |     createLiteral(x >= 0 ? 0 : 180), | ||||||
|     createLiteral(Math.abs(x)), |     createLiteral(Math.abs(x)), | ||||||
|   ]) |   ]) | ||||||
|   ;((pipeExpression.body[3] as CallExpression) |   ;((pipeExpression.body[2] as CallExpression) | ||||||
|     .arguments[0] as ArrayExpression) = createArrayExpression([ |     .arguments[0] as ArrayExpression) = createArrayExpression([ | ||||||
|     createBinaryExpression([ |     createBinaryExpression([ | ||||||
|       createCallExpressionStdLib('segAng', [createIdentifier(tag)]), |       createCallExpressionStdLib('segAng', [createIdentifier(tag)]), | ||||||
| @ -130,7 +128,7 @@ export function updateCenterRectangleSketch( | |||||||
|   let startY = originY - Math.abs(deltaY) |   let startY = originY - Math.abs(deltaY) | ||||||
|  |  | ||||||
|   // pipeExpression.body[1] is startProfileAt |   // pipeExpression.body[1] is startProfileAt | ||||||
|   let callExpression = pipeExpression.body[1] |   let callExpression = pipeExpression.body[0] | ||||||
|   if (isCallExpression(callExpression)) { |   if (isCallExpression(callExpression)) { | ||||||
|     const arrayExpression = callExpression.arguments[0] |     const arrayExpression = callExpression.arguments[0] | ||||||
|     if (isArrayExpression(arrayExpression)) { |     if (isArrayExpression(arrayExpression)) { | ||||||
| @ -144,7 +142,7 @@ export function updateCenterRectangleSketch( | |||||||
|   const twoX = deltaX * 2 |   const twoX = deltaX * 2 | ||||||
|   const twoY = deltaY * 2 |   const twoY = deltaY * 2 | ||||||
|  |  | ||||||
|   callExpression = pipeExpression.body[2] |   callExpression = pipeExpression.body[1] | ||||||
|   if (isCallExpression(callExpression)) { |   if (isCallExpression(callExpression)) { | ||||||
|     const arrayExpression = callExpression.arguments[0] |     const arrayExpression = callExpression.arguments[0] | ||||||
|     if (isArrayExpression(arrayExpression)) { |     if (isArrayExpression(arrayExpression)) { | ||||||
| @ -160,7 +158,7 @@ export function updateCenterRectangleSketch( | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   callExpression = pipeExpression.body[3] |   callExpression = pipeExpression.body[2] | ||||||
|   if (isCallExpression(callExpression)) { |   if (isCallExpression(callExpression)) { | ||||||
|     const arrayExpression = callExpression.arguments[0] |     const arrayExpression = callExpression.arguments[0] | ||||||
|     if (isArrayExpression(arrayExpression)) { |     if (isArrayExpression(arrayExpression)) { | ||||||
|  | |||||||
| @ -276,18 +276,19 @@ export function getEventForSegmentSelection( | |||||||
|   } |   } | ||||||
|   if (!id || !group) return null |   if (!id || !group) return null | ||||||
|   const artifact = engineCommandManager.artifactGraph.get(id) |   const artifact = engineCommandManager.artifactGraph.get(id) | ||||||
|   const codeRefs = getCodeRefsByArtifactId( |   if (!artifact) return null | ||||||
|     id, |   const node = getNodeFromPath<Expr>(kclManager.ast, group.userData.pathToNode) | ||||||
|     engineCommandManager.artifactGraph |   if (err(node)) return null | ||||||
|   ) |  | ||||||
|   if (!artifact || !codeRefs) return null |  | ||||||
|   return { |   return { | ||||||
|     type: 'Set selection', |     type: 'Set selection', | ||||||
|     data: { |     data: { | ||||||
|       selectionType: 'singleCodeCursor', |       selectionType: 'singleCodeCursor', | ||||||
|       selection: { |       selection: { | ||||||
|         artifact, |         artifact, | ||||||
|         codeRef: codeRefs[0], |         codeRef: { | ||||||
|  |           pathToNode: group?.userData?.pathToNode, | ||||||
|  |           range: [node.node.start, node.node.end, 0], | ||||||
|  |         }, | ||||||
|       }, |       }, | ||||||
|     }, |     }, | ||||||
|   } |   } | ||||||
| @ -572,8 +573,7 @@ export function getSelectionTypeDisplayText( | |||||||
|   const selectionsByType = getSelectionCountByType(selection) |   const selectionsByType = getSelectionCountByType(selection) | ||||||
|   if (selectionsByType === 'none') return null |   if (selectionsByType === 'none') return null | ||||||
|  |  | ||||||
|   return selectionsByType |   return [...selectionsByType.entries()] | ||||||
|     .entries() |  | ||||||
|     .map( |     .map( | ||||||
|       // Hack for showing "face" instead of "extrude-wall" in command bar text |       // Hack for showing "face" instead of "extrude-wall" in command bar text | ||||||
|       ([type, count]) => |       ([type, count]) => | ||||||
| @ -581,7 +581,6 @@ export function getSelectionTypeDisplayText( | |||||||
|           count > 1 ? 's' : '' |           count > 1 ? 's' : '' | ||||||
|         }` |         }` | ||||||
|     ) |     ) | ||||||
|     .toArray() |  | ||||||
|     .join(', ') |     .join(', ') | ||||||
| } | } | ||||||
|  |  | ||||||
| @ -591,7 +590,7 @@ export function canSubmitSelectionArg( | |||||||
| ) { | ) { | ||||||
|   return ( |   return ( | ||||||
|     selectionsByType !== 'none' && |     selectionsByType !== 'none' && | ||||||
|     selectionsByType.entries().every(([type, count]) => { |     [...selectionsByType.entries()].every(([type, count]) => { | ||||||
|       const foundIndex = argument.selectionTypes.findIndex((s) => s === type) |       const foundIndex = argument.selectionTypes.findIndex((s) => s === type) | ||||||
|       return ( |       return ( | ||||||
|         foundIndex !== -1 && |         foundIndex !== -1 && | ||||||
| @ -867,7 +866,6 @@ export function updateSelections( | |||||||
|             JSON.stringify(pathToNode) |             JSON.stringify(pathToNode) | ||||||
|           ) { |           ) { | ||||||
|             artifact = a |             artifact = a | ||||||
|             console.log('found artifact', a) |  | ||||||
|             break |             break | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
|  | |||||||
| @ -2,8 +2,6 @@ import { CustomIconName } from 'components/CustomIcon' | |||||||
| import { DEV } from 'env' | import { DEV } from 'env' | ||||||
| import { commandBarActor, commandBarMachine } from 'machines/commandBarMachine' | import { commandBarActor, commandBarMachine } from 'machines/commandBarMachine' | ||||||
| import { | import { | ||||||
|   canRectangleOrCircleTool, |  | ||||||
|   isClosedSketch, |  | ||||||
|   isEditingExistingSketch, |   isEditingExistingSketch, | ||||||
|   modelingMachine, |   modelingMachine, | ||||||
|   pipeHasCircle, |   pipeHasCircle, | ||||||
| @ -72,7 +70,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = { | |||||||
|         icon: 'sketch', |         icon: 'sketch', | ||||||
|         status: 'available', |         status: 'available', | ||||||
|         title: ({ sketchPathId }) => |         title: ({ sketchPathId }) => | ||||||
|           `${sketchPathId ? 'Edit' : 'Start'} Sketch`, |           sketchPathId ? 'Edit Sketch' : 'Start Sketch', | ||||||
|         showTitle: true, |         showTitle: true, | ||||||
|         hotkey: 'S', |         hotkey: 'S', | ||||||
|         description: 'Start drawing a 2D sketch', |         description: 'Start drawing a 2D sketch', | ||||||
| @ -360,13 +358,6 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = { | |||||||
|       { |       { | ||||||
|         id: 'line', |         id: 'line', | ||||||
|         onClick: ({ modelingState, modelingSend }) => { |         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({ |           modelingSend({ | ||||||
|             type: 'change tool', |             type: 'change tool', | ||||||
|             data: { |             data: { | ||||||
| @ -375,7 +366,6 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = { | |||||||
|                 : 'none', |                 : 'none', | ||||||
|             }, |             }, | ||||||
|           }) |           }) | ||||||
|           } |  | ||||||
|         }, |         }, | ||||||
|         icon: 'line', |         icon: 'line', | ||||||
|         status: 'available', |         status: 'available', | ||||||
| @ -386,8 +376,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = { | |||||||
|           }) || |           }) || | ||||||
|           state.matches({ |           state.matches({ | ||||||
|             Sketch: { 'Circle tool': 'Awaiting Radius' }, |             Sketch: { 'Circle tool': 'Awaiting Radius' }, | ||||||
|           }) || |           }), | ||||||
|           isClosedSketch(state.context), |  | ||||||
|         title: 'Line', |         title: 'Line', | ||||||
|         hotkey: (state) => |         hotkey: (state) => | ||||||
|           state.matches({ Sketch: 'Line tool' }) ? ['Esc', 'L'] : 'L', |           state.matches({ Sketch: 'Line tool' }) ? ['Esc', 'L'] : 'L', | ||||||
| @ -467,14 +456,10 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = { | |||||||
|           icon: 'circle', |           icon: 'circle', | ||||||
|           status: 'available', |           status: 'available', | ||||||
|           title: 'Center circle', |           title: 'Center circle', | ||||||
|           disabled: (state) => |           disabled: (state) => state.matches('Sketch no face'), | ||||||
|             state.matches('Sketch no face') || |  | ||||||
|             (!canRectangleOrCircleTool(state.context) && |  | ||||||
|               !state.matches({ Sketch: 'Circle tool' }) && |  | ||||||
|               !state.matches({ Sketch: 'circle3PointToolSelect' })), |  | ||||||
|           isActive: (state) => |           isActive: (state) => | ||||||
|             state.matches({ Sketch: 'Circle tool' }) || |             state.matches({ Sketch: 'Circle tool' }) || | ||||||
|             state.matches({ Sketch: 'circle3PointToolSelect' }), |             state.matches({ Sketch: 'Circle three point tool' }), | ||||||
|           hotkey: (state) => |           hotkey: (state) => | ||||||
|             state.matches({ Sketch: 'Circle tool' }) ? ['Esc', 'C'] : 'C', |             state.matches({ Sketch: 'Circle tool' }) ? ['Esc', 'C'] : 'C', | ||||||
|           showTitle: false, |           showTitle: false, | ||||||
| @ -488,9 +473,9 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = { | |||||||
|               type: 'change tool', |               type: 'change tool', | ||||||
|               data: { |               data: { | ||||||
|                 tool: !modelingState.matches({ |                 tool: !modelingState.matches({ | ||||||
|                   Sketch: 'circle3PointToolSelect', |                   Sketch: 'Circle three point tool', | ||||||
|                 }) |                 }) | ||||||
|                   ? 'circle3Points' |                   ? 'circleThreePointNeo' | ||||||
|                   : 'none', |                   : 'none', | ||||||
|               }, |               }, | ||||||
|             }), |             }), | ||||||
| @ -516,10 +501,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = { | |||||||
|             }), |             }), | ||||||
|           icon: 'rectangle', |           icon: 'rectangle', | ||||||
|           status: 'available', |           status: 'available', | ||||||
|           disabled: (state) => |           disabled: (state) => state.matches('Sketch no face'), | ||||||
|             state.matches('Sketch no face') || |  | ||||||
|             (!canRectangleOrCircleTool(state.context) && |  | ||||||
|               !state.matches({ Sketch: 'Rectangle tool' })), |  | ||||||
|           title: 'Corner rectangle', |           title: 'Corner rectangle', | ||||||
|           hotkey: (state) => |           hotkey: (state) => | ||||||
|             state.matches({ Sketch: 'Rectangle tool' }) ? ['Esc', 'R'] : 'R', |             state.matches({ Sketch: 'Rectangle tool' }) ? ['Esc', 'R'] : 'R', | ||||||
| @ -542,10 +524,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = { | |||||||
|             }), |             }), | ||||||
|           icon: 'arc', |           icon: 'arc', | ||||||
|           status: 'available', |           status: 'available', | ||||||
|           disabled: (state) => |           disabled: (state) => state.matches('Sketch no face'), | ||||||
|             state.matches('Sketch no face') || |  | ||||||
|             (!canRectangleOrCircleTool(state.context) && |  | ||||||
|               !state.matches({ Sketch: 'Center Rectangle tool' })), |  | ||||||
|           title: 'Center rectangle', |           title: 'Center rectangle', | ||||||
|           hotkey: (state) => |           hotkey: (state) => | ||||||
|             state.matches({ Sketch: 'Center Rectangle tool' }) |             state.matches({ Sketch: 'Center Rectangle tool' }) | ||||||
|  | |||||||
| @ -97,3 +97,7 @@ export function trap<T>( | |||||||
|     }) |     }) | ||||||
|   return true |   return true | ||||||
| } | } | ||||||
|  |  | ||||||
|  | export function reject(errOrString: Error | string): Promise<never> { | ||||||
|  |   return Promise.reject(errOrString) | ||||||
|  | } | ||||||
|  | |||||||
| @ -109,7 +109,7 @@ function OnboardingWarningWeb(props: OnboardingResetWarningProps) { | |||||||
|           codeManager.updateCodeStateEditor(bracket) |           codeManager.updateCodeStateEditor(bracket) | ||||||
|           await codeManager.writeToFile() |           await codeManager.writeToFile() | ||||||
|  |  | ||||||
|           await kclManager.executeCode(true) |           await kclManager.executeCode({ zoomToFit: true }) | ||||||
|           props.setShouldShowWarning(false) |           props.setShouldShowWarning(false) | ||||||
|         }, reportRejection)} |         }, reportRejection)} | ||||||
|         nextText="Overwrite code and continue" |         nextText="Overwrite code and continue" | ||||||
|  | |||||||
| @ -11,7 +11,7 @@ export default function Sketching() { | |||||||
|     async function clearEditor() { |     async function clearEditor() { | ||||||
|       // We do want to update both the state and editor here. |       // We do want to update both the state and editor here. | ||||||
|       codeManager.updateCodeStateEditor('') |       codeManager.updateCodeStateEditor('') | ||||||
|       await kclManager.executeCode(true) |       await kclManager.executeCode({ zoomToFit: true }) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // eslint-disable-next-line @typescript-eslint/no-floating-promises |     // eslint-disable-next-line @typescript-eslint/no-floating-promises | ||||||
|  | |||||||
| @ -100,7 +100,7 @@ export function useDemoCode() { | |||||||
|     setTimeout( |     setTimeout( | ||||||
|       toSync(async () => { |       toSync(async () => { | ||||||
|         codeManager.updateCodeStateEditor(bracket) |         codeManager.updateCodeStateEditor(bracket) | ||||||
|         await kclManager.executeCode(true) |         await kclManager.executeCode({ zoomToFit: true }) | ||||||
|         await codeManager.writeToFile() |         await codeManager.writeToFile() | ||||||
|       }, reportRejection) |       }, reportRejection) | ||||||
|     ) |     ) | ||||||
|  | |||||||
| @ -188,6 +188,9 @@ pub struct Wall { | |||||||
|     pub sweep_id: ArtifactId, |     pub sweep_id: ArtifactId, | ||||||
|     #[serde(default, skip_serializing_if = "Vec::is_empty")] |     #[serde(default, skip_serializing_if = "Vec::is_empty")] | ||||||
|     pub path_ids: Vec<ArtifactId>, |     pub path_ids: Vec<ArtifactId>, | ||||||
|  |     /// This is for the sketch-on-face plane, not for the wall itself.  Traverse | ||||||
|  |     /// to the extrude and/or segment to get the wall's code_ref. | ||||||
|  |     pub face_code_ref: CodeRef, | ||||||
| } | } | ||||||
|  |  | ||||||
| #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)] | #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)] | ||||||
| @ -201,6 +204,9 @@ pub struct Cap { | |||||||
|     pub sweep_id: ArtifactId, |     pub sweep_id: ArtifactId, | ||||||
|     #[serde(default, skip_serializing_if = "Vec::is_empty")] |     #[serde(default, skip_serializing_if = "Vec::is_empty")] | ||||||
|     pub path_ids: Vec<ArtifactId>, |     pub path_ids: Vec<ArtifactId>, | ||||||
|  |     /// This is for the sketch-on-face plane, not for the cap itself.  Traverse | ||||||
|  |     /// to the extrude and/or segment to get the cap's code_ref. | ||||||
|  |     pub face_code_ref: CodeRef, | ||||||
| } | } | ||||||
|  |  | ||||||
| #[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, ts_rs::TS)] | #[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, ts_rs::TS)] | ||||||
| @ -619,6 +625,17 @@ fn artifacts_to_update( | |||||||
|                         edge_cut_edge_ids: wall.edge_cut_edge_ids.clone(), |                         edge_cut_edge_ids: wall.edge_cut_edge_ids.clone(), | ||||||
|                         sweep_id: wall.sweep_id, |                         sweep_id: wall.sweep_id, | ||||||
|                         path_ids: wall.path_ids.clone(), |                         path_ids: wall.path_ids.clone(), | ||||||
|  |                         face_code_ref: wall.face_code_ref.clone(), | ||||||
|  |                     })]); | ||||||
|  |                 } | ||||||
|  |                 Some(Artifact::Cap(cap)) => { | ||||||
|  |                     return Ok(vec![Artifact::Cap(Cap { | ||||||
|  |                         id: current_plane_id.into(), | ||||||
|  |                         sub_type: cap.sub_type, | ||||||
|  |                         edge_cut_edge_ids: cap.edge_cut_edge_ids.clone(), | ||||||
|  |                         sweep_id: cap.sweep_id, | ||||||
|  |                         path_ids: cap.path_ids.clone(), | ||||||
|  |                         face_code_ref: cap.face_code_ref.clone(), | ||||||
|                     })]); |                     })]); | ||||||
|                 } |                 } | ||||||
|                 Some(_) | None => { |                 Some(_) | None => { | ||||||
| @ -668,6 +685,7 @@ fn artifacts_to_update( | |||||||
|                     edge_cut_edge_ids: wall.edge_cut_edge_ids.clone(), |                     edge_cut_edge_ids: wall.edge_cut_edge_ids.clone(), | ||||||
|                     sweep_id: wall.sweep_id, |                     sweep_id: wall.sweep_id, | ||||||
|                     path_ids: vec![id], |                     path_ids: vec![id], | ||||||
|  |                     face_code_ref: wall.face_code_ref.clone(), | ||||||
|                 })); |                 })); | ||||||
|             } |             } | ||||||
|             return Ok(return_arr); |             return Ok(return_arr); | ||||||
| @ -794,13 +812,48 @@ fn artifacts_to_update( | |||||||
|                         source_ranges: vec![range], |                         source_ranges: vec![range], | ||||||
|                     }) |                     }) | ||||||
|                 })?; |                 })?; | ||||||
|                 return_arr.push(Artifact::Wall(Wall { |                 let extra_artifact = _exec_artifacts.values().find_map(|a| { | ||||||
|  |                     if let Artifact::StartSketchOnFace { face_id: id, .. } = a { | ||||||
|  |                         if *id == face_id.0 { | ||||||
|  |                             return Some(a.clone()); | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                     None | ||||||
|  |                 }); | ||||||
|  |                 let sketch_on_face_source_range = extra_artifact.and_then(|a| match a { | ||||||
|  |                     Artifact::StartSketchOnFace { source_range, .. } => Some(source_range), | ||||||
|  |                     _ => None, | ||||||
|  |                 }); | ||||||
|  |                 let mut wall_artifact = Wall { | ||||||
|  |                     id: face_id, | ||||||
|  |                     seg_id: curve_id, | ||||||
|  |                     edge_cut_edge_ids: Vec::new(), | ||||||
|  |                     sweep_id: path.sweep_id.expect("Expected sweep_id to be Some"), | ||||||
|  |                     path_ids: Vec::new(), | ||||||
|  |                     face_code_ref: CodeRef { | ||||||
|  |                         range, | ||||||
|  |                         path_to_node: path_to_node.clone(), | ||||||
|  |                     }, | ||||||
|  |                 }; | ||||||
|  |  | ||||||
|  |                 if let Some(sketch_on_face_source_range) = sketch_on_face_source_range { | ||||||
|  |                     wall_artifact.face_code_ref = CodeRef { | ||||||
|  |                         range: sketch_on_face_source_range, | ||||||
|  |                         path_to_node: path_to_node.clone(), | ||||||
|  |                     }; | ||||||
|  |                 } | ||||||
|  |                 let wall = Wall { | ||||||
|                     id: face_id, |                     id: face_id, | ||||||
|                     seg_id: curve_id, |                     seg_id: curve_id, | ||||||
|                     edge_cut_edge_ids: Vec::new(), |                     edge_cut_edge_ids: Vec::new(), | ||||||
|                     sweep_id: path_sweep_id, |                     sweep_id: path_sweep_id, | ||||||
|                     path_ids: vec![], |                     path_ids: vec![], | ||||||
|                 })); |                     face_code_ref: CodeRef { | ||||||
|  |                         range, | ||||||
|  |                         path_to_node: path_to_node.clone(), | ||||||
|  |                     }, | ||||||
|  |                 }; | ||||||
|  |                 return_arr.push(Artifact::Wall(wall)); | ||||||
|                 let mut new_seg = seg.clone(); |                 let mut new_seg = seg.clone(); | ||||||
|                 new_seg.surface_id = Some(face_id); |                 new_seg.surface_id = Some(face_id); | ||||||
|                 return_arr.push(Artifact::Segment(new_seg)); |                 return_arr.push(Artifact::Segment(new_seg)); | ||||||
| @ -820,6 +873,7 @@ fn artifacts_to_update( | |||||||
|                     let Some(face_id) = face.face_id.map(ArtifactId::new) else { |                     let Some(face_id) = face.face_id.map(ArtifactId::new) else { | ||||||
|                         continue; |                         continue; | ||||||
|                     }; |                     }; | ||||||
|  |                     let cap = face.clone().cap; | ||||||
|                     let path_sweep_id = path.sweep_id.ok_or_else(|| { |                     let path_sweep_id = path.sweep_id.ok_or_else(|| { | ||||||
|                         KclError::Internal(KclErrorDetails { |                         KclError::Internal(KclErrorDetails { | ||||||
|                             message:format!( |                             message:format!( | ||||||
| @ -828,13 +882,39 @@ fn artifacts_to_update( | |||||||
|                             source_ranges: vec![range], |                             source_ranges: vec![range], | ||||||
|                         }) |                         }) | ||||||
|                     })?; |                     })?; | ||||||
|                     return_arr.push(Artifact::Cap(Cap { |                     let extra_artifact = _exec_artifacts.values().find(|a| { | ||||||
|  |                         if let Artifact::StartSketchOnFace { face_id: id, .. } = a { | ||||||
|  |                             *id == face_id.0 | ||||||
|  |                         } else { | ||||||
|  |                             false | ||||||
|  |                         } | ||||||
|  |                     }); | ||||||
|  |                     let sketch_on_face_source_range = extra_artifact.and_then(|a| match a { | ||||||
|  |                         Artifact::StartSketchOnFace { source_range, .. } => Some(source_range), | ||||||
|  |                         _ => None, | ||||||
|  |                     }); | ||||||
|  |                     let mut cap_artifact = Cap { | ||||||
|                         id: face_id, |                         id: face_id, | ||||||
|                         sub_type, |                         sub_type: match cap { | ||||||
|  |                             ExtrusionFaceCapType::Bottom => CapSubType::Start, | ||||||
|  |                             _ => CapSubType::End, | ||||||
|  |                         }, | ||||||
|                         edge_cut_edge_ids: Vec::new(), |                         edge_cut_edge_ids: Vec::new(), | ||||||
|                         sweep_id: path_sweep_id, |                         sweep_id: path.sweep_id.expect("Expected sweep_id to be Some"), | ||||||
|                         path_ids: Vec::new(), |                         path_ids: Vec::new(), | ||||||
|                     })); |                         face_code_ref: CodeRef { | ||||||
|  |                             range, | ||||||
|  |                             path_to_node: path_to_node.clone(), | ||||||
|  |                         }, | ||||||
|  |                     }; | ||||||
|  |                     if let Some(sketch_on_face_source_range) = sketch_on_face_source_range { | ||||||
|  |                         let range = sketch_on_face_source_range; | ||||||
|  |                         cap_artifact.face_code_ref = CodeRef { | ||||||
|  |                             range: *range, | ||||||
|  |                             path_to_node: path_to_node.clone(), | ||||||
|  |                         }; | ||||||
|  |                     } | ||||||
|  |                     return_arr.push(Artifact::Cap(cap_artifact)); | ||||||
|                     let Some(Artifact::Sweep(sweep)) = artifacts.get(&path_sweep_id) else { |                     let Some(Artifact::Sweep(sweep)) = artifacts.get(&path_sweep_id) else { | ||||||
|                         continue; |                         continue; | ||||||
|                     }; |                     }; | ||||||
|  | |||||||
| @ -11,6 +11,7 @@ use crate::{ | |||||||
|     errors::KclError, |     errors::KclError, | ||||||
|     execution::{ExecState, Metadata, TagEngineInfo, TagIdentifier, UnitLen}, |     execution::{ExecState, Metadata, TagEngineInfo, TagIdentifier, UnitLen}, | ||||||
|     parsing::ast::types::{Node, NodeRef, TagDeclarator, TagNode}, |     parsing::ast::types::{Node, NodeRef, TagDeclarator, TagNode}, | ||||||
|  |     std::shapes::circle_three_point, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| type Point2D = kcmc::shared::Point2d<f64>; | type Point2D = kcmc::shared::Point2d<f64>; | ||||||
| @ -243,9 +244,9 @@ pub struct Plane { | |||||||
|     pub value: PlaneType, |     pub value: PlaneType, | ||||||
|     /// Origin of the plane. |     /// Origin of the plane. | ||||||
|     pub origin: Point3d, |     pub origin: Point3d, | ||||||
|     /// What should the plane’s X axis be? |     /// What should the plane's X axis be? | ||||||
|     pub x_axis: Point3d, |     pub x_axis: Point3d, | ||||||
|     /// What should the plane’s Y axis be? |     /// What should the plane's Y axis be? | ||||||
|     pub y_axis: Point3d, |     pub y_axis: Point3d, | ||||||
|     /// The z-axis (normal). |     /// The z-axis (normal). | ||||||
|     pub z_axis: Point3d, |     pub z_axis: Point3d, | ||||||
| @ -366,9 +367,9 @@ pub struct Face { | |||||||
|     pub artifact_id: ArtifactId, |     pub artifact_id: ArtifactId, | ||||||
|     /// The tag of the face. |     /// The tag of the face. | ||||||
|     pub value: String, |     pub value: String, | ||||||
|     /// What should the face’s X axis be? |     /// What should the face's X axis be? | ||||||
|     pub x_axis: Point3d, |     pub x_axis: Point3d, | ||||||
|     /// What should the face’s Y axis be? |     /// What should the face's Y axis be? | ||||||
|     pub y_axis: Point3d, |     pub y_axis: Point3d, | ||||||
|     /// The z-axis (normal). |     /// The z-axis (normal). | ||||||
|     pub z_axis: Point3d, |     pub z_axis: Point3d, | ||||||
| @ -773,6 +774,19 @@ pub enum Path { | |||||||
|         /// This is used to compute the tangential angle. |         /// This is used to compute the tangential angle. | ||||||
|         ccw: bool, |         ccw: bool, | ||||||
|     }, |     }, | ||||||
|  |     CircleThreePoint { | ||||||
|  |         #[serde(flatten)] | ||||||
|  |         base: BasePath, | ||||||
|  |         /// Point 1 of the circle | ||||||
|  |         #[ts(type = "[number, number]")] | ||||||
|  |         p1: [f64; 2], | ||||||
|  |         /// Point 2 of the circle | ||||||
|  |         #[ts(type = "[number, number]")] | ||||||
|  |         p2: [f64; 2], | ||||||
|  |         /// Point 3 of the circle | ||||||
|  |         #[ts(type = "[number, number]")] | ||||||
|  |         p3: [f64; 2], | ||||||
|  |     }, | ||||||
|     /// A path that is horizontal. |     /// A path that is horizontal. | ||||||
|     Horizontal { |     Horizontal { | ||||||
|         #[serde(flatten)] |         #[serde(flatten)] | ||||||
| @ -815,6 +829,7 @@ enum PathType { | |||||||
|     TangentialArc, |     TangentialArc, | ||||||
|     TangentialArcTo, |     TangentialArcTo, | ||||||
|     Circle, |     Circle, | ||||||
|  |     CircleThreePoint, | ||||||
|     Horizontal, |     Horizontal, | ||||||
|     AngledLineTo, |     AngledLineTo, | ||||||
|     Arc, |     Arc, | ||||||
| @ -827,6 +842,7 @@ impl From<&Path> for PathType { | |||||||
|             Path::TangentialArcTo { .. } => Self::TangentialArcTo, |             Path::TangentialArcTo { .. } => Self::TangentialArcTo, | ||||||
|             Path::TangentialArc { .. } => Self::TangentialArc, |             Path::TangentialArc { .. } => Self::TangentialArc, | ||||||
|             Path::Circle { .. } => Self::Circle, |             Path::Circle { .. } => Self::Circle, | ||||||
|  |             Path::CircleThreePoint { .. } => Self::CircleThreePoint, | ||||||
|             Path::Horizontal { .. } => Self::Horizontal, |             Path::Horizontal { .. } => Self::Horizontal, | ||||||
|             Path::AngledLineTo { .. } => Self::AngledLineTo, |             Path::AngledLineTo { .. } => Self::AngledLineTo, | ||||||
|             Path::Base { .. } => Self::Base, |             Path::Base { .. } => Self::Base, | ||||||
| @ -845,6 +861,7 @@ impl Path { | |||||||
|             Path::TangentialArcTo { base, .. } => base.geo_meta.id, |             Path::TangentialArcTo { base, .. } => base.geo_meta.id, | ||||||
|             Path::TangentialArc { base, .. } => base.geo_meta.id, |             Path::TangentialArc { base, .. } => base.geo_meta.id, | ||||||
|             Path::Circle { base, .. } => base.geo_meta.id, |             Path::Circle { base, .. } => base.geo_meta.id, | ||||||
|  |             Path::CircleThreePoint { base, .. } => base.geo_meta.id, | ||||||
|             Path::Arc { base, .. } => base.geo_meta.id, |             Path::Arc { base, .. } => base.geo_meta.id, | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| @ -858,6 +875,7 @@ impl Path { | |||||||
|             Path::TangentialArcTo { base, .. } => base.tag.clone(), |             Path::TangentialArcTo { base, .. } => base.tag.clone(), | ||||||
|             Path::TangentialArc { base, .. } => base.tag.clone(), |             Path::TangentialArc { base, .. } => base.tag.clone(), | ||||||
|             Path::Circle { base, .. } => base.tag.clone(), |             Path::Circle { base, .. } => base.tag.clone(), | ||||||
|  |             Path::CircleThreePoint { base, .. } => base.tag.clone(), | ||||||
|             Path::Arc { base, .. } => base.tag.clone(), |             Path::Arc { base, .. } => base.tag.clone(), | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| @ -871,6 +889,7 @@ impl Path { | |||||||
|             Path::TangentialArcTo { base, .. } => base, |             Path::TangentialArcTo { base, .. } => base, | ||||||
|             Path::TangentialArc { base, .. } => base, |             Path::TangentialArc { base, .. } => base, | ||||||
|             Path::Circle { base, .. } => base, |             Path::Circle { base, .. } => base, | ||||||
|  |             Path::CircleThreePoint { base, .. } => base, | ||||||
|             Path::Arc { base, .. } => base, |             Path::Arc { base, .. } => base, | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| @ -908,6 +927,15 @@ impl Path { | |||||||
|                 linear_distance(self.get_from(), self.get_to()) |                 linear_distance(self.get_from(), self.get_to()) | ||||||
|             } |             } | ||||||
|             Self::Circle { radius, .. } => 2.0 * std::f64::consts::PI * radius, |             Self::Circle { radius, .. } => 2.0 * std::f64::consts::PI * radius, | ||||||
|  |             Self::CircleThreePoint { .. } => { | ||||||
|  |                 let circle_center = crate::std::utils::calculate_circle_from_3_points([ | ||||||
|  |                     self.get_base().from.into(), | ||||||
|  |                     self.get_base().to.into(), | ||||||
|  |                     self.get_base().to.into(), | ||||||
|  |                 ]); | ||||||
|  |                 let radius = linear_distance(&[circle_center.center.x, circle_center.center.y], &self.get_base().from); | ||||||
|  |                 2.0 * std::f64::consts::PI * radius | ||||||
|  |             } | ||||||
|             Self::Arc { .. } => { |             Self::Arc { .. } => { | ||||||
|                 // TODO: Call engine utils to figure this out. |                 // TODO: Call engine utils to figure this out. | ||||||
|                 linear_distance(self.get_from(), self.get_to()) |                 linear_distance(self.get_from(), self.get_to()) | ||||||
| @ -924,6 +952,7 @@ impl Path { | |||||||
|             Path::TangentialArcTo { base, .. } => Some(base), |             Path::TangentialArcTo { base, .. } => Some(base), | ||||||
|             Path::TangentialArc { base, .. } => Some(base), |             Path::TangentialArc { base, .. } => Some(base), | ||||||
|             Path::Circle { base, .. } => Some(base), |             Path::Circle { base, .. } => Some(base), | ||||||
|  |             Path::CircleThreePoint { base, .. } => Some(base), | ||||||
|             Path::Arc { base, .. } => Some(base), |             Path::Arc { base, .. } => Some(base), | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| @ -943,6 +972,17 @@ impl Path { | |||||||
|                 ccw: *ccw, |                 ccw: *ccw, | ||||||
|                 radius: *radius, |                 radius: *radius, | ||||||
|             }, |             }, | ||||||
|  |             Path::CircleThreePoint { p1, p2, p3, .. } => { | ||||||
|  |                 let circle_center = | ||||||
|  |                     crate::std::utils::calculate_circle_from_3_points([(*p1).into(), (*p2).into(), (*p3).into()]); | ||||||
|  |                 let radius = linear_distance(&[circle_center.center.x, circle_center.center.y], &p1); | ||||||
|  |                 let center_point = [circle_center.center.x, circle_center.center.y]; | ||||||
|  |                 GetTangentialInfoFromPathsResult::Circle { | ||||||
|  |                     center: center_point, | ||||||
|  |                     ccw: true, | ||||||
|  |                     radius, | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|             Path::ToPoint { .. } | Path::Horizontal { .. } | Path::AngledLineTo { .. } | Path::Base { .. } => { |             Path::ToPoint { .. } | Path::Horizontal { .. } | Path::AngledLineTo { .. } | Path::Base { .. } => { | ||||||
|                 let base = self.get_base(); |                 let base = self.get_base(); | ||||||
|                 GetTangentialInfoFromPathsResult::PreviousPoint(base.from) |                 GetTangentialInfoFromPathsResult::PreviousPoint(base.from) | ||||||
|  | |||||||
| @ -243,7 +243,8 @@ pub(crate) async fn do_post_extrude( | |||||||
|                     Path::Arc { .. } |                     Path::Arc { .. } | ||||||
|                     | Path::TangentialArc { .. } |                     | Path::TangentialArc { .. } | ||||||
|                     | Path::TangentialArcTo { .. } |                     | Path::TangentialArcTo { .. } | ||||||
|                     | Path::Circle { .. } => { |                     | Path::Circle { .. } | ||||||
|  |                     | Path::CircleThreePoint { .. } => { | ||||||
|                         let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc { |                         let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc { | ||||||
|                             face_id: *actual_face_id, |                             face_id: *actual_face_id, | ||||||
|                             tag: path.get_base().tag.clone(), |                             tag: path.get_base().tag.clone(), | ||||||
|  | |||||||
| @ -181,6 +181,9 @@ pub async fn circle_three_point(exec_state: &mut ExecState, args: Args) -> Resul | |||||||
|         tag = {docs = "Identifier for the circle to reference elsewhere."}, |         tag = {docs = "Identifier for the circle to reference elsewhere."}, | ||||||
|     } |     } | ||||||
| }] | }] | ||||||
|  |  | ||||||
|  | /// Similar to inner_circle, but needs to retain 3-point information in the | ||||||
|  | /// path so it can be used for other features, otherwise it's lost. | ||||||
| async fn inner_circle_three_point( | async fn inner_circle_three_point( | ||||||
|     p1: [f64; 2], |     p1: [f64; 2], | ||||||
|     p2: [f64; 2], |     p2: [f64; 2], | ||||||
| @ -191,18 +194,69 @@ async fn inner_circle_three_point( | |||||||
|     args: Args, |     args: Args, | ||||||
| ) -> Result<Sketch, KclError> { | ) -> Result<Sketch, KclError> { | ||||||
|     let center = calculate_circle_center(p1, p2, p3); |     let center = calculate_circle_center(p1, p2, p3); | ||||||
|     inner_circle( |  | ||||||
|         CircleData { |  | ||||||
|             center, |  | ||||||
|     // It can be the distance to any of the 3 points - they all lay on the circumference. |     // It can be the distance to any of the 3 points - they all lay on the circumference. | ||||||
|             radius: distance(center.into(), p2.into()), |     let radius = distance(center.into(), p2.into()); | ||||||
|         }, |  | ||||||
|         sketch_surface_or_group, |     let sketch_surface = match sketch_surface_or_group { | ||||||
|         tag, |         SketchOrSurface::SketchSurface(surface) => surface, | ||||||
|  |         SketchOrSurface::Sketch(group) => group.on, | ||||||
|  |     }; | ||||||
|  |     let sketch = crate::std::sketch::inner_start_profile_at( | ||||||
|  |         [center[0] + radius, center[1]], | ||||||
|  |         sketch_surface, | ||||||
|  |         None, | ||||||
|         exec_state, |         exec_state, | ||||||
|         args, |         args.clone(), | ||||||
|     ) |     ) | ||||||
|     .await |     .await?; | ||||||
|  |  | ||||||
|  |     let from = [center[0] + radius, center[1]]; | ||||||
|  |     let angle_start = Angle::zero(); | ||||||
|  |     let angle_end = Angle::turn(); | ||||||
|  |  | ||||||
|  |     let id = exec_state.next_uuid(); | ||||||
|  |  | ||||||
|  |     args.batch_modeling_cmd( | ||||||
|  |         id, | ||||||
|  |         ModelingCmd::from(mcmd::ExtendPath { | ||||||
|  |             path: sketch.id.into(), | ||||||
|  |             segment: PathSegment::Arc { | ||||||
|  |                 start: angle_start, | ||||||
|  |                 end: angle_end, | ||||||
|  |                 center: KPoint2d::from(center).map(LengthUnit), | ||||||
|  |                 radius: radius.into(), | ||||||
|  |                 relative: false, | ||||||
|  |             }, | ||||||
|  |         }), | ||||||
|  |     ) | ||||||
|  |     .await?; | ||||||
|  |  | ||||||
|  |     let current_path = Path::CircleThreePoint { | ||||||
|  |         base: BasePath { | ||||||
|  |             from, | ||||||
|  |             to: from, | ||||||
|  |             tag: tag.clone(), | ||||||
|  |             geo_meta: GeoMeta { | ||||||
|  |                 id, | ||||||
|  |                 metadata: args.source_range.into(), | ||||||
|  |             }, | ||||||
|  |         }, | ||||||
|  |         p1, | ||||||
|  |         p2, | ||||||
|  |         p3, | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     let mut new_sketch = sketch.clone(); | ||||||
|  |     if let Some(tag) = &tag { | ||||||
|  |         new_sketch.add_tag(tag, ¤t_path); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     new_sketch.paths.push(current_path); | ||||||
|  |  | ||||||
|  |     args.batch_modeling_cmd(id, ModelingCmd::from(mcmd::ClosePath { path_id: new_sketch.id })) | ||||||
|  |         .await?; | ||||||
|  |  | ||||||
|  |     Ok(new_sketch) | ||||||
| } | } | ||||||
|  |  | ||||||
| /// Type of the polygon | /// Type of the polygon | ||||||
|  | |||||||
