Compare commits
	
		
			128 Commits
		
	
	
		
			nightly-v2
			...
			franknoiro
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| df8977407c | |||
| 42016b7335 | |||
| 09f3e6e170 | |||
| 3747db01b8 | |||
| bf1a42f6c0 | |||
| b4d1a36bd9 | |||
| b937934834 | |||
| 0208eecefa | |||
| 9999e4e62f | |||
| b390e3e083 | |||
| 9a89926749 | |||
| d7f834f23d | |||
| 500d92d1fc | |||
| bd10c65bdb | |||
| 039092ec24 | |||
| 8a920b6cb7 | |||
| c1db093e0f | |||
| 5f8ae22b04 | |||
| dc7b901eb9 | |||
| 60dcf9d79e | |||
| 520f899ad4 | |||
| 41340c8bf0 | |||
| a78ec6cd17 | |||
| de526ae36e | |||
| 4a8e582865 | |||
| 1854064274 | |||
| 4a8897be4b | |||
| 83f458fc36 | |||
| a686fe914b | |||
| cba2349064 | |||
| 5ae92bcf5c | |||
| ad8e306bdb | |||
| 508e1c919c | |||
| 58ec6100c4 | |||
| 11eceefedf | |||
| 11a678df5b | |||
| 22c0003eb1 | |||
| 8f0a40ba6e | |||
| 89b0ccb090 | |||
| 5d22308ad2 | |||
| 5580631c8f | |||
| e1494c9f16 | |||
| 4a0d852a3c | |||
| b213834316 | |||
| 1d8348c2cf | |||
| 2227287c9d | |||
| 680fc30682 | |||
| 40fb6a44d3 | |||
| 5713bfd9fa | |||
| 77902d550f | |||
| db895d6801 | |||
| 3e1f8584ea | |||
| 2501a98cd9 | |||
| e60b0e64ba | |||
| 3379cc489a | |||
| a8b7328f65 | |||
| ab6995bde3 | |||
| 6df5e70186 | |||
| 6b1cc36911 | |||
| 6a16e47491 | |||
| f4f0533179 | |||
| 6360b8acac | |||
| 064a41d675 | |||
| e075622a7f | |||
| 7a7929211a | |||
| 1f5f42963d | |||
| 235e6a1056 | |||
| 09cfbc1837 | |||
| 319029235c | |||
| a7f4b0f037 | |||
| d3afa38bd5 | |||
| 45416df215 | |||
| ee54cdd27a | |||
| 84ae567f37 | |||
| f94671f1bb | |||
| bcf3790266 | |||
| 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 | 
							
								
								
									
										16260
									
								
								docs/kcl/std.json
									
									
									
									
									
								
							
							
						
						| @ -19,8 +19,8 @@ A face. | ||||
| | `id` |`string`| The id of the face. | No | | ||||
| | `artifactId` |[`ArtifactId`](/docs/kcl/types/ArtifactId)| The artifact ID. | No | | ||||
| | `value` |`string`| The tag of the face. | No | | ||||
| | `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the face’s X axis be? | No | | ||||
| | `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the face’s Y axis be? | No | | ||||
| | `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the face's X axis be? | No | | ||||
| | `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the face's Y axis be? | No | | ||||
| | `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No | | ||||
| | `solid` |[`Solid`](/docs/kcl/types/Solid)| The solid the face is on. | No | | ||||
| | `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A face. | No | | ||||
|  | ||||
| @ -98,6 +98,29 @@ a complete arc | ||||
| | `__geoMeta` |[`GeoMeta`](/docs/kcl/types/GeoMeta)| Metadata. | No | | ||||
|  | ||||
|  | ||||
| ---- | ||||
| A base path. | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `type` |enum: `CircleThreePoint`|  | No | | ||||
| | `p1` |`[number, number]`| Point 1 of the circle | No | | ||||
| | `p2` |`[number, number]`| Point 2 of the circle | No | | ||||
| | `p3` |`[number, number]`| Point 3 of the circle | No | | ||||
| | `from` |`[number, number]`| The from point. | No | | ||||
| | `to` |`[number, number]`| The to point. | No | | ||||
| | `tag` |[`TagDeclarator`](/docs/kcl/types#tag-declaration)| The tag of the path. | No | | ||||
| | `__geoMeta` |[`GeoMeta`](/docs/kcl/types/GeoMeta)| Metadata. | No | | ||||
|  | ||||
|  | ||||
| ---- | ||||
| A path that is horizontal. | ||||
|  | ||||
|  | ||||
| @ -20,8 +20,8 @@ A plane. | ||||
| | `artifactId` |[`ArtifactId`](/docs/kcl/types/ArtifactId)| The artifact ID. | No | | ||||
| | `value` |[`PlaneType`](/docs/kcl/types/PlaneType)| A plane. | No | | ||||
| | `origin` |[`Point3d`](/docs/kcl/types/Point3d)| Origin of the plane. | No | | ||||
| | `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane’s X axis be? | No | | ||||
| | `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane’s Y axis be? | No | | ||||
| | `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane's X axis be? | No | | ||||
| | `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane's Y axis be? | No | | ||||
| | `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No | | ||||
| | `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A plane. | No | | ||||
| | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`|  | No | | ||||
|  | ||||
| @ -29,8 +29,8 @@ A plane. | ||||
| | `artifactId` |[`ArtifactId`](/docs/kcl/types/ArtifactId)| The artifact ID. | No | | ||||
| | `value` |[`PlaneType`](/docs/kcl/types/PlaneType)| A sketch type. | No | | ||||
| | `origin` |[`Point3d`](/docs/kcl/types/Point3d)| Origin of the plane. | No | | ||||
| | `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane’s X axis be? | No | | ||||
| | `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane’s Y axis be? | No | | ||||
| | `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane's X axis be? | No | | ||||
| | `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane's Y axis be? | No | | ||||
| | `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No | | ||||
| | `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A sketch type. | No | | ||||
| | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`|  | No | | ||||
| @ -53,8 +53,8 @@ A face. | ||||
| | `id` |`string`| The id of the face. | No | | ||||
| | `artifactId` |[`ArtifactId`](/docs/kcl/types/ArtifactId)| The artifact ID. | No | | ||||
| | `value` |`string`| The tag of the face. | No | | ||||
| | `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the face’s X axis be? | No | | ||||
| | `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the face’s Y axis be? | No | | ||||
| | `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the face's X axis be? | No | | ||||
| | `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the face's Y axis be? | No | | ||||
| | `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No | | ||||
| | `solid` |[`Solid`](/docs/kcl/types/Solid)| The solid the face is on. | No | | ||||
| | `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A sketch type. | No | | ||||
|  | ||||
| @ -54,23 +54,26 @@ async function doBasicSketch( | ||||
|   const startXPx = 600 | ||||
|   await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) | ||||
|   if (openPanes.includes('code')) { | ||||
|     await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ') | ||||
|   |> startProfileAt(${commonPoints.startAt}, %)`) | ||||
|     await expect(u.codeLocator).toContainText( | ||||
|       `sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${commonPoints.startAt}, sketch001)` | ||||
|     ) | ||||
|   } | ||||
|   await page.waitForTimeout(500) | ||||
|   await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10) | ||||
|   await page.waitForTimeout(500) | ||||
|  | ||||
|   if (openPanes.includes('code')) { | ||||
|     await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ') | ||||
|   |> startProfileAt(${commonPoints.startAt}, %) | ||||
|     await expect(u.codeLocator) | ||||
|       .toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${commonPoints.startAt}, sketch001) | ||||
|   |> xLine(${commonPoints.num1}, %)`) | ||||
|   } | ||||
|   await page.waitForTimeout(500) | ||||
|   await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20) | ||||
|   if (openPanes.includes('code')) { | ||||
|     await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ') | ||||
|   |> startProfileAt(${commonPoints.startAt}, %) | ||||
|     await expect(u.codeLocator) | ||||
|       .toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${ | ||||
|       commonPoints.startAt | ||||
|     }, sketch001) | ||||
|   |> xLine(${commonPoints.num1}, %) | ||||
|   |> yLine(${commonPoints.num1 + 0.01}, %)`) | ||||
|   } else { | ||||
| @ -79,8 +82,10 @@ async function doBasicSketch( | ||||
|   await page.waitForTimeout(200) | ||||
|   await page.mouse.click(startXPx, 500 - PUR * 20) | ||||
|   if (openPanes.includes('code')) { | ||||
|     await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ') | ||||
|   |> startProfileAt(${commonPoints.startAt}, %) | ||||
|     await expect(u.codeLocator) | ||||
|       .toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${ | ||||
|       commonPoints.startAt | ||||
|     }, sketch001) | ||||
|   |> xLine(${commonPoints.num1}, %) | ||||
|   |> yLine(${commonPoints.num1 + 0.01}, %) | ||||
|   |> xLine(${commonPoints.num2 * -1}, %)`) | ||||
| @ -137,8 +142,10 @@ async function doBasicSketch( | ||||
|  | ||||
|   // Open the code pane. | ||||
|   await u.openKclCodePanel() | ||||
|   await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ') | ||||
|   |> startProfileAt(${commonPoints.startAt}, %) | ||||
|   await expect(u.codeLocator) | ||||
|     .toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${ | ||||
|     commonPoints.startAt | ||||
|   }, sketch001) | ||||
|   |> xLine(${commonPoints.num1}, %, $seg01) | ||||
|   |> yLine(${commonPoints.num1 + 0.01}, %) | ||||
|   |> xLine(-segLen(seg01), %)`) | ||||
|  | ||||
| @ -43,8 +43,7 @@ test.describe( | ||||
|         }, | ||||
|       } | ||||
|  | ||||
|       const code = `sketch001 = startSketchOn('${plane}') | ||||
|     |> startProfileAt([0.9, -1.22], %)` | ||||
|       const code = `sketch001 = startSketchOn('${plane}')profile001 = startProfileAt([0.9, -1.22], sketch001)` | ||||
|  | ||||
|       await u.openDebugPanel() | ||||
|  | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| import type { Page, Locator } from '@playwright/test' | ||||
| import { expect } from '@playwright/test' | ||||
| import { uuidv4 } from 'lib/utils' | ||||
| import { isArray, uuidv4 } from 'lib/utils' | ||||
| import { | ||||
|   closeDebugPanel, | ||||
|   doAndWaitForImageDiff, | ||||
| @ -9,13 +9,15 @@ import { | ||||
|   sendCustomCmd, | ||||
| } from '../test-utils' | ||||
|  | ||||
| type mouseParams = { | ||||
| type MouseParams = { | ||||
|   pixelDiff?: number | ||||
|   shouldDbClick?: boolean | ||||
|   delay?: number | ||||
| } | ||||
| type mouseDragToParams = mouseParams & { | ||||
| type MouseDragToParams = MouseParams & { | ||||
|   fromPoint: { x: number; y: number } | ||||
| } | ||||
| type mouseDragFromParams = mouseParams & { | ||||
| type MouseDragFromParams = MouseParams & { | ||||
|   toPoint: { x: number; y: number } | ||||
| } | ||||
|  | ||||
| @ -26,12 +28,12 @@ type SceneSerialised = { | ||||
|   } | ||||
| } | ||||
|  | ||||
| type ClickHandler = (clickParams?: mouseParams) => Promise<void | boolean> | ||||
| type MoveHandler = (moveParams?: mouseParams) => Promise<void | boolean> | ||||
| type DblClickHandler = (clickParams?: mouseParams) => Promise<void | boolean> | ||||
| type DragToHandler = (dragParams: mouseDragToParams) => Promise<void | boolean> | ||||
| type ClickHandler = (clickParams?: MouseParams) => Promise<void | boolean> | ||||
| type MoveHandler = (moveParams?: MouseParams) => Promise<void | boolean> | ||||
| type DblClickHandler = (clickParams?: MouseParams) => Promise<void | boolean> | ||||
| type DragToHandler = (dragParams: MouseDragToParams) => Promise<void | boolean> | ||||
| type DragFromHandler = ( | ||||
|   dragParams: mouseDragFromParams | ||||
|   dragParams: MouseDragFromParams | ||||
| ) => Promise<void | boolean> | ||||
|  | ||||
| export class SceneFixture { | ||||
| @ -77,17 +79,26 @@ export class SceneFixture { | ||||
|     { steps }: { steps: number } = { steps: 20 } | ||||
|   ): [ClickHandler, MoveHandler, DblClickHandler] => | ||||
|     [ | ||||
|       (clickParams?: mouseParams) => { | ||||
|       (clickParams?: MouseParams) => { | ||||
|         if (clickParams?.pixelDiff) { | ||||
|           return doAndWaitForImageDiff( | ||||
|             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 | ||||
|           ) | ||||
|         } | ||||
|         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) { | ||||
|           return doAndWaitForImageDiff( | ||||
|             this.page, | ||||
| @ -97,7 +108,7 @@ export class SceneFixture { | ||||
|         } | ||||
|         return this.page.mouse.move(x, y, { steps }) | ||||
|       }, | ||||
|       (clickParams?: mouseParams) => { | ||||
|       (clickParams?: MouseParams) => { | ||||
|         if (clickParams?.pixelDiff) { | ||||
|           return doAndWaitForImageDiff( | ||||
|             this.page, | ||||
| @ -114,7 +125,7 @@ export class SceneFixture { | ||||
|     { steps }: { steps: number } = { steps: 20 } | ||||
|   ): [DragToHandler, DragFromHandler] => | ||||
|     [ | ||||
|       (dragToParams: mouseDragToParams) => { | ||||
|       (dragToParams: MouseDragToParams) => { | ||||
|         if (dragToParams?.pixelDiff) { | ||||
|           return doAndWaitForImageDiff( | ||||
|             this.page, | ||||
| @ -131,7 +142,7 @@ export class SceneFixture { | ||||
|           targetPosition: { x, y }, | ||||
|         }) | ||||
|       }, | ||||
|       (dragFromParams: mouseDragFromParams) => { | ||||
|       (dragFromParams: MouseDragFromParams) => { | ||||
|         if (dragFromParams?.pixelDiff) { | ||||
|           return doAndWaitForImageDiff( | ||||
|             this.page, | ||||
| @ -219,7 +230,7 @@ export class SceneFixture { | ||||
|   } | ||||
|  | ||||
|   expectPixelColor = async ( | ||||
|     colour: [number, number, number], | ||||
|     colour: [number, number, number] | [number, number, number][], | ||||
|     coords: { x: number; y: 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 isArray(colour[0]) | ||||
| } | ||||
|  | ||||
| export async function expectPixelColor( | ||||
|   page: Page, | ||||
|   colour: [number, number, number], | ||||
|   colour: [number, number, number] | [number, number, number][], | ||||
|   coords: { x: number; y: number }, | ||||
|   diff: number | ||||
| ) { | ||||
|   let finalValue = colour | ||||
|   await expect | ||||
|     .poll(async () => { | ||||
|       const pixel = (await getPixelRGBs(page)(coords, 1))[0] | ||||
|       if (!pixel) return null | ||||
|       finalValue = pixel | ||||
|       return pixel.every( | ||||
|         (channel, index) => Math.abs(channel - colour[index]) < diff | ||||
|       ) | ||||
|     }) | ||||
|     .poll( | ||||
|       async () => { | ||||
|         const pixel = (await getPixelRGBs(page)(coords, 1))[0] | ||||
|         if (!pixel) return null | ||||
|         finalValue = pixel | ||||
|         if (!isColourArray(colour)) { | ||||
|           return pixel.every( | ||||
|             (channel, index) => Math.abs(channel - colour[index]) < diff | ||||
|           ) | ||||
|         } | ||||
|         return colour.some((c) => | ||||
|           c.every((channel, index) => Math.abs(pixel[index] - channel) < diff) | ||||
|         ) | ||||
|       }, | ||||
|       { timeout: 10_000 } | ||||
|     ) | ||||
|     .toBeTruthy() | ||||
|     .catch((cause) => { | ||||
|       throw new Error( | ||||
|  | ||||
| @ -23,7 +23,10 @@ export class ToolbarFixture { | ||||
|   helixButton!: Locator | ||||
|   startSketchBtn!: Locator | ||||
|   lineBtn!: Locator | ||||
|   tangentialArcBtn!: Locator | ||||
|   circleBtn!: Locator | ||||
|   rectangleBtn!: Locator | ||||
|   lengthConstraintBtn!: Locator | ||||
|   exitSketchBtn!: Locator | ||||
|   editSketchBtn!: Locator | ||||
|   fileTreeBtn!: Locator | ||||
| @ -53,7 +56,10 @@ export class ToolbarFixture { | ||||
|     this.helixButton = page.getByTestId('helix') | ||||
|     this.startSketchBtn = page.getByTestId('sketch') | ||||
|     this.lineBtn = page.getByTestId('line') | ||||
|     this.tangentialArcBtn = page.getByTestId('tangential-arc') | ||||
|     this.circleBtn = page.getByTestId('circle-center') | ||||
|     this.rectangleBtn = page.getByTestId('corner-rectangle') | ||||
|     this.lengthConstraintBtn = page.getByTestId('constraint-length') | ||||
|     this.exitSketchBtn = page.getByTestId('sketch-exit') | ||||
|     this.editSketchBtn = page.getByText('Edit Sketch') | ||||
|     this.fileTreeBtn = page.locator('[id="files-button-holder"]') | ||||
| @ -119,6 +125,25 @@ export class ToolbarFixture { | ||||
|       await expect(this.exeIndicator).toBeVisible({ timeout: 15_000 }) | ||||
|     } | ||||
|   } | ||||
|   selectCenterRectangle = async () => { | ||||
|     await this.page | ||||
|       .getByRole('button', { name: 'caret down Corner rectangle:' }) | ||||
|       .click() | ||||
|     await expect( | ||||
|       this.page.getByTestId('dropdown-center-rectangle') | ||||
|     ).toBeVisible() | ||||
|     await this.page.getByTestId('dropdown-center-rectangle').click() | ||||
|   } | ||||
|  | ||||
|   selectCircleThreePoint = async () => { | ||||
|     await this.page | ||||
|       .getByRole('button', { name: 'caret down Center circle:' }) | ||||
|       .click() | ||||
|     await expect( | ||||
|       this.page.getByTestId('dropdown-circle-three-points') | ||||
|     ).toBeVisible() | ||||
|     await this.page.getByTestId('dropdown-circle-three-points').click() | ||||
|   } | ||||
|  | ||||
|   async closePane(paneId: SidebarType) { | ||||
|     return closePane(this.page, paneId + SIDEBAR_BUTTON_SUFFIX) | ||||
|  | ||||
| @ -219,18 +219,13 @@ test.describe('Point-and-click tests', { tag: ['@skipWin'] }, () => { | ||||
|  | ||||
|         afterChamferSelectSnippet: | ||||
|           'sketch002 = startSketchOn(extrude001, seg03)', | ||||
|         afterRectangle1stClickSnippet: 'startProfileAt([205.96, 254.59], %)', | ||||
|         afterRectangle2ndClickSnippet: `angledLine([0, 11.39], %, $rectangleSegmentA002) | ||||
|     |> angledLine([ | ||||
|          segAng(rectangleSegmentA002) - 90, | ||||
|          105.26 | ||||
|        ], %, $rectangleSegmentB001) | ||||
|     |> angledLine([ | ||||
|          segAng(rectangleSegmentA002), | ||||
|          -segLen(rectangleSegmentA002) | ||||
|        ], %, $rectangleSegmentC001) | ||||
|     |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) | ||||
|     |> close()`, | ||||
|         afterRectangle1stClickSnippet: | ||||
|           'startProfileAt([205.96, 254.59], sketch002)', | ||||
|         afterRectangle2ndClickSnippet: `angledLine([0,11.39],%,$rectangleSegmentA002) | ||||
|         |>angledLine([segAng(rectangleSegmentA002)-90,105.26],%) | ||||
|         |>angledLine([segAng(rectangleSegmentA002),-segLen(rectangleSegmentA002)],%) | ||||
|         |>line(endAbsolute=[profileStartX(%),profileStartY(%)]) | ||||
|         |>close()`, | ||||
|       }) | ||||
|  | ||||
|       await sketchOnAChamfer({ | ||||
| @ -251,19 +246,15 @@ test.describe('Point-and-click tests', { tag: ['@skipWin'] }, () => { | ||||
|  | ||||
|         afterChamferSelectSnippet: | ||||
|           'sketch003 = startSketchOn(extrude001, seg04)', | ||||
|         afterRectangle1stClickSnippet: 'startProfileAt([-209.64, 255.28], %)', | ||||
|         afterRectangle2ndClickSnippet: `angledLine([0, 11.56], %, $rectangleSegmentA003) | ||||
|     |> angledLine([ | ||||
|          segAng(rectangleSegmentA003) - 90, | ||||
|          106.84 | ||||
|        ], %, $rectangleSegmentB002) | ||||
|     |> angledLine([ | ||||
|          segAng(rectangleSegmentA003), | ||||
|          -segLen(rectangleSegmentA003) | ||||
|        ], %, $rectangleSegmentC002) | ||||
|     |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) | ||||
|     |> close()`, | ||||
|         afterRectangle1stClickSnippet: | ||||
|           'startProfileAt([-209.64, 255.28], sketch003)', | ||||
|         afterRectangle2ndClickSnippet: `angledLine([0,11.56],%,$rectangleSegmentA003) | ||||
|         |>angledLine([segAng(rectangleSegmentA003)-90,106.84],%) | ||||
|         |>angledLine([segAng(rectangleSegmentA003),-segLen(rectangleSegmentA003)],%) | ||||
|         |>line(endAbsolute=[profileStartX(%),profileStartY(%)]) | ||||
|         |>close()`, | ||||
|       }) | ||||
|  | ||||
|       await sketchOnAChamfer({ | ||||
|         clickCoords: { x: 677, y: 87 }, | ||||
|         cameraPos: { x: -6200, y: 1500, z: 6200 }, | ||||
| @ -276,19 +267,14 @@ test.describe('Point-and-click tests', { tag: ['@skipWin'] }, () => { | ||||
|          ] | ||||
|        }, %)`, | ||||
|         afterChamferSelectSnippet: | ||||
|           'sketch003 = startSketchOn(extrude001, seg04)', | ||||
|         afterRectangle1stClickSnippet: 'startProfileAt([75.8, 317.2], %)', | ||||
|         afterRectangle2ndClickSnippet: `angledLine([0, 11.56], %, $rectangleSegmentA003) | ||||
|     |> angledLine([ | ||||
|          segAng(rectangleSegmentA003) - 90, | ||||
|          106.84 | ||||
|        ], %, $rectangleSegmentB002) | ||||
|     |> angledLine([ | ||||
|          segAng(rectangleSegmentA003), | ||||
|          -segLen(rectangleSegmentA003) | ||||
|        ], %, $rectangleSegmentC002) | ||||
|     |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) | ||||
|     |> close()`, | ||||
|           'sketch004 = startSketchOn(extrude001, seg05)', | ||||
|         afterRectangle1stClickSnippet: | ||||
|           'startProfileAt([82.57, 322.96], sketch004)', | ||||
|         afterRectangle2ndClickSnippet: `angledLine([0,11.16],%,$rectangleSegmentA004) | ||||
|         |>angledLine([segAng(rectangleSegmentA004)-90,103.07],%) | ||||
|         |>angledLine([segAng(rectangleSegmentA004),-segLen(rectangleSegmentA004)],%) | ||||
|         |>line(endAbsolute=[profileStartX(%),profileStartY(%)]) | ||||
|         |>close()`, | ||||
|       }) | ||||
|       /// last one | ||||
|       await sketchOnAChamfer({ | ||||
| @ -301,104 +287,98 @@ test.describe('Point-and-click tests', { tag: ['@skipWin'] }, () => { | ||||
|        }, %)`, | ||||
|         afterChamferSelectSnippet: | ||||
|           'sketch005 = startSketchOn(extrude001, seg06)', | ||||
|         afterRectangle1stClickSnippet: 'startProfileAt([-23.43, 19.69], %)', | ||||
|         afterRectangle2ndClickSnippet: `angledLine([0, 9.1], %, $rectangleSegmentA005) | ||||
|  | ||||
|     |> angledLine([ | ||||
|          segAng(rectangleSegmentA005) - 90, | ||||
|          84.07 | ||||
|        ], %, $rectangleSegmentB004) | ||||
|     |> angledLine([ | ||||
|          segAng(rectangleSegmentA005), | ||||
|          -segLen(rectangleSegmentA005) | ||||
|        ], %, $rectangleSegmentC004) | ||||
|     |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) | ||||
|     |> close()`, | ||||
|         afterRectangle1stClickSnippet: | ||||
|           'startProfileAt([-23.43, 19.69], sketch005)', | ||||
|         afterRectangle2ndClickSnippet: `angledLine([0,9.1],%,$rectangleSegmentA005) | ||||
|         |>angledLine([segAng(rectangleSegmentA005)-90,84.07],%) | ||||
|         |>angledLine([segAng(rectangleSegmentA005),-segLen(rectangleSegmentA005)],%) | ||||
|         |>line(endAbsolute=[profileStartX(%),profileStartY(%)]) | ||||
|         |>close()`, | ||||
|       }) | ||||
|  | ||||
|       await test.step('verify at the end of the test that final code is what is expected', async () => { | ||||
|         await editor.expectEditor.toContain( | ||||
|           `sketch001 = startSketchOn('XZ') | ||||
|   |> startProfileAt([75.8, 317.2], %) // [$startCapTag, $EndCapTag] | ||||
|   |> angledLine([0, 268.43], %, $rectangleSegmentA001) | ||||
|   |> angledLine([ | ||||
|        segAng(rectangleSegmentA001) - 90, | ||||
|        217.26 | ||||
|      ], %, $seg01) | ||||
|   |> angledLine([ | ||||
|        segAng(rectangleSegmentA001), | ||||
|        -segLen(rectangleSegmentA001) | ||||
|      ], %, $yo) | ||||
|   |> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $seg02) | ||||
|   |> close() | ||||
| extrude001 = extrude(sketch001, length = 100) | ||||
|   |> chamfer({ | ||||
|        length = 30, | ||||
|        tags = [getOppositeEdge(seg01)] | ||||
|      }, %, $seg03) | ||||
|   |> chamfer({ length = 30, tags = [seg01] }, %, $seg04) | ||||
|   |> chamfer({ | ||||
|        length = 30, | ||||
|        tags = [getNextAdjacentEdge(seg02)] | ||||
|      }, %, $seg05) | ||||
|   |> chamfer({ | ||||
|        length = 30, | ||||
|        tags = [getNextAdjacentEdge(yo)] | ||||
|      }, %, $seg06) | ||||
| sketch005 = startSketchOn(extrude001, seg06) | ||||
| profile004 = startProfileAt([-23.43, 19.69], sketch005) | ||||
|   |> angledLine([0, 9.1], %, $rectangleSegmentA005) | ||||
|   |> angledLine([ | ||||
|        segAng(rectangleSegmentA005) - 90, | ||||
|        84.07 | ||||
|      ], %) | ||||
|   |> angledLine([ | ||||
|        segAng(rectangleSegmentA005), | ||||
|        -segLen(rectangleSegmentA005) | ||||
|      ], %) | ||||
|   |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) | ||||
|   |> close() | ||||
| sketch004 = startSketchOn(extrude001, seg05) | ||||
| profile003 = startProfileAt([82.57, 322.96], sketch004) | ||||
|   |> angledLine([0, 11.16], %, $rectangleSegmentA004) | ||||
|   |> angledLine([ | ||||
|        segAng(rectangleSegmentA004) - 90, | ||||
|        103.07 | ||||
|      ], %) | ||||
|   |> angledLine([ | ||||
|        segAng(rectangleSegmentA004), | ||||
|        -segLen(rectangleSegmentA004) | ||||
|      ], %) | ||||
|   |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) | ||||
|   |> close() | ||||
| sketch003 = startSketchOn(extrude001, seg04) | ||||
| profile002 = startProfileAt([-209.64, 255.28], sketch003) | ||||
|   |> angledLine([0, 11.56], %, $rectangleSegmentA003) | ||||
|   |> angledLine([ | ||||
|        segAng(rectangleSegmentA003) - 90, | ||||
|        106.84 | ||||
|      ], %) | ||||
|   |> angledLine([ | ||||
|        segAng(rectangleSegmentA003), | ||||
|        -segLen(rectangleSegmentA003) | ||||
|      ], %) | ||||
|   |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) | ||||
|   |> close() | ||||
| sketch002 = startSketchOn(extrude001, seg03) | ||||
| profile001 = startProfileAt([205.96, 254.59], sketch002) | ||||
|   |> angledLine([0, 11.39], %, $rectangleSegmentA002) | ||||
|   |> angledLine([ | ||||
|        segAng(rectangleSegmentA002) - 90, | ||||
|        105.26 | ||||
|      ], %) | ||||
|   |> angledLine([ | ||||
|        segAng(rectangleSegmentA002), | ||||
|        -segLen(rectangleSegmentA002) | ||||
|      ], %) | ||||
|   |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) | ||||
|   |> close() | ||||
|  | ||||
|       |> startProfileAt([75.8, 317.2], %) // [$startCapTag, $EndCapTag] | ||||
|       |> angledLine([0, 268.43], %, $rectangleSegmentA001) | ||||
|       |> angledLine([ | ||||
|            segAng(rectangleSegmentA001) - 90, | ||||
|            217.26 | ||||
|          ], %, $seg01) | ||||
|       |> angledLine([ | ||||
|            segAng(rectangleSegmentA001), | ||||
|            -segLen(rectangleSegmentA001) | ||||
|          ], %, $yo) | ||||
|       |> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $seg02) | ||||
|       |> close() | ||||
|     extrude001 = extrude(sketch001, length = 100) | ||||
|       |> chamfer({ | ||||
|            length = 30, | ||||
|            tags = [getOppositeEdge(seg01)] | ||||
|          }, %, $seg03) | ||||
|       |> chamfer({ length = 30, tags = [seg01] }, %, $seg04) | ||||
|       |> chamfer({ | ||||
|            length = 30, | ||||
|            tags = [getNextAdjacentEdge(seg02)] | ||||
|          }, %, $seg05) | ||||
|       |> chamfer({ | ||||
|            length = 30, | ||||
|            tags = [getNextAdjacentEdge(yo)] | ||||
|          }, %, $seg06) | ||||
|     sketch005 = startSketchOn(extrude001, seg06) | ||||
|       |> startProfileAt([-23.43,19.69], %) | ||||
|       |> angledLine([0, 9.1], %, $rectangleSegmentA005) | ||||
|       |> angledLine([ | ||||
|            segAng(rectangleSegmentA005) - 90, | ||||
|            84.07 | ||||
|          ], %, $rectangleSegmentB004) | ||||
|       |> angledLine([ | ||||
|            segAng(rectangleSegmentA005), | ||||
|            -segLen(rectangleSegmentA005) | ||||
|          ], %, $rectangleSegmentC004) | ||||
|       |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) | ||||
|       |> close() | ||||
|     sketch004 = startSketchOn(extrude001, seg05) | ||||
|       |> startProfileAt([82.57,322.96], %) | ||||
|       |> angledLine([0, 11.16], %, $rectangleSegmentA004) | ||||
|       |> angledLine([ | ||||
|            segAng(rectangleSegmentA004) - 90, | ||||
|            103.07 | ||||
|          ], %, $rectangleSegmentB003) | ||||
|       |> angledLine([ | ||||
|            segAng(rectangleSegmentA004), | ||||
|            -segLen(rectangleSegmentA004) | ||||
|          ], %, $rectangleSegmentC003) | ||||
|       |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) | ||||
|       |> close() | ||||
|     sketch003 = startSketchOn(extrude001, seg04) | ||||
|       |> startProfileAt([-209.64,255.28], %) | ||||
|       |> angledLine([0, 11.56], %, $rectangleSegmentA003) | ||||
|       |> angledLine([ | ||||
|            segAng(rectangleSegmentA003) - 90, | ||||
|            106.84 | ||||
|          ], %, $rectangleSegmentB002) | ||||
|       |> angledLine([ | ||||
|            segAng(rectangleSegmentA003), | ||||
|            -segLen(rectangleSegmentA003) | ||||
|          ], %, $rectangleSegmentC002) | ||||
|       |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) | ||||
|       |> close() | ||||
|     sketch002 = startSketchOn(extrude001, seg03) | ||||
|       |> startProfileAt([205.96,254.59], %) | ||||
|       |> angledLine([0, 11.39], %, $rectangleSegmentA002) | ||||
|       |> angledLine([ | ||||
|            segAng(rectangleSegmentA002) - 90, | ||||
|            105.26 | ||||
|          ], %, $rectangleSegmentB001) | ||||
|       |> angledLine([ | ||||
|            segAng(rectangleSegmentA002), | ||||
|            -segLen(rectangleSegmentA002) | ||||
|          ], %, $rectangleSegmentC001) | ||||
|       |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) | ||||
|       |> close() | ||||
|     `, | ||||
| `, | ||||
|           { shouldNormalise: true } | ||||
|         ) | ||||
|       }) | ||||
| @ -443,18 +423,13 @@ test.describe('Point-and-click tests', { tag: ['@skipWin'] }, () => { | ||||
|         beforeChamferSnippetEnd: '}, extrude001)', | ||||
|         afterChamferSelectSnippet: | ||||
|           'sketch002 = startSketchOn(extrude001, seg03)', | ||||
|         afterRectangle1stClickSnippet: 'startProfileAt([205.96, 254.59], %)', | ||||
|         afterRectangle2ndClickSnippet: `angledLine([0, 11.39], %, $rectangleSegmentA002) | ||||
|     |> angledLine([ | ||||
|          segAng(rectangleSegmentA002) - 90, | ||||
|          105.26 | ||||
|        ], %, $rectangleSegmentB001) | ||||
|     |> angledLine([ | ||||
|          segAng(rectangleSegmentA002), | ||||
|          -segLen(rectangleSegmentA002) | ||||
|        ], %, $rectangleSegmentC001) | ||||
|     |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) | ||||
|     |> close()`, | ||||
|         afterRectangle1stClickSnippet: | ||||
|           'startProfileAt([205.96, 254.59], sketch002)', | ||||
|         afterRectangle2ndClickSnippet: `angledLine([0,11.39],%,$rectangleSegmentA002) | ||||
|         |>angledLine([segAng(rectangleSegmentA002)-90,105.26],%) | ||||
|         |>angledLine([segAng(rectangleSegmentA002),-segLen(rectangleSegmentA002)],%) | ||||
|         |>line(endAbsolute=[profileStartX(%),profileStartY(%)]) | ||||
|         |>close()`, | ||||
|       }) | ||||
|       await editor.expectEditor.toContain( | ||||
|         `sketch001 = startSketchOn('XZ') | ||||
| @ -484,17 +459,17 @@ chamf = chamfer({ | ||||
|        ] | ||||
|      }, %) | ||||
| sketch002 = startSketchOn(extrude001, seg03) | ||||
|   |> startProfileAt([205.96, 254.59], %) | ||||
| profile001 = startProfileAt([205.96, 254.59], sketch002) | ||||
|   |> angledLine([0, 11.39], %, $rectangleSegmentA002) | ||||
|   |> angledLine([ | ||||
|        segAng(rectangleSegmentA002) - 90, | ||||
|        105.26 | ||||
|      ], %, $rectangleSegmentB001) | ||||
|      ], %) | ||||
|   |> angledLine([ | ||||
|        segAng(rectangleSegmentA002), | ||||
|        -segLen(rectangleSegmentA002) | ||||
|      ], %, $rectangleSegmentC001) | ||||
|   |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) | ||||
|      ], %) | ||||
|   |> line(endAbsolute=[profileStartX(%), profileStartY(%)]) | ||||
|   |> close() | ||||
| `, | ||||
|         { shouldNormalise: true } | ||||
| @ -561,10 +536,10 @@ sketch002 = startSketchOn(extrude001, seg03) | ||||
|  | ||||
|     const expectedCodeSnippets = { | ||||
|       sketchOnXzPlane: `sketch001 = startSketchOn('XZ')`, | ||||
|       pointAtOrigin: `startProfileAt([${originSloppy.kcl[0]}, ${originSloppy.kcl[1]}], %)`, | ||||
|       pointAtOrigin: `startProfileAt([${originSloppy.kcl[0]}, ${originSloppy.kcl[1]}], sketch001)`, | ||||
|       segmentOnXAxis: `xLine(${xAxisSloppy.kcl[0]}, %)`, | ||||
|       afterSegmentDraggedOffYAxis: `startProfileAt([${offYAxis.kcl[0]}, ${offYAxis.kcl[1]}], %)`, | ||||
|       afterSegmentDraggedOnYAxis: `startProfileAt([${yAxisSloppy.kcl[0]}, ${yAxisSloppy.kcl[1]}], %)`, | ||||
|       afterSegmentDraggedOffYAxis: `startProfileAt([${offYAxis.kcl[0]}, ${offYAxis.kcl[1]}], sketch001)`, | ||||
|       afterSegmentDraggedOnYAxis: `startProfileAt([${yAxisSloppy.kcl[0]}, ${yAxisSloppy.kcl[1]}], sketch001)`, | ||||
|     } | ||||
|  | ||||
|     await test.step(`Start a sketch on the XZ plane`, async () => { | ||||
| @ -605,6 +580,7 @@ sketch002 = startSketchOn(extrude001, seg03) | ||||
|         expectedCodeSnippets.afterSegmentDraggedOnYAxis | ||||
|       ) | ||||
|     }) | ||||
|     await editor.page.waitForTimeout(1000) | ||||
|   }) | ||||
|  | ||||
|   test(`Verify user can double-click to edit a sketch`, async ({ | ||||
| @ -1397,12 +1373,12 @@ sketch002 = startSketchOn('XZ') | ||||
|       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 toolbar.openPane('code') | ||||
|       await editor.expectEditor.toContain(sweepDeclaration) | ||||
|       await editor.expectState({ | ||||
|         diagnostics: [], | ||||
|  | ||||
| @ -444,8 +444,7 @@ test( | ||||
|  | ||||
|     const startXPx = 600 | ||||
|     await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) | ||||
|     code += ` | ||||
|   |> startProfileAt([7.19, -9.7], %)` | ||||
|     code += `profile001 = startProfileAt([7.19, -9.7], sketch001)` | ||||
|     await expect(page.locator('.cm-content')).toHaveText(code) | ||||
|     await page.waitForTimeout(100) | ||||
|  | ||||
| @ -456,7 +455,9 @@ test( | ||||
|       mask: [page.getByTestId('model-state-indicator')], | ||||
|     }) | ||||
|  | ||||
|     await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10) | ||||
|     const lineEndClick = () => | ||||
|       page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10) | ||||
|     await lineEndClick() | ||||
|     await page.waitForTimeout(100) | ||||
|  | ||||
|     code += ` | ||||
| @ -467,6 +468,15 @@ test( | ||||
|       .getByRole('button', { name: 'arc Tangential Arc', exact: true }) | ||||
|       .click() | ||||
|  | ||||
|     // click on the end of the profile to continue it | ||||
|     await page.waitForTimeout(300) | ||||
|     await lineEndClick() | ||||
|     await page.waitForTimeout(100) | ||||
|  | ||||
|     // click to continue profile | ||||
|     await page.mouse.move(813, 392, { steps: 10 }) | ||||
|     await page.waitForTimeout(100) | ||||
|  | ||||
|     await page.mouse.move(startXPx + PUR * 30, 500 - PUR * 20, { steps: 10 }) | ||||
|  | ||||
|     await page.waitForTimeout(1000) | ||||
| @ -589,8 +599,7 @@ test( | ||||
|       mask: [page.getByTestId('model-state-indicator')], | ||||
|     }) | ||||
|     await expect(page.locator('.cm-content')).toHaveText( | ||||
|       `sketch001 = startSketchOn('XZ') | ||||
|   |> circle({ center = [14.44, -2.44], radius = 1 }, %)` | ||||
|       `sketch001 = startSketchOn('XZ')profile001 = circle({ center = [14.44, -2.44], radius = 1 }, sketch001)` | ||||
|     ) | ||||
|   } | ||||
| ) | ||||
| @ -634,8 +643,7 @@ test.describe( | ||||
|  | ||||
|       const startXPx = 600 | ||||
|       await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) | ||||
|       code += ` | ||||
|   |> startProfileAt([7.19, -9.7], %)` | ||||
|       code += `profile001 = startProfileAt([7.19, -9.7], sketch001)` | ||||
|       await expect(u.codeLocator).toHaveText(code) | ||||
|       await page.waitForTimeout(100) | ||||
|  | ||||
| @ -653,6 +661,10 @@ test.describe( | ||||
|         .click() | ||||
|       await page.waitForTimeout(100) | ||||
|  | ||||
|       // click to continue profile | ||||
|       await page.mouse.click(813, 392) | ||||
|       await page.waitForTimeout(100) | ||||
|  | ||||
|       await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20) | ||||
|  | ||||
|       code += ` | ||||
| @ -739,8 +751,7 @@ test.describe( | ||||
|  | ||||
|       const startXPx = 600 | ||||
|       await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) | ||||
|       code += ` | ||||
|   |> startProfileAt([182.59, -246.32], %)` | ||||
|       code += `profile001 = startProfileAt([182.59, -246.32], sketch001)` | ||||
|       await expect(u.codeLocator).toHaveText(code) | ||||
|       await page.waitForTimeout(100) | ||||
|  | ||||
| @ -758,6 +769,10 @@ test.describe( | ||||
|         .click() | ||||
|       await page.waitForTimeout(100) | ||||
|  | ||||
|       // click to continue profile | ||||
|       await page.mouse.click(813, 392) | ||||
|       await page.waitForTimeout(100) | ||||
|  | ||||
|       await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20) | ||||
|  | ||||
|       code += ` | ||||
|  | ||||
| Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 55 KiB | 
| Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 54 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: 46 KiB | 
| Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB | 
| Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB | 
| @ -1,6 +1,7 @@ | ||||
| import { test, expect } from './zoo-test' | ||||
|  | ||||
| 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( | ||||
| @ -111,18 +112,17 @@ test.describe('Test network and connection issues', () => { | ||||
|  | ||||
|       const startXPx = 600 | ||||
|       await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) | ||||
|       await expect(page.locator('.cm-content')) | ||||
|         .toHaveText(`sketch001 = startSketchOn('XZ') | ||||
|   |> startProfileAt(${commonPoints.startAt}, %)`) | ||||
|       await expect(page.locator('.cm-content')).toHaveText( | ||||
|         `sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${commonPoints.startAt}, sketch001)` | ||||
|       ) | ||||
|       await page.waitForTimeout(100) | ||||
|  | ||||
|       await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10) | ||||
|       await page.waitForTimeout(100) | ||||
|  | ||||
|       await expect(page.locator('.cm-content')) | ||||
|         .toHaveText(`sketch001 = startSketchOn('XZ') | ||||
|   |> startProfileAt(${commonPoints.startAt}, %) | ||||
|   |> xLine(${commonPoints.num1}, %)`) | ||||
|         .toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${commonPoints.startAt}, sketch001) | ||||
|       |> xLine(${commonPoints.num1}, %)`) | ||||
|  | ||||
|       // Expect the network to be up | ||||
|       await expect(networkToggle).toContainText('Connected') | ||||
| @ -169,7 +169,9 @@ test.describe('Test network and connection issues', () => { | ||||
|       await page.mouse.click(100, 100) | ||||
|  | ||||
|       // select a line | ||||
|       await page.getByText(`startProfileAt(${commonPoints.startAt}, %)`).click() | ||||
|       await page | ||||
|         .getByText(`startProfileAt(${commonPoints.startAt}, sketch001)`) | ||||
|         .click() | ||||
|  | ||||
|       // enter sketch again | ||||
|       await u.doAndWaitForCmd( | ||||
| @ -183,11 +185,36 @@ test.describe('Test network and connection issues', () => { | ||||
|  | ||||
|       await page.waitForTimeout(150) | ||||
|  | ||||
|       const camCommand: EngineCommand = { | ||||
|         type: 'modeling_cmd_req', | ||||
|         cmd_id: uuidv4(), | ||||
|         cmd: { | ||||
|           type: 'default_camera_look_at', | ||||
|           center: { x: 109, y: 0, z: -152 }, | ||||
|           vantage: { x: 115, y: -505, z: -152 }, | ||||
|           up: { x: 0, y: 0, z: 1 }, | ||||
|         }, | ||||
|       } | ||||
|       const updateCamCommand: EngineCommand = { | ||||
|         type: 'modeling_cmd_req', | ||||
|         cmd_id: uuidv4(), | ||||
|         cmd: { | ||||
|           type: 'default_camera_get_settings', | ||||
|         }, | ||||
|       } | ||||
|       await u.sendCustomCmd(camCommand) | ||||
|       await page.waitForTimeout(100) | ||||
|       await u.sendCustomCmd(updateCamCommand) | ||||
|       await page.waitForTimeout(100) | ||||
|  | ||||
|       // click to continue profile | ||||
|       await page.mouse.click(1007, 400) | ||||
|       await page.waitForTimeout(100) | ||||
|       // Ensure we can continue sketching | ||||
|       await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20) | ||||
|       await expect.poll(u.normalisedEditorCode) | ||||
|         .toBe(`sketch001 = startSketchOn('XZ') | ||||
|   |> startProfileAt([12.34, -12.34], %) | ||||
| profile001 = startProfileAt([12.34, -12.34], sketch001) | ||||
|   |> xLine(12.34, %) | ||||
|   |> line(end = [-12.34, 12.34]) | ||||
|  | ||||
| @ -197,7 +224,7 @@ test.describe('Test network and connection issues', () => { | ||||
|  | ||||
|       await expect.poll(u.normalisedEditorCode) | ||||
|         .toBe(`sketch001 = startSketchOn('XZ') | ||||
|   |> startProfileAt([12.34, -12.34], %) | ||||
| profile001 = startProfileAt([12.34, -12.34], sketch001) | ||||
|   |> xLine(12.34, %) | ||||
|   |> line(end = [-12.34, 12.34]) | ||||
|   |> xLine(-12.34, %) | ||||
|  | ||||
| @ -19,7 +19,7 @@ test.describe('Testing constraints', { tag: ['@skipWin'] }, () => { | ||||
|   |> line(end = [20, 0]) | ||||
|   |> line(end = [0, 20]) | ||||
|   |> xLine(-20, %) | ||||
|     ` | ||||
| ` | ||||
|       ) | ||||
|     }) | ||||
|  | ||||
| @ -673,7 +673,7 @@ test.describe('Testing constraints', { tag: ['@skipWin'] }, () => { | ||||
|       }, | ||||
|     ] as const | ||||
|     for (const { testName, addVariable, value, constraint } of cases) { | ||||
|       test(`${testName}`, async ({ context, homePage, page }) => { | ||||
|       test(`${testName}`, async ({ context, homePage, page, editor }) => { | ||||
|         // constants and locators | ||||
|         const cmdBarKclInput = page | ||||
|           .getByTestId('cmd-bar-arg-value') | ||||
| @ -706,7 +706,9 @@ part002 = startSketchOn('XZ') | ||||
|         await page.setBodyDimensions({ width: 1200, height: 500 }) | ||||
|  | ||||
|         await homePage.goToModelingScene() | ||||
|         await u.waitForPageLoad() | ||||
|  | ||||
|         await editor.scrollToText('line(end = [74.36, 130.4])', true) | ||||
|         await page.getByText('line(end = [74.36, 130.4])').click() | ||||
|         await page.getByRole('button', { name: 'Edit Sketch' }).click() | ||||
|  | ||||
|  | ||||
| @ -66,33 +66,34 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => { | ||||
|     const startXPx = 600 | ||||
|     await u.closeDebugPanel() | ||||
|     await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) | ||||
|     await expect(page.locator('.cm-content')) | ||||
|       .toHaveText(`sketch001 = startSketchOn('XZ') | ||||
|       |> startProfileAt(${commonPoints.startAt}, %)`) | ||||
|     await expect(page.locator('.cm-content')).toHaveText( | ||||
|       `sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${commonPoints.startAt}, sketch001)` | ||||
|     ) | ||||
|  | ||||
|     await page.waitForTimeout(100) | ||||
|     await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10) | ||||
|  | ||||
|     await expect(page.locator('.cm-content')) | ||||
|       .toHaveText(`sketch001 = startSketchOn('XZ') | ||||
|       |> startProfileAt(${commonPoints.startAt}, %) | ||||
|       |> xLine(${commonPoints.num1}, %)`) | ||||
|       .toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${commonPoints.startAt}, sketch001) | ||||
|     |> xLine(${commonPoints.num1}, %)`) | ||||
|  | ||||
|     await page.waitForTimeout(100) | ||||
|     await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20) | ||||
|     await expect(page.locator('.cm-content')) | ||||
|       .toHaveText(`sketch001 = startSketchOn('XZ') | ||||
|       |> startProfileAt(${commonPoints.startAt}, %) | ||||
|       |> xLine(${commonPoints.num1}, %) | ||||
|       |> yLine(${commonPoints.num1 + 0.01}, %)`) | ||||
|       .toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${ | ||||
|       commonPoints.startAt | ||||
|     }, sketch001) | ||||
|     |> xLine(${commonPoints.num1}, %) | ||||
|     |> yLine(${commonPoints.num1 + 0.01}, %)`) | ||||
|     await page.waitForTimeout(100) | ||||
|     await page.mouse.click(startXPx, 500 - PUR * 20) | ||||
|     await expect(page.locator('.cm-content')) | ||||
|       .toHaveText(`sketch001 = startSketchOn('XZ') | ||||
|       |> startProfileAt(${commonPoints.startAt}, %) | ||||
|       |> xLine(${commonPoints.num1}, %) | ||||
|       |> yLine(${commonPoints.num1 + 0.01}, %) | ||||
|       |> xLine(${commonPoints.num2 * -1}, %)`) | ||||
|       .toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${ | ||||
|       commonPoints.startAt | ||||
|     }, sketch001) | ||||
|     |> xLine(${commonPoints.num1}, %) | ||||
|     |> yLine(${commonPoints.num1 + 0.01}, %) | ||||
|     |> xLine(${commonPoints.num2 * -1}, %)`) | ||||
|  | ||||
|     // deselect line tool | ||||
|     await page.getByRole('button', { name: 'line Line', exact: true }).click() | ||||
| @ -259,66 +260,88 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => { | ||||
|       localStorage.setItem( | ||||
|         'persistCode', | ||||
|         `sketch001 = startSketchOn('XZ') | ||||
|       |> startProfileAt([-79.26, 95.04], %) | ||||
|       |> line(end = [112.54, 127.64], tag = $seg02) | ||||
|       |> line(end = [170.36, -121.61], tag = $seg01) | ||||
|       |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) | ||||
|       |> close() | ||||
|   extrude001 = extrude(sketch001, length = 50) | ||||
|   sketch005 = startSketchOn(extrude001, 'END') | ||||
|     |> startProfileAt([23.24, 136.52], %) | ||||
|     |> line(end = [-8.44, 36.61]) | ||||
|     |> line(end = [49.4, 2.05]) | ||||
|     |> line(end = [29.69, -46.95]) | ||||
|     |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) | ||||
|     |> close() | ||||
|   sketch003 = startSketchOn(extrude001, seg01) | ||||
|     |> startProfileAt([21.23, 17.81], %) | ||||
|     |> line(end = [51.97, 21.32]) | ||||
|     |> line(end = [4.07, -22.75]) | ||||
|     |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) | ||||
|     |> close() | ||||
|   sketch002 = startSketchOn(extrude001, seg02) | ||||
|     |> startProfileAt([-100.54, 16.99], %) | ||||
|     |> line(end = [0, 20.03]) | ||||
|     |> line(end = [62.61, 0], tag = $seg03) | ||||
|     |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) | ||||
|     |> close() | ||||
|   extrude002 = extrude(sketch002, length = 50) | ||||
|   sketch004 = startSketchOn(extrude002, seg03) | ||||
|     |> startProfileAt([57.07, 134.77], %) | ||||
|     |> line(end = [-4.72, 22.84]) | ||||
|     |> line(end = [28.8, 6.71]) | ||||
|     |> line(end = [9.19, -25.33]) | ||||
|     |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) | ||||
|     |> close() | ||||
|   extrude003 = extrude(sketch004, length = 20) | ||||
|   pipeLength = 40 | ||||
|   pipeSmallDia = 10 | ||||
|   pipeLargeDia = 20 | ||||
|   thickness = 0.5 | ||||
|   part009 = startSketchOn('XY') | ||||
|     |> startProfileAt([pipeLargeDia - (thickness / 2), 38], %) | ||||
|     |> line(end = [thickness, 0]) | ||||
|     |> line(end = [0, -1]) | ||||
|     |> angledLineToX({ | ||||
|      angle = 60, | ||||
|      to = pipeSmallDia + thickness | ||||
|    }, %) | ||||
|     |> line(end = [0, -pipeLength]) | ||||
|     |> angledLineToX({ | ||||
|      angle = -60, | ||||
|      to = pipeLargeDia + thickness | ||||
|    }, %) | ||||
|     |> line(end = [0, -1]) | ||||
|     |> line(end = [-thickness, 0]) | ||||
|     |> line(end = [0, 1]) | ||||
|     |> angledLineToX({ angle = 120, to = pipeSmallDia }, %) | ||||
|     |> line(end = [0, pipeLength]) | ||||
|     |> angledLineToX({ angle = 60, to = pipeLargeDia }, %) | ||||
|     |> close() | ||||
|   rev = revolve({ axis: 'y' }, part009) | ||||
|   ` | ||||
|   |> startProfileAt([-79.26, 95.04], %) | ||||
|   |> line(end = [112.54, 127.64], tag = $seg02) | ||||
|   |> line(end = [170.36, -121.61], tag = $seg01) | ||||
|   |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) | ||||
|   |> close() | ||||
| extrude001 = extrude(sketch001, length = 50) | ||||
| sketch005 = startSketchOn(extrude001, 'END') | ||||
|   |> startProfileAt([23.24, 136.52], %) | ||||
|   |> line(end = [-8.44, 36.61]) | ||||
|   |> line(end = [49.4, 2.05]) | ||||
|   |> line(end = [29.69, -46.95]) | ||||
|   |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) | ||||
|   |> close() | ||||
| sketch003 = startSketchOn(extrude001, seg01) | ||||
|   |> startProfileAt([21.23, 17.81], %) | ||||
|   |> line(end = [51.97, 21.32]) | ||||
|   |> line(end = [4.07, -22.75]) | ||||
|   |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) | ||||
|   |> close() | ||||
| sketch002 = startSketchOn(extrude001, seg02) | ||||
|   |> startProfileAt([-100.54, 16.99], %) | ||||
|   |> line(end = [0, 20.03]) | ||||
|   |> line(end = [62.61, 0], tag = $seg03) | ||||
|   |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) | ||||
|   |> close() | ||||
| extrude002 = extrude(sketch002, length = 50) | ||||
| sketch004 = startSketchOn(extrude002, seg03) | ||||
|   |> startProfileAt([57.07, 134.77], %) | ||||
|   |> line(end = [-4.72, 22.84]) | ||||
|   |> line(end = [28.8, 6.71]) | ||||
|   |> line(end = [9.19, -25.33]) | ||||
|   |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) | ||||
|   |> close() | ||||
| extrude003 = extrude(sketch004, length = 20) | ||||
| pipeLength = 40 | ||||
| pipeSmallDia = 10 | ||||
| pipeLargeDia = 20 | ||||
| thickness = 0.5 | ||||
| part009 = startSketchOn('XY') | ||||
|   |> startProfileAt([pipeLargeDia - (thickness / 2), 38], %) | ||||
|   |> line(end = [thickness, 0]) | ||||
|   |> line(end = [0, -1]) | ||||
|   |> angledLineToX({ | ||||
|        angle = 60, | ||||
|        to = pipeSmallDia + thickness | ||||
|      }, %) | ||||
|   |> line(end = [0, -pipeLength]) | ||||
|   |> angledLineToX({ | ||||
|        angle = -60, | ||||
|        to = pipeLargeDia + thickness | ||||
|      }, %) | ||||
|   |> line(end = [0, -1]) | ||||
|   |> line(end = [-thickness, 0]) | ||||
|   |> line(end = [0, 1]) | ||||
|   |> angledLineToX({ angle = 120, to = pipeSmallDia }, %) | ||||
|   |> line(end = [0, pipeLength]) | ||||
|   |> angledLineToX({ angle = 60, to = pipeLargeDia }, %) | ||||
|   |> close() | ||||
| rev = revolve({ axis = 'y' }, part009) | ||||
| sketch006 = startSketchOn('XY') | ||||
| profile001 = circle({ | ||||
|   center = [42.91, -70.42], | ||||
|   radius = 17.96 | ||||
| }, sketch006) | ||||
| profile002 = startProfileAt([86.92, -63.81], sketch006) | ||||
|   |> angledLine([0, 63.81], %, $rectangleSegmentA001) | ||||
|   |> angledLine([ | ||||
|        segAng(rectangleSegmentA001) - 90, | ||||
|        17.05 | ||||
|      ], %) | ||||
|   |> angledLine([ | ||||
|        segAng(rectangleSegmentA001), | ||||
|        -segLen(rectangleSegmentA001) | ||||
|      ], %) | ||||
|   |> 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) | ||||
|     await page.setBodyDimensions({ width: 1000, height: 500 }) | ||||
| @ -347,9 +370,10 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => { | ||||
|     }) | ||||
|     await page.waitForTimeout(100) | ||||
|  | ||||
|     const revolve = { x: 646, y: 248 } | ||||
|     const revolve = { x: 635, y: 253 } | ||||
|     const parentExtrude = { x: 915, y: 133 } | ||||
|     const solid2d = { x: 770, y: 167 } | ||||
|     const individualProfile = { x: 694, y: 432 } | ||||
|  | ||||
|     // DELETE REVOLVE | ||||
|     await page.mouse.click(revolve.x, revolve.y) | ||||
| @ -415,6 +439,20 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => { | ||||
|     await u.expectCmdLog('[data-message-type="execution-done"]', 10_000) | ||||
|     await page.waitForTimeout(200) | ||||
|     await expect(u.codeLocator).not.toContainText(`sketch005 = startSketchOn({`) | ||||
|  | ||||
|     // Delete a single profile | ||||
|     await page.mouse.click(individualProfile.x, individualProfile.y) | ||||
|     await page.waitForTimeout(100) | ||||
|     const codeToBeDeletedSnippet = | ||||
|       'profile003 = startProfileAt([40.16, -120.48], sketch006)' | ||||
|     await expect(page.locator('.cm-activeLine')).toHaveText( | ||||
|       '  |> line(end = [20.91, -28.61])' | ||||
|     ) | ||||
|     await u.clearCommandLogs() | ||||
|     await page.keyboard.press('Backspace') | ||||
|     await u.expectCmdLog('[data-message-type="execution-done"]', 10_000) | ||||
|     await page.waitForTimeout(200) | ||||
|     await expect(u.codeLocator).not.toContainText(codeToBeDeletedSnippet) | ||||
|   }) | ||||
|   test("Deleting solid that the AST mod can't handle results in a toast message", async ({ | ||||
|     page, | ||||
| @ -1216,12 +1254,15 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => { | ||||
|  | ||||
|     await page.waitForTimeout(600) | ||||
|  | ||||
|     const firstClickCoords = { x: 650, y: 200 } as const | ||||
|     // Place a point because the line tool will exit if no points are pressed | ||||
|     await page.mouse.click(650, 200) | ||||
|     await page.mouse.click(firstClickCoords.x, firstClickCoords.y) | ||||
|     await page.waitForTimeout(600) | ||||
|  | ||||
|     // Code before exiting the tool | ||||
|     let previousCodeContent = await page.locator('.cm-content').innerText() | ||||
|     let previousCodeContent = ( | ||||
|       await page.locator('.cm-content').innerText() | ||||
|     ).replace(/\s+/g, '') | ||||
|  | ||||
|     // deselect the line tool by clicking it | ||||
|     await page.getByRole('button', { name: 'line Line', exact: true }).click() | ||||
| @ -1233,14 +1274,23 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => { | ||||
|     await page.mouse.click(750, 200) | ||||
|     await page.waitForTimeout(100) | ||||
|  | ||||
|     // expect no change | ||||
|     await expect(page.locator('.cm-content')).toHaveText(previousCodeContent) | ||||
|     await expect | ||||
|       .poll(async () => { | ||||
|         let str = await page.locator('.cm-content').innerText() | ||||
|         str = str.replace(/\s+/g, '') | ||||
|         return str | ||||
|       }) | ||||
|       .toBe(previousCodeContent) | ||||
|  | ||||
|     // select line tool again | ||||
|     await page.getByRole('button', { name: 'line Line', exact: true }).click() | ||||
|  | ||||
|     await u.closeDebugPanel() | ||||
|  | ||||
|     // Click to continue profile | ||||
|     await page.mouse.click(firstClickCoords.x, firstClickCoords.y) | ||||
|     await page.waitForTimeout(100) | ||||
|  | ||||
|     // line tool should work as expected again | ||||
|     await page.mouse.click(700, 200) | ||||
|     await expect(page.locator('.cm-content')).not.toHaveText( | ||||
|  | ||||
| @ -209,8 +209,13 @@ test('First escape in tool pops you out of tool, second exits sketch mode', asyn | ||||
|   // Draw a line | ||||
|   await page.mouse.move(700, 200, { steps: 5 }) | ||||
|   await page.mouse.click(700, 200) | ||||
|   await page.mouse.move(800, 250, { steps: 5 }) | ||||
|   await page.mouse.click(800, 250) | ||||
|  | ||||
|   const secondMousePosition = { x: 800, y: 250 } | ||||
|  | ||||
|   await page.mouse.move(secondMousePosition.x, secondMousePosition.y, { | ||||
|     steps: 5, | ||||
|   }) | ||||
|   await page.mouse.click(secondMousePosition.x, secondMousePosition.y) | ||||
|   // Unequip line tool | ||||
|   await page.keyboard.press('Escape') | ||||
|   // Make sure we didn't pop out of sketch mode. | ||||
| @ -219,11 +224,23 @@ test('First escape in tool pops you out of tool, second exits sketch mode', asyn | ||||
|   // Equip arc tool | ||||
|   await page.keyboard.press('a') | ||||
|   await expect(arcButton).toHaveAttribute('aria-pressed', 'true') | ||||
|  | ||||
|   // click in the same position again to continue the profile | ||||
|   await page.mouse.move(secondMousePosition.x, secondMousePosition.y, { | ||||
|     steps: 5, | ||||
|   }) | ||||
|   await page.mouse.click(secondMousePosition.x, secondMousePosition.y) | ||||
|  | ||||
|   await page.mouse.move(1000, 100, { steps: 5 }) | ||||
|   await page.mouse.click(1000, 100) | ||||
|   await page.keyboard.press('Escape') | ||||
|   await page.keyboard.press('l') | ||||
|   await expect(lineButton).toHaveAttribute('aria-pressed', 'true') | ||||
|   await expect(arcButton).toHaveAttribute('aria-pressed', 'false') | ||||
|   await expect | ||||
|     .poll(async () => { | ||||
|       await page.keyboard.press('l') | ||||
|       return lineButton.getAttribute('aria-pressed') | ||||
|     }) | ||||
|     .toBe('true') | ||||
|  | ||||
|   // Do not close the sketch. | ||||
|   // On close it will exit sketch mode. | ||||
| @ -519,9 +536,9 @@ extrude001 = extrude(sketch001, length = 5 + 7)` | ||||
|  | ||||
|   await expect.poll(u.normalisedEditorCode).toContain( | ||||
|     u.normalisedCode(`sketch002 = startSketchOn(extrude001, seg01) | ||||
|   |> startProfileAt([-12.94, 6.6], %) | ||||
|   |> line(end = [2.45, -0.2]) | ||||
|   |> line(end = [-2.6, -1.25]) | ||||
| profile001 = startProfileAt([-12.34, 12.34], sketch002) | ||||
|   |> line(end = [12.34, -12.34]) | ||||
|   |> line(end = [-12.34, -12.34]) | ||||
|   |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) | ||||
|   |> close() | ||||
| `) | ||||
| @ -537,9 +554,8 @@ extrude001 = extrude(sketch001, length = 5 + 7)` | ||||
|   await page.getByText('startProfileAt([-12').click() | ||||
|   await expect(page.getByRole('button', { name: 'Edit Sketch' })).toBeVisible() | ||||
|   await page.getByRole('button', { name: 'Edit Sketch' }).click() | ||||
|   await page.waitForTimeout(400) | ||||
|   await page.waitForTimeout(150) | ||||
|   await page.setBodyDimensions({ width: 1200, height: 1200 }) | ||||
|   await page.waitForTimeout(500) | ||||
|   await page.setViewportSize({ width: 1200, height: 1200 }) | ||||
|   await u.openAndClearDebugPanel() | ||||
|   await u.updateCamPosition([452, -152, 1166]) | ||||
|   await u.closeDebugPanel() | ||||
|  | ||||
| @ -5,7 +5,6 @@ import { useModelingContext } from 'hooks/useModelingContext' | ||||
| import { useNetworkContext } from 'hooks/useNetworkContext' | ||||
| import { NetworkHealthState } from 'hooks/useNetworkStatus' | ||||
| import { ActionButton } from 'components/ActionButton' | ||||
| import { isSingleCursorInPipe } from 'lang/queryAst' | ||||
| import { useKclContext } from 'lang/KclProvider' | ||||
| import { ActionButtonDropdown } from 'components/ActionButtonDropdown' | ||||
| import { useHotkeys } from 'react-hotkeys-hook' | ||||
| @ -21,6 +20,7 @@ import { | ||||
| } from 'lib/toolbar' | ||||
| import { isDesktop } from 'lib/isDesktop' | ||||
| import { openExternalBrowserIfDesktop } from 'lib/openWindow' | ||||
| import { isCursorInFunctionDefinition } from 'lang/queryAst' | ||||
| import { commandBarActor } from 'machines/commandBarMachine' | ||||
| import { isArray } from 'lib/utils' | ||||
|  | ||||
| @ -37,7 +37,12 @@ export function Toolbar({ | ||||
|   const buttonBorderClassName = '!border-transparent' | ||||
|  | ||||
|   const sketchPathId = useMemo(() => { | ||||
|     if (!isSingleCursorInPipe(context.selectionRanges, kclManager.ast)) | ||||
|     if ( | ||||
|       isCursorInFunctionDefinition( | ||||
|         kclManager.ast, | ||||
|         context.selectionRanges.graphSelections[0] | ||||
|       ) | ||||
|     ) | ||||
|       return false | ||||
|     return isCursorInSketchCommandRange( | ||||
|       engineCommandManager.artifactGraph, | ||||
|  | ||||
| @ -124,14 +124,7 @@ export const ClientSideScene = ({ | ||||
|         'mouseup', | ||||
|         toSync(sceneInfra.onMouseUp, reportRejection) | ||||
|       ) | ||||
|       sceneEntitiesManager | ||||
|         .tearDownSketch() | ||||
|         .then(() => { | ||||
|           // no op | ||||
|         }) | ||||
|         .catch((e) => { | ||||
|           console.error(e) | ||||
|         }) | ||||
|       sceneEntitiesManager.tearDownSketch({ removeAxis: true }) | ||||
|     } | ||||
|   }, []) | ||||
|  | ||||
| @ -152,7 +145,8 @@ export const ClientSideScene = ({ | ||||
|       state.matches({ Sketch: 'Line tool' }) || | ||||
|       state.matches({ Sketch: 'Tangential arc to' }) || | ||||
|       state.matches({ Sketch: 'Rectangle tool' }) || | ||||
|       state.matches({ Sketch: 'Circle tool' }) | ||||
|       state.matches({ Sketch: 'Circle tool' }) || | ||||
|       state.matches({ Sketch: 'Circle three point tool' }) | ||||
|     ) { | ||||
|       cursor = 'crosshair' | ||||
|     } else { | ||||
| @ -190,12 +184,15 @@ const Overlays = () => { | ||||
|       style={{ zIndex: '99999999' }} | ||||
|     > | ||||
|       {Object.entries(context.segmentOverlays) | ||||
|         .filter((a) => a[1].visible) | ||||
|         .map(([pathToNodeString, overlay], index) => { | ||||
|         .flatMap((a) => | ||||
|           a[1].map((b) => ({ pathToNodeString: a[0], overlay: b })) | ||||
|         ) | ||||
|         .filter((a) => a.overlay.visible) | ||||
|         .map(({ pathToNodeString, overlay }, index) => { | ||||
|           return ( | ||||
|             <Overlay | ||||
|               overlay={overlay} | ||||
|               key={pathToNodeString} | ||||
|               key={pathToNodeString + String(index)} | ||||
|               pathToNodeString={pathToNodeString} | ||||
|               overlayIndex={index} | ||||
|             /> | ||||
| @ -236,11 +233,17 @@ const Overlay = ({ | ||||
|  | ||||
|   const constraints = | ||||
|     callExpression.type === 'CallExpression' | ||||
|       ? getConstraintInfo(callExpression, codeManager.code, overlay.pathToNode) | ||||
|       ? getConstraintInfo( | ||||
|           callExpression, | ||||
|           codeManager.code, | ||||
|           overlay.pathToNode, | ||||
|           overlay.filterValue | ||||
|         ) | ||||
|       : getConstraintInfoKw( | ||||
|           callExpression, | ||||
|           codeManager.code, | ||||
|           overlay.pathToNode | ||||
|           overlay.pathToNode, | ||||
|           overlay.filterValue | ||||
|         ) | ||||
|  | ||||
|   const offset = 20 // px | ||||
| @ -260,7 +263,6 @@ const Overlay = ({ | ||||
|       state.matches({ Sketch: 'Tangential arc to' }) || | ||||
|       state.matches({ Sketch: 'Rectangle tool' }) | ||||
|     ) | ||||
|  | ||||
|   return ( | ||||
|     <div className={`absolute w-0 h-0`}> | ||||
|       <div | ||||
| @ -318,17 +320,18 @@ const Overlay = ({ | ||||
|           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 | ||||
|           */} | ||||
|           {callExpression?.callee?.name !== 'circle' && ( | ||||
|             <SegmentMenu | ||||
|               verticalPosition={ | ||||
|                 overlay.windowCoords[1] > window.innerHeight / 2 | ||||
|                   ? 'top' | ||||
|                   : 'bottom' | ||||
|               } | ||||
|               pathToNode={overlay.pathToNode} | ||||
|               stdLibFnName={constraints[0]?.stdLibFnName} | ||||
|             /> | ||||
|           )} | ||||
|           {callExpression?.callee?.name !== 'circle' && | ||||
|             callExpression?.callee?.name !== 'circleThreePoint' && ( | ||||
|               <SegmentMenu | ||||
|                 verticalPosition={ | ||||
|                   overlay.windowCoords[1] > window.innerHeight / 2 | ||||
|                     ? 'top' | ||||
|                     : 'bottom' | ||||
|                 } | ||||
|                 pathToNode={overlay.pathToNode} | ||||
|                 stdLibFnName={constraints[0]?.stdLibFnName} | ||||
|               /> | ||||
|             )} | ||||
|         </div> | ||||
|       )} | ||||
|     </div> | ||||
| @ -449,6 +452,8 @@ export async function deleteSegment({ | ||||
|   if (!sketchDetails) return | ||||
|   await sceneEntitiesManager.updateAstAndRejigSketch( | ||||
|     pathToNode, | ||||
|     sketchDetails.sketchNodePaths, | ||||
|     sketchDetails.planeNodePath, | ||||
|     modifiedAst, | ||||
|     sketchDetails.zAxis, | ||||
|     sketchDetails.yAxis, | ||||
|  | ||||
| @ -182,13 +182,15 @@ export class SceneInfra { | ||||
|   callbacks: (() => SegmentOverlayPayload | null)[] = [] | ||||
|   _overlayCallbacks(callbacks: (() => SegmentOverlayPayload | null)[]) { | ||||
|     const segmentOverlayPayload: SegmentOverlayPayload = { | ||||
|       type: 'set-many', | ||||
|       type: 'add-many', | ||||
|       overlays: {}, | ||||
|     } | ||||
|     callbacks.forEach((cb) => { | ||||
|       const overlay = cb() | ||||
|       if (overlay?.type === 'set-one') { | ||||
|         segmentOverlayPayload.overlays[overlay.pathToNodeString] = overlay.seg | ||||
|       } else if (overlay?.type === 'add-many') { | ||||
|         Object.assign(segmentOverlayPayload.overlays, overlay.overlays) | ||||
|       } | ||||
|     }) | ||||
|     this.modelingSend({ | ||||
| @ -213,25 +215,27 @@ export class SceneInfra { | ||||
|  | ||||
|   overlayThrottleMap: { [pathToNodeString: string]: number } = {} | ||||
|   updateOverlayDetails({ | ||||
|     arrowGroup, | ||||
|     handle, | ||||
|     group, | ||||
|     isHandlesVisible, | ||||
|     from, | ||||
|     to, | ||||
|     angle, | ||||
|     hasThreeDotMenu, | ||||
|   }: { | ||||
|     arrowGroup: Group | ||||
|     handle: Group | ||||
|     group: Group | ||||
|     isHandlesVisible: boolean | ||||
|     from: Coords2d | ||||
|     to: Coords2d | ||||
|     hasThreeDotMenu: boolean | ||||
|     angle?: number | ||||
|   }): 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) | ||||
|  | ||||
|       // Get the position of the object3D in world space | ||||
|       arrowGroup.getWorldPosition(vector) | ||||
|       handle.getWorldPosition(vector) | ||||
|  | ||||
|       // Project that position to screen space | ||||
|       vector.project(this.camControls.camera) | ||||
| @ -244,13 +248,16 @@ export class SceneInfra { | ||||
|       return { | ||||
|         type: 'set-one', | ||||
|         pathToNodeString, | ||||
|         seg: { | ||||
|           windowCoords: [x, y], | ||||
|           angle: _angle, | ||||
|           group, | ||||
|           pathToNode: group.userData.pathToNode, | ||||
|           visible: isHandlesVisible, | ||||
|         }, | ||||
|         seg: [ | ||||
|           { | ||||
|             windowCoords: [x, y], | ||||
|             angle: _angle, | ||||
|             group, | ||||
|             pathToNode: group.userData.pathToNode, | ||||
|             visible: isHandlesVisible, | ||||
|             hasThreeDotMenu, | ||||
|           }, | ||||
|         ], | ||||
|       } | ||||
|     } | ||||
|     return null | ||||
|  | ||||
| @ -31,6 +31,12 @@ import { | ||||
|   CIRCLE_SEGMENT, | ||||
|   CIRCLE_SEGMENT_BODY, | ||||
|   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_OFFSET_PX, | ||||
|   HIDE_HOVER_SEGMENT_LENGTH, | ||||
| @ -56,11 +62,16 @@ import { | ||||
| } from './sceneInfra' | ||||
| import { Themes, getThemeColorForThreeJs } from 'lib/theme' | ||||
| 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 { err } from 'lib/trap' | ||||
| import { editorManager, sceneInfra } from 'lib/singletons' | ||||
| import { sceneInfra } from 'lib/singletons' | ||||
| import { Selections } from 'lib/selections' | ||||
| import { calculate_circle_from_3_points } from 'wasm-lib/pkg/wasm_lib' | ||||
| import { commandBarActor } from 'machines/commandBarMachine' | ||||
|  | ||||
| interface CreateSegmentArgs { | ||||
| @ -307,11 +318,12 @@ class StraightSegment implements SegmentUtils { | ||||
|     } | ||||
|     return () => | ||||
|       sceneInfra.updateOverlayDetails({ | ||||
|         arrowGroup, | ||||
|         handle: arrowGroup, | ||||
|         group, | ||||
|         isHandlesVisible, | ||||
|         from, | ||||
|         to, | ||||
|         hasThreeDotMenu: true, | ||||
|       }) | ||||
|   } | ||||
| } | ||||
| @ -483,12 +495,13 @@ class TangentialArcToSegment implements SegmentUtils { | ||||
|     ) | ||||
|     return () => | ||||
|       sceneInfra.updateOverlayDetails({ | ||||
|         arrowGroup, | ||||
|         handle: arrowGroup, | ||||
|         group, | ||||
|         isHandlesVisible, | ||||
|         from, | ||||
|         to, | ||||
|         angle, | ||||
|         hasThreeDotMenu: true, | ||||
|       }) | ||||
|   } | ||||
| } | ||||
| @ -684,35 +697,255 @@ class CircleSegment implements SegmentUtils { | ||||
|     } | ||||
|     return () => | ||||
|       sceneInfra.updateOverlayDetails({ | ||||
|         arrowGroup, | ||||
|         handle: arrowGroup, | ||||
|         group, | ||||
|         isHandlesVisible, | ||||
|         from: from, | ||||
|         to: [center[0], center[1]], | ||||
|         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({ | ||||
|   from, | ||||
|   isDraft = false, | ||||
|   scale = 1, | ||||
|   theme, | ||||
|   isSelected, | ||||
|   size = 12, | ||||
|   ...rest | ||||
| }: { | ||||
|   from: Coords2d | ||||
|   scale?: number | ||||
|   theme: Themes | ||||
|   isSelected?: boolean | ||||
|   size?: number | ||||
| } & ( | ||||
|   | { isDraft: true } | ||||
|   | { isDraft: false; id: string; pathToNode: PathToNode } | ||||
| )) { | ||||
|   const group = new Group() | ||||
|  | ||||
|   const geometry = new BoxGeometry(12, 12, 12) // in pixels scaled later | ||||
|   const geometry = new BoxGeometry(size, size, size) // in pixels scaled later | ||||
|   const baseColor = getThemeColorForThreeJs(theme) | ||||
|   const color = isSelected ? 0x0000ff : baseColor | ||||
|   const body = new MeshBasicMaterial({ color }) | ||||
| @ -774,6 +1007,29 @@ function createCircleCenterHandle( | ||||
|   circleCenterGroup.scale.set(scale, scale, scale) | ||||
|   return circleCenterGroup | ||||
| } | ||||
| function createCircleThreePointHandle( | ||||
|   scale = 1, | ||||
|   theme: Themes, | ||||
|   name: `circle-three-point-handle${'1' | '2' | '3'}`, | ||||
|   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( | ||||
|   scale: number, | ||||
| @ -1100,4 +1356,5 @@ export const segmentUtils = { | ||||
|   straight: new StraightSegment(), | ||||
|   tangentialArcTo: new TangentialArcToSegment(), | ||||
|   circle: new CircleSegment(), | ||||
|   circleThreePoint: new CircleThreePointSegment(), | ||||
| } as const | ||||
|  | ||||
| @ -25,7 +25,7 @@ import { useSetupEngineManager } from 'hooks/useSetupEngineManager' | ||||
| import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' | ||||
| import { | ||||
|   isCursorInSketchCommandRange, | ||||
|   updatePathToNodeFromMap, | ||||
|   updateSketchDetailsNodePaths, | ||||
| } from 'lang/util' | ||||
| import { | ||||
|   kclManager, | ||||
| @ -65,17 +65,30 @@ import { | ||||
|   replaceValueAtNodePath, | ||||
|   sketchOnExtrudedFace, | ||||
|   sketchOnOffsetPlane, | ||||
|   splitPipedProfile, | ||||
|   startSketchOnDefault, | ||||
| } from 'lang/modifyAst' | ||||
| import { PathToNode, Program, parse, recast, resultIsOk } from 'lang/wasm' | ||||
| import { artifactIsPlaneWithPaths, isSingleCursorInPipe } from 'lang/queryAst' | ||||
| import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils' | ||||
| import { | ||||
|   PathToNode, | ||||
|   Program, | ||||
|   VariableDeclaration, | ||||
|   parse, | ||||
|   recast, | ||||
|   resultIsOk, | ||||
| } from 'lang/wasm' | ||||
| import { | ||||
|   artifactIsPlaneWithPaths, | ||||
|   doesSketchPipeNeedSplitting, | ||||
|   getNodeFromPath, | ||||
|   isCursorInFunctionDefinition, | ||||
|   traverse, | ||||
| } from 'lang/queryAst' | ||||
| import { exportFromEngine } from 'lib/exportFromEngine' | ||||
| import { Models } from '@kittycad/lib/dist/types/src' | ||||
| import toast from 'react-hot-toast' | ||||
| import { useLoaderData, useNavigate, useSearchParams } from 'react-router-dom' | ||||
| import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls' | ||||
| import { err, reportRejection, trap } from 'lib/trap' | ||||
| import { err, reportRejection, trap, reject } from 'lib/trap' | ||||
| import { | ||||
|   ExportIntent, | ||||
|   EngineConnectionStateType, | ||||
| @ -86,10 +99,16 @@ import { useFileContext } from 'hooks/useFileContext' | ||||
| import { uuidv4 } from 'lib/utils' | ||||
| import { IndexLoaderData } from 'lib/types' | ||||
| import { Node } from 'wasm-lib/kcl/bindings/Node' | ||||
| import { | ||||
|   getFaceCodeRef, | ||||
|   getPathsFromArtifact, | ||||
|   getPlaneFromArtifact, | ||||
| } from 'lang/std/artifactGraph' | ||||
| import { promptToEditFlow } from 'lib/promptToEdit' | ||||
| import { kclEditorActor } from 'machines/kclEditorMachine' | ||||
| import { commandBarActor } from 'machines/commandBarMachine' | ||||
| import { useToken } from 'machines/appMachine' | ||||
| import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils' | ||||
|  | ||||
| type MachineContext<T extends AnyStateMachine> = { | ||||
|   state: StateFrom<T> | ||||
| @ -254,7 +273,11 @@ export const ModelingMachineProvider = ({ | ||||
|         'Set Segment Overlays': assign({ | ||||
|           segmentOverlays: ({ context: { segmentOverlays }, event }) => { | ||||
|             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') | ||||
|               return { | ||||
|                 ...segmentOverlays, | ||||
| @ -287,7 +310,7 @@ export const ModelingMachineProvider = ({ | ||||
|           return { | ||||
|             sketchDetails: { | ||||
|               ...sketchDetails, | ||||
|               sketchPathToNode: event.data, | ||||
|               sketchEntryNodePath: event.data, | ||||
|             }, | ||||
|           } | ||||
|         }), | ||||
| @ -483,9 +506,17 @@ export const ModelingMachineProvider = ({ | ||||
|                 selectionRanges: setSelections.selection, | ||||
|                 sketchDetails: { | ||||
|                   ...sketchDetails, | ||||
|                   sketchPathToNode: | ||||
|                     setSelections.updatedPathToNode || | ||||
|                     sketchDetails?.sketchPathToNode || | ||||
|                   sketchEntryNodePath: | ||||
|                     setSelections.updatedSketchEntryNodePath || | ||||
|                     sketchDetails?.sketchEntryNodePath || | ||||
|                     [], | ||||
|                   sketchNodePaths: | ||||
|                     setSelections.updatedSketchNodePaths || | ||||
|                     sketchDetails?.sketchNodePaths || | ||||
|                     [], | ||||
|                   planeNodePath: | ||||
|                     setSelections.updatedPlaneNodePath || | ||||
|                     sketchDetails?.planeNodePath || | ||||
|                     [], | ||||
|                 }, | ||||
|               } | ||||
| @ -638,7 +669,12 @@ export const ModelingMachineProvider = ({ | ||||
|           if (artifactIsPlaneWithPaths(selectionRanges)) { | ||||
|             return true | ||||
|           } | ||||
|           if (!isSingleCursorInPipe(selectionRanges, kclManager.ast)) | ||||
|           if ( | ||||
|             isCursorInFunctionDefinition( | ||||
|               kclManager.ast, | ||||
|               selectionRanges.graphSelections[0] | ||||
|             ) | ||||
|           ) | ||||
|             return false | ||||
|           return !!isCursorInSketchCommandRange( | ||||
|             engineCommandManager.artifactGraph, | ||||
| @ -666,13 +702,33 @@ export const ModelingMachineProvider = ({ | ||||
|           async ({ input: { sketchDetails } }) => { | ||||
|             if (!sketchDetails) return | ||||
|             if (kclManager.ast.body.length) { | ||||
|               // this assumes no changes have been made to the sketch besides what we did when entering the sketch | ||||
|               // i.e. doesn't account for user's adding code themselves, maybe we need store a flag userEditedSinceSketchMode? | ||||
|               const newAst = structuredClone(kclManager.ast) | ||||
|               const varDecIndex = sketchDetails.sketchPathToNode[1][0] | ||||
|               const varDecIndex = sketchDetails.planeNodePath[1][0] | ||||
|  | ||||
|               const varDec = getNodeFromPath<VariableDeclaration>( | ||||
|                 newAst, | ||||
|                 sketchDetails.planeNodePath, | ||||
|                 'VariableDeclaration' | ||||
|               ) | ||||
|               if (err(varDec)) return reject(new Error('No varDec')) | ||||
|               const variableName = varDec.node.declaration.id.name | ||||
|               let isIdentifierUsed = false | ||||
|               traverse(newAst, { | ||||
|                 enter: (node) => { | ||||
|                   if ( | ||||
|                     node.type === 'Identifier' && | ||||
|                     node.name === variableName | ||||
|                   ) { | ||||
|                     isIdentifierUsed = true | ||||
|                   } | ||||
|                 }, | ||||
|               }) | ||||
|               if (isIdentifierUsed) return | ||||
|  | ||||
|               // remove body item at varDecIndex | ||||
|               newAst.body = newAst.body.filter((_, i) => i !== varDecIndex) | ||||
|               await kclManager.executeAstMock(newAst) | ||||
|               await codeManager.updateEditorWithAstAndWriteToFile(newAst) | ||||
|             } | ||||
|             sceneInfra.setCallbacks({ | ||||
|               onClick: () => {}, | ||||
| @ -682,7 +738,7 @@ export const ModelingMachineProvider = ({ | ||||
|           } | ||||
|         ), | ||||
|         'animate-to-face': fromPromise(async ({ input }) => { | ||||
|           if (!input) return undefined | ||||
|           if (!input) return null | ||||
|           if (input.type === 'extrudeFace' || input.type === 'offsetPlane') { | ||||
|             const sketched = | ||||
|               input.type === 'extrudeFace' | ||||
| @ -709,7 +765,9 @@ export const ModelingMachineProvider = ({ | ||||
|             await letEngineAnimateAndSyncCamAfter(engineCommandManager, id) | ||||
|             sceneInfra.camControls.syncDirection = 'clientToEngine' | ||||
|             return { | ||||
|               sketchPathToNode: pathToNewSketchNode, | ||||
|               sketchEntryNodePath: [], | ||||
|               planeNodePath: pathToNewSketchNode, | ||||
|               sketchNodePaths: [], | ||||
|               zAxis: input.zAxis, | ||||
|               yAxis: input.yAxis, | ||||
|               origin: input.position, | ||||
| @ -730,7 +788,9 @@ export const ModelingMachineProvider = ({ | ||||
|           ) | ||||
|  | ||||
|           return { | ||||
|             sketchPathToNode: pathToNode, | ||||
|             sketchEntryNodePath: [], | ||||
|             planeNodePath: pathToNode, | ||||
|             sketchNodePaths: [], | ||||
|             zAxis: input.zAxis, | ||||
|             yAxis: input.yAxis, | ||||
|             origin: [0, 0, 0], | ||||
| @ -739,21 +799,49 @@ export const ModelingMachineProvider = ({ | ||||
|         }), | ||||
|         'animate-to-sketch': fromPromise( | ||||
|           async ({ input: { selectionRanges } }) => { | ||||
|             const sourceRange = | ||||
|               selectionRanges.graphSelections[0]?.codeRef?.range | ||||
|             const sketchPathToNode = getNodePathFromSourceRange( | ||||
|               kclManager.ast, | ||||
|               sourceRange | ||||
|             const plane = getPlaneFromArtifact( | ||||
|               selectionRanges.graphSelections[0].artifact, | ||||
|               engineCommandManager.artifactGraph | ||||
|             ) | ||||
|             const info = await getSketchOrientationDetails( | ||||
|               sketchPathToNode || [] | ||||
|             if (err(plane)) return Promise.reject(plane) | ||||
|  | ||||
|             const sketch = Object.values(kclManager.execState.variables).find( | ||||
|               (variable) => | ||||
|                 variable?.type === 'Sketch' && | ||||
|                 variable.value.artifactId === plane.pathIds[0] | ||||
|             ) | ||||
|             if (!sketch || sketch.type !== 'Sketch') | ||||
|               return Promise.reject(new Error('No sketch')) | ||||
|             const info = await getSketchOrientationDetails(sketch.value) | ||||
|  | ||||
|             await letEngineAnimateAndSyncCamAfter( | ||||
|               engineCommandManager, | ||||
|               info?.sketchDetails?.faceId || '' | ||||
|             ) | ||||
|  | ||||
|             const sketchArtifact = engineCommandManager.artifactGraph.get( | ||||
|               plane.pathIds[0] | ||||
|             ) | ||||
|             if (sketchArtifact?.type !== 'path') | ||||
|               return Promise.reject(new Error('No sketch artifact')) | ||||
|             const sketchPaths = getPathsFromArtifact({ | ||||
|               artifact: engineCommandManager.artifactGraph.get(plane.id), | ||||
|               sketchPathToNode: sketchArtifact?.codeRef?.pathToNode, | ||||
|               artifactGraph: engineCommandManager.artifactGraph, | ||||
|               ast: kclManager.ast, | ||||
|             }) | ||||
|             if (err(sketchPaths)) return Promise.reject(sketchPaths) | ||||
|             let codeRef = getFaceCodeRef(plane) | ||||
|             if (!codeRef) return Promise.reject(new Error('No plane codeRef')) | ||||
|             // codeRef.pathToNode is not always populated correctly | ||||
|             const planeNodePath = getNodePathFromSourceRange( | ||||
|               kclManager.ast, | ||||
|               codeRef.range | ||||
|             ) | ||||
|             return { | ||||
|               sketchPathToNode: sketchPathToNode || [], | ||||
|               sketchEntryNodePath: sketchArtifact.codeRef.pathToNode || [], | ||||
|               sketchNodePaths: sketchPaths, | ||||
|               planeNodePath, | ||||
|               zAxis: info.sketchDetails.zAxis || null, | ||||
|               yAxis: info.sketchDetails.yAxis || null, | ||||
|               origin: info.sketchDetails.origin.map( | ||||
| @ -766,7 +854,7 @@ export const ModelingMachineProvider = ({ | ||||
|  | ||||
|         'Get horizontal info': fromPromise( | ||||
|           async ({ input: { selectionRanges, sketchDetails } }) => { | ||||
|             const { modifiedAst, pathToNodeMap } = | ||||
|             const { modifiedAst, pathToNodeMap, exprInsertIndex } = | ||||
|               await applyConstraintHorzVertDistance({ | ||||
|                 constraint: 'setHorzDistance', | ||||
|                 selectionRanges, | ||||
| @ -778,13 +866,23 @@ export const ModelingMachineProvider = ({ | ||||
|  | ||||
|             if (!sketchDetails) | ||||
|               return Promise.reject(new Error('No sketch details')) | ||||
|             const updatedPathToNode = updatePathToNodeFromMap( | ||||
|               sketchDetails.sketchPathToNode, | ||||
|               pathToNodeMap | ||||
|             ) | ||||
|  | ||||
|             const { | ||||
|               updatedSketchEntryNodePath, | ||||
|               updatedSketchNodePaths, | ||||
|               updatedPlaneNodePath, | ||||
|             } = updateSketchDetailsNodePaths({ | ||||
|               sketchEntryNodePath: sketchDetails.sketchEntryNodePath, | ||||
|               sketchNodePaths: sketchDetails.sketchNodePaths, | ||||
|               planeNodePath: sketchDetails.planeNodePath, | ||||
|               exprInsertIndex, | ||||
|             }) | ||||
|  | ||||
|             const updatedAst = | ||||
|               await sceneEntitiesManager.updateAstAndRejigSketch( | ||||
|                 updatedPathToNode, | ||||
|                 updatedSketchEntryNodePath, | ||||
|                 updatedSketchNodePaths, | ||||
|                 updatedPlaneNodePath, | ||||
|                 _modifiedAst, | ||||
|                 sketchDetails.zAxis, | ||||
|                 sketchDetails.yAxis, | ||||
| @ -805,13 +903,15 @@ export const ModelingMachineProvider = ({ | ||||
|             return { | ||||
|               selectionType: 'completeSelection', | ||||
|               selection, | ||||
|               updatedPathToNode, | ||||
|               updatedSketchEntryNodePath, | ||||
|               updatedSketchNodePaths, | ||||
|               updatedPlaneNodePath, | ||||
|             } | ||||
|           } | ||||
|         ), | ||||
|         'Get vertical info': fromPromise( | ||||
|           async ({ input: { selectionRanges, sketchDetails } }) => { | ||||
|             const { modifiedAst, pathToNodeMap } = | ||||
|             const { modifiedAst, pathToNodeMap, exprInsertIndex } = | ||||
|               await applyConstraintHorzVertDistance({ | ||||
|                 constraint: 'setVertDistance', | ||||
|                 selectionRanges, | ||||
| @ -822,13 +922,23 @@ export const ModelingMachineProvider = ({ | ||||
|             const _modifiedAst = pResult.program | ||||
|             if (!sketchDetails) | ||||
|               return Promise.reject(new Error('No sketch details')) | ||||
|             const updatedPathToNode = updatePathToNodeFromMap( | ||||
|               sketchDetails.sketchPathToNode, | ||||
|               pathToNodeMap | ||||
|             ) | ||||
|  | ||||
|             const { | ||||
|               updatedSketchEntryNodePath, | ||||
|               updatedSketchNodePaths, | ||||
|               updatedPlaneNodePath, | ||||
|             } = updateSketchDetailsNodePaths({ | ||||
|               sketchEntryNodePath: sketchDetails.sketchEntryNodePath, | ||||
|               sketchNodePaths: sketchDetails.sketchNodePaths, | ||||
|               planeNodePath: sketchDetails.planeNodePath, | ||||
|               exprInsertIndex, | ||||
|             }) | ||||
|  | ||||
|             const updatedAst = | ||||
|               await sceneEntitiesManager.updateAstAndRejigSketch( | ||||
|                 updatedPathToNode, | ||||
|                 updatedSketchEntryNodePath, | ||||
|                 updatedSketchNodePaths, | ||||
|                 updatedPlaneNodePath, | ||||
|                 _modifiedAst, | ||||
|                 sketchDetails.zAxis, | ||||
|                 sketchDetails.yAxis, | ||||
| @ -849,7 +959,9 @@ export const ModelingMachineProvider = ({ | ||||
|             return { | ||||
|               selectionType: 'completeSelection', | ||||
|               selection, | ||||
|               updatedPathToNode, | ||||
|               updatedSketchEntryNodePath, | ||||
|               updatedSketchNodePaths, | ||||
|               updatedPlaneNodePath, | ||||
|             } | ||||
|           } | ||||
|         ), | ||||
| @ -859,14 +971,15 @@ export const ModelingMachineProvider = ({ | ||||
|               selectionRanges, | ||||
|             }) | ||||
|             if (err(info)) return Promise.reject(info) | ||||
|             const { modifiedAst, pathToNodeMap } = await (info.enabled | ||||
|               ? applyConstraintAngleBetween({ | ||||
|                   selectionRanges, | ||||
|                 }) | ||||
|               : applyConstraintAngleLength({ | ||||
|                   selectionRanges, | ||||
|                   angleOrLength: 'setAngle', | ||||
|                 })) | ||||
|             const { modifiedAst, pathToNodeMap, exprInsertIndex } = | ||||
|               await (info.enabled | ||||
|                 ? applyConstraintAngleBetween({ | ||||
|                     selectionRanges, | ||||
|                   }) | ||||
|                 : applyConstraintAngleLength({ | ||||
|                     selectionRanges, | ||||
|                     angleOrLength: 'setAngle', | ||||
|                   })) | ||||
|             const pResult = parse(recast(modifiedAst)) | ||||
|             if (trap(pResult) || !resultIsOk(pResult)) | ||||
|               return Promise.reject(new Error('Unexpected compilation error')) | ||||
| @ -875,13 +988,23 @@ export const ModelingMachineProvider = ({ | ||||
|  | ||||
|             if (!sketchDetails) | ||||
|               return Promise.reject(new Error('No sketch details')) | ||||
|             const updatedPathToNode = updatePathToNodeFromMap( | ||||
|               sketchDetails.sketchPathToNode, | ||||
|               pathToNodeMap | ||||
|             ) | ||||
|  | ||||
|             const { | ||||
|               updatedSketchEntryNodePath, | ||||
|               updatedSketchNodePaths, | ||||
|               updatedPlaneNodePath, | ||||
|             } = updateSketchDetailsNodePaths({ | ||||
|               sketchEntryNodePath: sketchDetails.sketchEntryNodePath, | ||||
|               sketchNodePaths: sketchDetails.sketchNodePaths, | ||||
|               planeNodePath: sketchDetails.planeNodePath, | ||||
|               exprInsertIndex, | ||||
|             }) | ||||
|  | ||||
|             const updatedAst = | ||||
|               await sceneEntitiesManager.updateAstAndRejigSketch( | ||||
|                 updatedPathToNode, | ||||
|                 updatedSketchEntryNodePath, | ||||
|                 updatedSketchNodePaths, | ||||
|                 updatedPlaneNodePath, | ||||
|                 _modifiedAst, | ||||
|                 sketchDetails.zAxis, | ||||
|                 sketchDetails.yAxis, | ||||
| @ -902,7 +1025,9 @@ export const ModelingMachineProvider = ({ | ||||
|             return { | ||||
|               selectionType: 'completeSelection', | ||||
|               selection, | ||||
|               updatedPathToNode, | ||||
|               updatedSketchEntryNodePath, | ||||
|               updatedSketchNodePaths, | ||||
|               updatedPlaneNodePath, | ||||
|             } | ||||
|           } | ||||
|         ), | ||||
| @ -917,20 +1042,30 @@ export const ModelingMachineProvider = ({ | ||||
|               length: lengthValue, | ||||
|             }) | ||||
|             if (err(constraintResult)) return Promise.reject(constraintResult) | ||||
|             const { modifiedAst, pathToNodeMap } = constraintResult | ||||
|             const { modifiedAst, pathToNodeMap, exprInsertIndex } = | ||||
|               constraintResult | ||||
|             const pResult = parse(recast(modifiedAst)) | ||||
|             if (trap(pResult) || !resultIsOk(pResult)) | ||||
|               return Promise.reject(new Error('Unexpected compilation error')) | ||||
|             const _modifiedAst = pResult.program | ||||
|             if (!sketchDetails) | ||||
|               return Promise.reject(new Error('No sketch details')) | ||||
|             const updatedPathToNode = updatePathToNodeFromMap( | ||||
|               sketchDetails.sketchPathToNode, | ||||
|               pathToNodeMap | ||||
|             ) | ||||
|  | ||||
|             const { | ||||
|               updatedSketchEntryNodePath, | ||||
|               updatedSketchNodePaths, | ||||
|               updatedPlaneNodePath, | ||||
|             } = updateSketchDetailsNodePaths({ | ||||
|               sketchEntryNodePath: sketchDetails.sketchEntryNodePath, | ||||
|               sketchNodePaths: sketchDetails.sketchNodePaths, | ||||
|               planeNodePath: sketchDetails.planeNodePath, | ||||
|               exprInsertIndex, | ||||
|             }) | ||||
|             const updatedAst = | ||||
|               await sceneEntitiesManager.updateAstAndRejigSketch( | ||||
|                 updatedPathToNode, | ||||
|                 updatedSketchEntryNodePath, | ||||
|                 updatedSketchNodePaths, | ||||
|                 updatedPlaneNodePath, | ||||
|                 _modifiedAst, | ||||
|                 sketchDetails.zAxis, | ||||
|                 sketchDetails.yAxis, | ||||
| @ -951,13 +1086,15 @@ export const ModelingMachineProvider = ({ | ||||
|             return { | ||||
|               selectionType: 'completeSelection', | ||||
|               selection, | ||||
|               updatedPathToNode, | ||||
|               updatedSketchEntryNodePath, | ||||
|               updatedSketchNodePaths, | ||||
|               updatedPlaneNodePath, | ||||
|             } | ||||
|           } | ||||
|         ), | ||||
|         'Get perpendicular distance info': fromPromise( | ||||
|           async ({ input: { selectionRanges, sketchDetails } }) => { | ||||
|             const { modifiedAst, pathToNodeMap } = | ||||
|             const { modifiedAst, pathToNodeMap, exprInsertIndex } = | ||||
|               await applyConstraintIntersect({ | ||||
|                 selectionRanges, | ||||
|               }) | ||||
| @ -967,13 +1104,22 @@ export const ModelingMachineProvider = ({ | ||||
|             const _modifiedAst = pResult.program | ||||
|             if (!sketchDetails) | ||||
|               return Promise.reject(new Error('No sketch details')) | ||||
|             const updatedPathToNode = updatePathToNodeFromMap( | ||||
|               sketchDetails.sketchPathToNode, | ||||
|               pathToNodeMap | ||||
|             ) | ||||
|  | ||||
|             const { | ||||
|               updatedSketchEntryNodePath, | ||||
|               updatedSketchNodePaths, | ||||
|               updatedPlaneNodePath, | ||||
|             } = updateSketchDetailsNodePaths({ | ||||
|               sketchEntryNodePath: sketchDetails.sketchEntryNodePath, | ||||
|               sketchNodePaths: sketchDetails.sketchNodePaths, | ||||
|               planeNodePath: sketchDetails.planeNodePath, | ||||
|               exprInsertIndex, | ||||
|             }) | ||||
|             const updatedAst = | ||||
|               await sceneEntitiesManager.updateAstAndRejigSketch( | ||||
|                 updatedPathToNode, | ||||
|                 updatedSketchEntryNodePath, | ||||
|                 updatedSketchNodePaths, | ||||
|                 updatedPlaneNodePath, | ||||
|                 _modifiedAst, | ||||
|                 sketchDetails.zAxis, | ||||
|                 sketchDetails.yAxis, | ||||
| @ -994,13 +1140,15 @@ export const ModelingMachineProvider = ({ | ||||
|             return { | ||||
|               selectionType: 'completeSelection', | ||||
|               selection, | ||||
|               updatedPathToNode, | ||||
|               updatedSketchEntryNodePath, | ||||
|               updatedSketchNodePaths, | ||||
|               updatedPlaneNodePath, | ||||
|             } | ||||
|           } | ||||
|         ), | ||||
|         'Get ABS X info': fromPromise( | ||||
|           async ({ input: { selectionRanges, sketchDetails } }) => { | ||||
|             const { modifiedAst, pathToNodeMap } = | ||||
|             const { modifiedAst, pathToNodeMap, exprInsertIndex } = | ||||
|               await applyConstraintAbsDistance({ | ||||
|                 constraint: 'xAbs', | ||||
|                 selectionRanges, | ||||
| @ -1011,13 +1159,22 @@ export const ModelingMachineProvider = ({ | ||||
|             const _modifiedAst = pResult.program | ||||
|             if (!sketchDetails) | ||||
|               return Promise.reject(new Error('No sketch details')) | ||||
|             const updatedPathToNode = updatePathToNodeFromMap( | ||||
|               sketchDetails.sketchPathToNode, | ||||
|               pathToNodeMap | ||||
|             ) | ||||
|  | ||||
|             const { | ||||
|               updatedSketchEntryNodePath, | ||||
|               updatedSketchNodePaths, | ||||
|               updatedPlaneNodePath, | ||||
|             } = updateSketchDetailsNodePaths({ | ||||
|               sketchEntryNodePath: sketchDetails.sketchEntryNodePath, | ||||
|               sketchNodePaths: sketchDetails.sketchNodePaths, | ||||
|               planeNodePath: sketchDetails.planeNodePath, | ||||
|               exprInsertIndex, | ||||
|             }) | ||||
|             const updatedAst = | ||||
|               await sceneEntitiesManager.updateAstAndRejigSketch( | ||||
|                 updatedPathToNode, | ||||
|                 updatedSketchEntryNodePath, | ||||
|                 updatedSketchNodePaths, | ||||
|                 updatedPlaneNodePath, | ||||
|                 _modifiedAst, | ||||
|                 sketchDetails.zAxis, | ||||
|                 sketchDetails.yAxis, | ||||
| @ -1038,13 +1195,15 @@ export const ModelingMachineProvider = ({ | ||||
|             return { | ||||
|               selectionType: 'completeSelection', | ||||
|               selection, | ||||
|               updatedPathToNode, | ||||
|               updatedSketchEntryNodePath, | ||||
|               updatedSketchNodePaths, | ||||
|               updatedPlaneNodePath, | ||||
|             } | ||||
|           } | ||||
|         ), | ||||
|         'Get ABS Y info': fromPromise( | ||||
|           async ({ input: { selectionRanges, sketchDetails } }) => { | ||||
|             const { modifiedAst, pathToNodeMap } = | ||||
|             const { modifiedAst, pathToNodeMap, exprInsertIndex } = | ||||
|               await applyConstraintAbsDistance({ | ||||
|                 constraint: 'yAbs', | ||||
|                 selectionRanges, | ||||
| @ -1055,13 +1214,22 @@ export const ModelingMachineProvider = ({ | ||||
|             const _modifiedAst = pResult.program | ||||
|             if (!sketchDetails) | ||||
|               return Promise.reject(new Error('No sketch details')) | ||||
|             const updatedPathToNode = updatePathToNodeFromMap( | ||||
|               sketchDetails.sketchPathToNode, | ||||
|               pathToNodeMap | ||||
|             ) | ||||
|  | ||||
|             const { | ||||
|               updatedSketchEntryNodePath, | ||||
|               updatedSketchNodePaths, | ||||
|               updatedPlaneNodePath, | ||||
|             } = updateSketchDetailsNodePaths({ | ||||
|               sketchEntryNodePath: sketchDetails.sketchEntryNodePath, | ||||
|               sketchNodePaths: sketchDetails.sketchNodePaths, | ||||
|               planeNodePath: sketchDetails.planeNodePath, | ||||
|               exprInsertIndex, | ||||
|             }) | ||||
|             const updatedAst = | ||||
|               await sceneEntitiesManager.updateAstAndRejigSketch( | ||||
|                 updatedPathToNode, | ||||
|                 updatedSketchEntryNodePath, | ||||
|                 updatedSketchNodePaths, | ||||
|                 updatedPlaneNodePath, | ||||
|                 _modifiedAst, | ||||
|                 sketchDetails.zAxis, | ||||
|                 sketchDetails.yAxis, | ||||
| @ -1082,7 +1250,9 @@ export const ModelingMachineProvider = ({ | ||||
|             return { | ||||
|               selectionType: 'completeSelection', | ||||
|               selection, | ||||
|               updatedPathToNode, | ||||
|               updatedSketchEntryNodePath, | ||||
|               updatedSketchNodePaths, | ||||
|               updatedPlaneNodePath, | ||||
|             } | ||||
|           } | ||||
|         ), | ||||
| @ -1102,9 +1272,11 @@ export const ModelingMachineProvider = ({ | ||||
|             let result: { | ||||
|               modifiedAst: Node<Program> | ||||
|               pathToReplaced: PathToNode | null | ||||
|               exprInsertIndex: number | ||||
|             } = { | ||||
|               modifiedAst: parsed, | ||||
|               pathToReplaced: null, | ||||
|               exprInsertIndex: -1, | ||||
|             } | ||||
|             // If the user provided a constant name, | ||||
|             // we need to insert the named constant | ||||
| @ -1134,6 +1306,7 @@ export const ModelingMachineProvider = ({ | ||||
|               result = { | ||||
|                 modifiedAst: parseResultAfterInsertion.program, | ||||
|                 pathToReplaced: astAfterReplacement.pathToReplaced, | ||||
|                 exprInsertIndex: astAfterReplacement.exprInsertIndex, | ||||
|               } | ||||
|             } else if ('valueText' in data.namedValue) { | ||||
|               // If they didn't provide a constant name, | ||||
| @ -1164,10 +1337,22 @@ export const ModelingMachineProvider = ({ | ||||
|             parsed = parsed as Node<Program> | ||||
|             if (!result.pathToReplaced) | ||||
|               return Promise.reject(new Error('No path to replaced node')) | ||||
|             const { | ||||
|               updatedSketchEntryNodePath, | ||||
|               updatedSketchNodePaths, | ||||
|               updatedPlaneNodePath, | ||||
|             } = updateSketchDetailsNodePaths({ | ||||
|               sketchEntryNodePath: sketchDetails.sketchEntryNodePath, | ||||
|               sketchNodePaths: sketchDetails.sketchNodePaths, | ||||
|               planeNodePath: sketchDetails.planeNodePath, | ||||
|               exprInsertIndex: result.exprInsertIndex, | ||||
|             }) | ||||
|  | ||||
|             const updatedAst = | ||||
|               await sceneEntitiesManager.updateAstAndRejigSketch( | ||||
|                 result.pathToReplaced || [], | ||||
|                 updatedSketchEntryNodePath, | ||||
|                 updatedSketchNodePaths, | ||||
|                 updatedPlaneNodePath, | ||||
|                 parsed, | ||||
|                 sketchDetails.zAxis, | ||||
|                 sketchDetails.yAxis, | ||||
| @ -1188,7 +1373,191 @@ export const ModelingMachineProvider = ({ | ||||
|             return { | ||||
|               selectionType: 'completeSelection', | ||||
|               selection, | ||||
|               updatedPathToNode: result.pathToReplaced, | ||||
|               updatedSketchEntryNodePath, | ||||
|               updatedSketchNodePaths, | ||||
|               updatedPlaneNodePath, | ||||
|             } | ||||
|           } | ||||
|         ), | ||||
|         'set-up-draft-circle': fromPromise( | ||||
|           async ({ input: { sketchDetails, data } }) => { | ||||
|             if (!sketchDetails || !data) | ||||
|               return reject('No sketch details or data') | ||||
|             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, | ||||
|               expressionIndexToDelete: -1, | ||||
|             } 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) | ||||
|             let moddedAst: Program = structuredClone(kclManager.ast) | ||||
|             let pathToProfile = sketchDetails.sketchEntryNodePath | ||||
|             let updatedSketchNodePaths = sketchDetails.sketchNodePaths | ||||
|             if (doesNeedSplitting) { | ||||
|               const splitResult = splitPipedProfile( | ||||
|                 moddedAst, | ||||
|                 sketchDetails.sketchEntryNodePath | ||||
|               ) | ||||
|               if (err(splitResult)) return reject(splitResult) | ||||
|               moddedAst = splitResult.modifiedAst | ||||
|               pathToProfile = splitResult.pathToProfile | ||||
|               updatedSketchNodePaths = [pathToProfile] | ||||
|             } | ||||
|  | ||||
|             const indexToDelete = sketchDetails?.expressionIndexToDelete || -1 | ||||
|             if (indexToDelete >= 0) { | ||||
|               // this is the expression that was added when as sketch tool was used but not completed | ||||
|               // i.e first click for the center of the circle, but not the second click for the radius | ||||
|               // we added a circle to editor, but they bailed out early so we should remove it | ||||
|               moddedAst.body.splice(indexToDelete, 1) | ||||
|               // make sure the deleted expression is removed from the sketchNodePaths | ||||
|               updatedSketchNodePaths = updatedSketchNodePaths.filter( | ||||
|                 (path) => path[1][0] !== indexToDelete | ||||
|               ) | ||||
|               // if the deleted expression was the entryNodePath, we should just make it the first sketchNodePath | ||||
|               // as a safe default | ||||
|               pathToProfile = | ||||
|                 pathToProfile[1][0] !== indexToDelete | ||||
|                   ? pathToProfile | ||||
|                   : updatedSketchNodePaths[0] | ||||
|             } | ||||
|             await kclManager.executeAstMock(moddedAst) | ||||
|             await codeManager.updateEditorWithAstAndWriteToFile(moddedAst) | ||||
|             return { | ||||
|               updatedEntryNodePath: pathToProfile, | ||||
|               updatedSketchNodePaths: updatedSketchNodePaths, | ||||
|               updatedPlaneNodePath: sketchDetails.planeNodePath, | ||||
|               expressionIndexToDelete: -1, | ||||
|             } | ||||
|           } | ||||
|         ), | ||||
|  | ||||
| @ -13,12 +13,7 @@ import { | ||||
|   getOperationLabel, | ||||
|   stdLibMap, | ||||
| } from 'lib/operations' | ||||
| import { | ||||
|   codeManager, | ||||
|   editorManager, | ||||
|   engineCommandManager, | ||||
|   kclManager, | ||||
| } from 'lib/singletons' | ||||
| import { editorManager, engineCommandManager, kclManager } from 'lib/singletons' | ||||
| import { ComponentProps, useEffect, useMemo, useRef, useState } from 'react' | ||||
| import { Operation } from 'wasm-lib/kcl/bindings/Operation' | ||||
| import { Actor, Prop } from 'xstate' | ||||
| @ -67,7 +62,7 @@ export const FeatureTreePane = () => { | ||||
|               ) | ||||
|             : null | ||||
|  | ||||
|           if (!artifact || !('codeRef' in artifact)) { | ||||
|           if (!artifact) { | ||||
|             modelingSend({ | ||||
|               type: 'Set selection', | ||||
|               data: { | ||||
|  | ||||
| @ -2,7 +2,12 @@ import { SVGProps } from 'react' | ||||
|  | ||||
| export const Spinner = (props: SVGProps<SVGSVGElement>) => { | ||||
|   return ( | ||||
|     <svg viewBox="0 0 10 10" className={'w-8 h-8'} {...props}> | ||||
|     <svg | ||||
|       data-testid="spinner" | ||||
|       viewBox="0 0 10 10" | ||||
|       className={'w-8 h-8'} | ||||
|       {...props} | ||||
|     > | ||||
|       <circle | ||||
|         cx="5" | ||||
|         cy="5" | ||||
|  | ||||
| @ -136,6 +136,7 @@ export async function applyConstraintIntersect({ | ||||
| }): Promise<{ | ||||
|   modifiedAst: Node<Program> | ||||
|   pathToNodeMap: PathToNodeMap | ||||
|   exprInsertIndex: number | ||||
| }> { | ||||
|   const info = intersectInfo({ | ||||
|     selectionRanges, | ||||
| @ -174,6 +175,7 @@ export async function applyConstraintIntersect({ | ||||
|     return { | ||||
|       modifiedAst, | ||||
|       pathToNodeMap, | ||||
|       exprInsertIndex: -1, | ||||
|     } | ||||
|   } | ||||
|   // transform again but forcing certain values | ||||
| @ -192,6 +194,7 @@ export async function applyConstraintIntersect({ | ||||
|   const { modifiedAst: _modifiedAst, pathToNodeMap: _pathToNodeMap } = | ||||
|     transform2 | ||||
|  | ||||
|   let exprInsertIndex = -1 | ||||
|   if (variableName) { | ||||
|     const newBody = [..._modifiedAst.body] | ||||
|     newBody.splice( | ||||
| @ -204,9 +207,11 @@ export async function applyConstraintIntersect({ | ||||
|       const index = pathToNode.findIndex((a) => a[0] === 'body') + 1 | ||||
|       pathToNode[index][0] = Number(pathToNode[index][0]) + 1 | ||||
|     }) | ||||
|     exprInsertIndex = newVariableInsertIndex | ||||
|   } | ||||
|   return { | ||||
|     modifiedAst: _modifiedAst, | ||||
|     pathToNodeMap: _pathToNodeMap, | ||||
|     exprInsertIndex, | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -28,7 +28,7 @@ export function removeConstrainingValuesInfo({ | ||||
|   | Error { | ||||
|   const _nodes = selectionRanges.graphSelections.map(({ codeRef }) => { | ||||
|     const tmp = getNodeFromPath<Expr>(kclManager.ast, codeRef.pathToNode) | ||||
|     if (err(tmp)) return tmp | ||||
|     if (tmp instanceof Error) return tmp | ||||
|     return tmp.node | ||||
|   }) | ||||
|   const _err1 = _nodes.find(err) | ||||
|  | ||||
| @ -92,6 +92,7 @@ export async function applyConstraintAbsDistance({ | ||||
| }): Promise<{ | ||||
|   modifiedAst: Program | ||||
|   pathToNodeMap: PathToNodeMap | ||||
|   exprInsertIndex: number | ||||
| }> { | ||||
|   const info = absDistanceInfo({ | ||||
|     selectionRanges, | ||||
| @ -131,6 +132,7 @@ export async function applyConstraintAbsDistance({ | ||||
|   if (err(transform2)) return Promise.reject(transform2) | ||||
|   const { modifiedAst: _modifiedAst, pathToNodeMap } = transform2 | ||||
|  | ||||
|   let exprInsertIndex = -1 | ||||
|   if (variableName) { | ||||
|     const newBody = [..._modifiedAst.body] | ||||
|     newBody.splice( | ||||
| @ -143,8 +145,9 @@ export async function applyConstraintAbsDistance({ | ||||
|       const index = pathToNode.findIndex((a) => a[0] === 'body') + 1 | ||||
|       pathToNode[index][0] = Number(pathToNode[index][0]) + 1 | ||||
|     }) | ||||
|     exprInsertIndex = newVariableInsertIndex | ||||
|   } | ||||
|   return { modifiedAst: _modifiedAst, pathToNodeMap } | ||||
|   return { modifiedAst: _modifiedAst, pathToNodeMap, exprInsertIndex } | ||||
| } | ||||
|  | ||||
| export function applyConstraintAxisAlign({ | ||||
|  | ||||
| @ -86,6 +86,7 @@ export async function applyConstraintAngleBetween({ | ||||
| }): Promise<{ | ||||
|   modifiedAst: Program | ||||
|   pathToNodeMap: PathToNodeMap | ||||
|   exprInsertIndex: number | ||||
| }> { | ||||
|   const info = angleBetweenInfo({ selectionRanges }) | ||||
|   if (err(info)) return Promise.reject(info) | ||||
| @ -122,6 +123,7 @@ export async function applyConstraintAngleBetween({ | ||||
|     return { | ||||
|       modifiedAst, | ||||
|       pathToNodeMap, | ||||
|       exprInsertIndex: -1, | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @ -141,6 +143,7 @@ export async function applyConstraintAngleBetween({ | ||||
|   const { modifiedAst: _modifiedAst, pathToNodeMap: _pathToNodeMap } = | ||||
|     transformed2 | ||||
|  | ||||
|   let exprInsertIndex = -1 | ||||
|   if (variableName) { | ||||
|     const newBody = [..._modifiedAst.body] | ||||
|     newBody.splice( | ||||
| @ -153,9 +156,11 @@ export async function applyConstraintAngleBetween({ | ||||
|       const index = pathToNode.findIndex((a) => a[0] === 'body') + 1 | ||||
|       pathToNode[index][0] = Number(pathToNode[index][0]) + 1 | ||||
|     }) | ||||
|     exprInsertIndex = newVariableInsertIndex | ||||
|   } | ||||
|   return { | ||||
|     modifiedAst: _modifiedAst, | ||||
|     pathToNodeMap: _pathToNodeMap, | ||||
|     exprInsertIndex, | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -87,15 +87,13 @@ export function horzVertDistanceInfo({ | ||||
| export async function applyConstraintHorzVertDistance({ | ||||
|   selectionRanges, | ||||
|   constraint, | ||||
|   // TODO align will always be false (covered by synconous applyConstraintHorzVertAlign), remove it | ||||
|   isAlign = false, | ||||
| }: { | ||||
|   selectionRanges: Selections | ||||
|   constraint: 'setHorzDistance' | 'setVertDistance' | ||||
|   isAlign?: false | ||||
| }): Promise<{ | ||||
|   modifiedAst: Program | ||||
|   pathToNodeMap: PathToNodeMap | ||||
|   exprInsertIndex: number | ||||
| }> { | ||||
|   const info = horzVertDistanceInfo({ | ||||
|     selectionRanges: selectionRanges, | ||||
| @ -133,13 +131,12 @@ export async function applyConstraintHorzVertDistance({ | ||||
|     return { | ||||
|       modifiedAst, | ||||
|       pathToNodeMap, | ||||
|       exprInsertIndex: -1, | ||||
|     } | ||||
|   } else { | ||||
|     if (!isExprBinaryPart(valueNode)) | ||||
|       return Promise.reject('Invalid valueNode, is not a BinaryPart') | ||||
|     let finalValue = isAlign | ||||
|       ? createLiteral(0) | ||||
|       : removeDoubleNegatives(valueNode, sign, variableName) | ||||
|     let finalValue = removeDoubleNegatives(valueNode, sign, variableName) | ||||
|     // transform again but forcing certain values | ||||
|     const transformed = transformSecondarySketchLinesTagFirst({ | ||||
|       ast: kclManager.ast, | ||||
| @ -152,6 +149,7 @@ export async function applyConstraintHorzVertDistance({ | ||||
|  | ||||
|     if (err(transformed)) return Promise.reject(transformed) | ||||
|     const { modifiedAst: _modifiedAst, pathToNodeMap } = transformed | ||||
|     let exprInsertIndex = -1 | ||||
|     if (variableName) { | ||||
|       const newBody = [..._modifiedAst.body] | ||||
|       newBody.splice( | ||||
| @ -164,10 +162,12 @@ export async function applyConstraintHorzVertDistance({ | ||||
|         const index = pathToNode.findIndex((a) => a[0] === 'body') + 1 | ||||
|         pathToNode[index][0] = Number(pathToNode[index][0]) + 1 | ||||
|       }) | ||||
|       exprInsertIndex = newVariableInsertIndex | ||||
|     } | ||||
|     return { | ||||
|       modifiedAst: _modifiedAst, | ||||
|       pathToNodeMap, | ||||
|       exprInsertIndex, | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -74,10 +74,14 @@ export async function applyConstraintLength({ | ||||
| }: { | ||||
|   length: KclCommandValue | ||||
|   selectionRanges: Selections | ||||
| }) { | ||||
| }): Promise<{ | ||||
|   modifiedAst: Program | ||||
|   pathToNodeMap: PathToNodeMap | ||||
|   exprInsertIndex: number | ||||
| }> { | ||||
|   const ast = kclManager.ast | ||||
|   const angleLength = angleLengthInfo({ selectionRanges }) | ||||
|   if (err(angleLength)) return angleLength | ||||
|   if (err(angleLength)) return Promise.reject(angleLength) | ||||
|   const { transforms } = angleLength | ||||
|  | ||||
|   let distanceExpression: Expr = length.valueAst | ||||
| @ -98,7 +102,7 @@ export async function applyConstraintLength({ | ||||
|   } | ||||
|  | ||||
|   if (!isExprBinaryPart(distanceExpression)) { | ||||
|     return new Error('Invalid valueNode, is not a BinaryPart') | ||||
|     return Promise.reject('Invalid valueNode, is not a BinaryPart') | ||||
|   } | ||||
|  | ||||
|   const retval = transformAstSketchLines({ | ||||
| @ -116,6 +120,12 @@ export async function applyConstraintLength({ | ||||
|   return { | ||||
|     modifiedAst: _modifiedAst, | ||||
|     pathToNodeMap, | ||||
|     exprInsertIndex: | ||||
|       'variableName' in length && | ||||
|       length.variableName && | ||||
|       length.insertIndex !== undefined | ||||
|         ? length.insertIndex | ||||
|         : -1, | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -128,6 +138,7 @@ export async function applyConstraintAngleLength({ | ||||
| }): Promise<{ | ||||
|   modifiedAst: Program | ||||
|   pathToNodeMap: PathToNodeMap | ||||
|   exprInsertIndex: number | ||||
| }> { | ||||
|   const angleLength = angleLengthInfo({ selectionRanges, angleOrLength }) | ||||
|   if (err(angleLength)) return Promise.reject(angleLength) | ||||
| @ -212,5 +223,6 @@ export async function applyConstraintAngleLength({ | ||||
|   return { | ||||
|     modifiedAst: _modifiedAst, | ||||
|     pathToNodeMap, | ||||
|     exprInsertIndex: variableName ? newVariableInsertIndex : -1, | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -413,7 +413,6 @@ export class KclManager { | ||||
|     if (!isInterrupted) { | ||||
|       sceneInfra.modelingSend({ type: 'code edit during sketch' }) | ||||
|     } | ||||
|  | ||||
|     this.engineCommandManager.addCommandLog({ | ||||
|       type: 'execution-done', | ||||
|       data: null, | ||||
| @ -465,6 +464,7 @@ export class KclManager { | ||||
|  | ||||
|     this._logs = logs | ||||
|     this.addDiagnostics(kclErrorsToDiagnostics(errors)) | ||||
|  | ||||
|     this._execState = execState | ||||
|     this._variables = execState.variables | ||||
|     if (!errors.length) { | ||||
|  | ||||
| @ -27,6 +27,7 @@ export type ToolTip = | ||||
|   | 'angledLineThatIntersects' | ||||
|   | 'tangentialArcTo' | ||||
|   | 'circle' | ||||
|   | 'circleThreePoint' | ||||
|  | ||||
| export const toolTips: Array<ToolTip> = [ | ||||
|   'line', | ||||
| @ -42,6 +43,7 @@ export const toolTips: Array<ToolTip> = [ | ||||
|   'yLineTo', | ||||
|   'angledLineThatIntersects', | ||||
|   'tangentialArcTo', | ||||
|   'circleThreePoint', | ||||
| ] | ||||
|  | ||||
| export async function executeAst({ | ||||
| @ -71,7 +73,6 @@ export async function executeAst({ | ||||
|       : executeWithEngine(ast, engineCommandManager, path)) | ||||
|  | ||||
|     await engineCommandManager.waitForAllCommands() | ||||
|  | ||||
|     return { | ||||
|       logs: [], | ||||
|       errors: [], | ||||
|  | ||||
| @ -3,7 +3,6 @@ import { | ||||
|   recast, | ||||
|   initPromise, | ||||
|   Identifier, | ||||
|   SourceRange, | ||||
|   topLevelRange, | ||||
|   LiteralValue, | ||||
|   Literal, | ||||
| @ -25,6 +24,7 @@ import { | ||||
|   deleteSegmentFromPipeExpression, | ||||
|   removeSingleConstraintInfo, | ||||
|   deleteFromSelection, | ||||
|   splitPipedProfile, | ||||
| } from './modifyAst' | ||||
| import { enginelessExecutor } from '../lib/testHelpers' | ||||
| import { findUsesOfTagInPipe } from './queryAst' | ||||
| @ -996,3 +996,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) | ||||
|   }) | ||||
| }) | ||||
|  | ||||
| @ -21,8 +21,11 @@ import { | ||||
|   SourceRange, | ||||
|   sketchFromKclValue, | ||||
|   isPathToNodeNumber, | ||||
|   parse, | ||||
|   formatNumber, | ||||
|   ArtifactGraph, | ||||
|   VariableMap, | ||||
|   KclValue, | ||||
| } from './wasm' | ||||
| import { | ||||
|   isNodeSafeToReplacePath, | ||||
| @ -31,6 +34,8 @@ import { | ||||
|   getNodeFromPath, | ||||
|   isNodeSafeToReplace, | ||||
|   traverse, | ||||
|   getBodyIndex, | ||||
|   isCallExprWithName, | ||||
|   ARG_INDEX_FIELD, | ||||
|   LABELED_ARG_FIELD, | ||||
| } from './queryAst' | ||||
| @ -48,7 +53,7 @@ import { | ||||
|   transformAstSketchLines, | ||||
| } from './std/sketchcombos' | ||||
| import { DefaultPlaneStr } from 'lib/planes' | ||||
| import { isOverlap, roundOff } from 'lib/utils' | ||||
| import { isArray, isOverlap, roundOff } from 'lib/utils' | ||||
| import { KCL_DEFAULT_CONSTANT_PREFIXES } from 'lib/constants' | ||||
| import { SimplifiedArgDetails } from './std/stdTypes' | ||||
| import { TagDeclarator } from 'wasm-lib/kcl/bindings/TagDeclarator' | ||||
| @ -56,8 +61,19 @@ import { Models } from '@kittycad/lib' | ||||
| import { ExtrudeFacePlane } from 'machines/modelingMachine' | ||||
| import { Node } from 'wasm-lib/kcl/bindings/Node' | ||||
| import { KclExpressionWithVariable } from 'lib/commandTypes' | ||||
| import { | ||||
|   Artifact, | ||||
|   expandCap, | ||||
|   expandPlane, | ||||
|   expandWall, | ||||
|   getArtifactOfTypes, | ||||
|   getArtifactsOfTypes, | ||||
|   getPathsFromArtifact, | ||||
| } from './std/artifactGraph' | ||||
| import { BodyItem } from 'wasm-lib/kcl/bindings/BodyItem' | ||||
| import { findKwArg } from './util' | ||||
| import { deleteEdgeTreatment } from './modifyAst/addEdgeTreatment' | ||||
| import { engineCommandManager } from 'lib/singletons' | ||||
|  | ||||
| export function startSketchOnDefault( | ||||
|   node: Node<Program>, | ||||
| @ -90,41 +106,54 @@ export function startSketchOnDefault( | ||||
|   } | ||||
| } | ||||
|  | ||||
| export function addStartProfileAt( | ||||
| export function insertNewStartProfileAt( | ||||
|   node: Node<Program>, | ||||
|   pathToNode: PathToNode, | ||||
|   at: [number, number] | ||||
| ): { modifiedAst: Node<Program>; pathToNode: PathToNode } | Error { | ||||
|   const _node1 = getNodeFromPath<VariableDeclaration>( | ||||
|   sketchEntryNodePath: PathToNode, | ||||
|   sketchNodePaths: PathToNode[], | ||||
|   planeNodePath: PathToNode, | ||||
|   at: [number, number], | ||||
|   insertType: 'start' | 'end' = 'end' | ||||
| ): | ||||
|   | { | ||||
|       modifiedAst: Node<Program> | ||||
|       updatedSketchNodePaths: PathToNode[] | ||||
|       updatedEntryNodePath: PathToNode | ||||
|     } | ||||
|   | Error { | ||||
|   const varDec = getNodeFromPath<VariableDeclarator>( | ||||
|     node, | ||||
|     pathToNode, | ||||
|     'VariableDeclaration' | ||||
|     planeNodePath, | ||||
|     'VariableDeclarator' | ||||
|   ) | ||||
|   if (err(_node1)) return _node1 | ||||
|   const variableDeclaration = _node1.node | ||||
|   if (variableDeclaration.type !== 'VariableDeclaration') { | ||||
|     return new Error('variableDeclaration.init.type !== PipeExpression') | ||||
|   } | ||||
|   const _node = { ...node } | ||||
|   const init = variableDeclaration.declaration.init | ||||
|   const startProfileAt = createCallExpressionStdLib('startProfileAt', [ | ||||
|     createArrayExpression([ | ||||
|       createLiteral(roundOff(at[0])), | ||||
|       createLiteral(roundOff(at[1])), | ||||
|     ]), | ||||
|     createPipeSubstitution(), | ||||
|   ]) | ||||
|   if (init.type === 'PipeExpression') { | ||||
|     init.body.splice(1, 0, startProfileAt) | ||||
|   } else { | ||||
|     variableDeclaration.declaration.init = createPipeExpression([ | ||||
|       init, | ||||
|       startProfileAt, | ||||
|   if (err(varDec)) return varDec | ||||
|   if (varDec.node.type !== 'VariableDeclarator') return new Error('not a var') | ||||
|  | ||||
|   const newExpression = createVariableDeclaration( | ||||
|     findUniqueName(node, 'profile'), | ||||
|     createCallExpressionStdLib('startProfileAt', [ | ||||
|       createArrayExpression([ | ||||
|         createLiteral(roundOff(at[0])), | ||||
|         createLiteral(roundOff(at[1])), | ||||
|       ]), | ||||
|       createIdentifier(varDec.node.id.name), | ||||
|     ]) | ||||
|   } | ||||
|   ) | ||||
|   const insertIndex = getInsertIndex(sketchNodePaths, planeNodePath, insertType) | ||||
|  | ||||
|   const _node = structuredClone(node) | ||||
|   // TODO the rest of this function will not be robust to work for sketches defined within a function declaration | ||||
|   _node.body.splice(insertIndex, 0, newExpression) | ||||
|  | ||||
|   const { updatedEntryNodePath, updatedSketchNodePaths } = | ||||
|     updateSketchNodePathsWithInsertIndex({ | ||||
|       insertIndex, | ||||
|       insertType, | ||||
|       sketchNodePaths, | ||||
|     }) | ||||
|   return { | ||||
|     modifiedAst: _node, | ||||
|     pathToNode, | ||||
|     updatedSketchNodePaths, | ||||
|     updatedEntryNodePath, | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -224,8 +253,21 @@ export function mutateKwArg( | ||||
|   for (let i = 0; i < node.arguments.length; i++) { | ||||
|     const arg = node.arguments[i] | ||||
|     if (arg.label.name === label) { | ||||
|       node.arguments[i].arg = val | ||||
|       return true | ||||
|       if (isLiteralArrayOrStatic(val) && isLiteralArrayOrStatic(arg.arg)) { | ||||
|         node.arguments[i].arg = val | ||||
|         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)) | ||||
| @ -288,15 +330,17 @@ export function mutateObjExpProp( | ||||
| export function extrudeSketch({ | ||||
|   node, | ||||
|   pathToNode, | ||||
|   shouldPipe = false, | ||||
|   distance = createLiteral(4), | ||||
|   extrudeName, | ||||
|   artifact, | ||||
|   artifactGraph, | ||||
| }: { | ||||
|   node: Node<Program> | ||||
|   pathToNode: PathToNode | ||||
|   shouldPipe?: boolean | ||||
|   distance: Expr | ||||
|   extrudeName?: string | ||||
|   artifactGraph: ArtifactGraph | ||||
|   artifact?: Artifact | ||||
| }): | ||||
|   | { | ||||
|       modifiedAst: Node<Program> | ||||
| @ -304,10 +348,16 @@ export function extrudeSketch({ | ||||
|       pathToExtrudeArg: PathToNode | ||||
|     } | ||||
|   | Error { | ||||
|   const orderedSketchNodePaths = getPathsFromArtifact({ | ||||
|     artifact: artifact, | ||||
|     sketchPathToNode: pathToNode, | ||||
|     artifactGraph, | ||||
|     ast: node, | ||||
|   }) | ||||
|   if (err(orderedSketchNodePaths)) return orderedSketchNodePaths | ||||
|   const _node = structuredClone(node) | ||||
|   const _node1 = getNodeFromPath(_node, pathToNode) | ||||
|   if (err(_node1)) return _node1 | ||||
|   const { node: sketchExpression } = _node1 | ||||
|  | ||||
|   // determine if sketchExpression is in a pipeExpression or not | ||||
|   const _node2 = getNodeFromPath<PipeExpression>( | ||||
| @ -316,9 +366,6 @@ export function extrudeSketch({ | ||||
|     'PipeExpression' | ||||
|   ) | ||||
|   if (err(_node2)) return _node2 | ||||
|   const { node: pipeExpression } = _node2 | ||||
|  | ||||
|   const isInPipeExpression = pipeExpression.type === 'PipeExpression' | ||||
|  | ||||
|   const _node3 = getNodeFromPath<VariableDeclarator>( | ||||
|     _node, | ||||
| @ -326,54 +373,27 @@ export function extrudeSketch({ | ||||
|     'VariableDeclarator' | ||||
|   ) | ||||
|   if (err(_node3)) return _node3 | ||||
|   const { node: variableDeclarator, shallowPath: pathToDecleration } = _node3 | ||||
|   const { node: variableDeclarator } = _node3 | ||||
|  | ||||
|   const sketchToExtrude = shouldPipe | ||||
|     ? createPipeSubstitution() | ||||
|     : createIdentifier(variableDeclarator.id.name) | ||||
|   const extrudeCall = createCallExpressionStdLibKw('extrude', sketchToExtrude, [ | ||||
|     createLabeledArg('length', distance), | ||||
|   ]) | ||||
|   const extrudeCall = createCallExpressionStdLibKw( | ||||
|     'extrude', | ||||
|     createIdentifier(variableDeclarator.id.name), | ||||
|     [createLabeledArg('length', distance)] | ||||
|   ) | ||||
|   // index of the 'length' arg above. If you reorder the labeled args above, | ||||
|   // make sure to update this too. | ||||
|   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, | ||||
|   // but rather a separate constant for the extrusion | ||||
|   const name = | ||||
|     extrudeName ?? findUniqueName(node, KCL_DEFAULT_CONSTANT_PREFIXES.EXTRUDE) | ||||
|   const VariableDeclaration = createVariableDeclaration(name, extrudeCall) | ||||
|  | ||||
|   const sketchIndexInPathToNode = | ||||
|     pathToDecleration.findIndex((a) => a[0] === 'body') + 1 | ||||
|   const sketchIndexInBody = pathToDecleration[ | ||||
|     sketchIndexInPathToNode | ||||
|   ][0] as number | ||||
|   const lastSketchNodePath = | ||||
|     orderedSketchNodePaths[orderedSketchNodePaths.length - 1] | ||||
|  | ||||
|   const sketchIndexInBody = Number(lastSketchNodePath[1][0]) | ||||
|   _node.body.splice(sketchIndexInBody + 1, 0, VariableDeclaration) | ||||
|  | ||||
|   const pathToExtrudeArg: PathToNode = [ | ||||
| @ -1374,6 +1394,39 @@ export async function deleteFromSelection( | ||||
|     ({} as any) | ||||
| ): Promise<Node<Program> | Error> { | ||||
|   const astClone = structuredClone(ast) | ||||
|   if ( | ||||
|     (selection.artifact?.type === 'plane' || | ||||
|       selection.artifact?.type === 'cap' || | ||||
|       selection.artifact?.type === 'wall') && | ||||
|     selection.artifact?.pathIds?.length | ||||
|   ) { | ||||
|     const plane = | ||||
|       selection.artifact.type === 'plane' | ||||
|         ? expandPlane(selection.artifact, engineCommandManager.artifactGraph) | ||||
|         : selection.artifact.type === 'wall' | ||||
|         ? expandWall(selection.artifact, engineCommandManager.artifactGraph) | ||||
|         : expandCap(selection.artifact, engineCommandManager.artifactGraph) | ||||
|     for (const path of plane.paths.sort( | ||||
|       (a, b) => b.codeRef.range[0] - a.codeRef.range[0] | ||||
|     )) { | ||||
|       const varDec = getNodeFromPath<VariableDeclarator>( | ||||
|         ast, | ||||
|         path.codeRef.pathToNode, | ||||
|         'VariableDeclarator' | ||||
|       ) | ||||
|       if (err(varDec)) return varDec | ||||
|       const bodyIndex = Number(varDec.shallowPath[1][0]) | ||||
|       astClone.body.splice(bodyIndex, 1) | ||||
|     } | ||||
|     // If it's a cap, we're not going to continue and try to | ||||
|     // delete the extrusion | ||||
|     if ( | ||||
|       selection.artifact.type === 'cap' || | ||||
|       selection.artifact.type === 'wall' | ||||
|     ) { | ||||
|       return astClone | ||||
|     } | ||||
|   } | ||||
|   const varDec = getNodeFromPath<VariableDeclarator>( | ||||
|     ast, | ||||
|     selection?.codeRef?.pathToNode, | ||||
| @ -1452,59 +1505,108 @@ export async function deleteFromSelection( | ||||
|     if (extrudeNameToDelete) { | ||||
|       await new Promise((resolve) => { | ||||
|         ;(async () => { | ||||
|           let currentVariableName = '' | ||||
|           const pathsDependingOnExtrude: Array<{ | ||||
|             path: PathToNode | ||||
|             sketchName: string | ||||
|             variable: KclValue | ||||
|           }> = [] | ||||
|           traverse(astClone, { | ||||
|             leave: (node) => { | ||||
|               if (node.type === 'VariableDeclaration') { | ||||
|                 currentVariableName = '' | ||||
|               } | ||||
|             }, | ||||
|             enter: (node, path) => { | ||||
|               ;(async () => { | ||||
|                 if (node.type === 'VariableDeclaration') { | ||||
|                   currentVariableName = node.declaration.id.name | ||||
|                 } | ||||
|                 if ( | ||||
|                   // match startSketchOn(${extrudeNameToDelete}) | ||||
|                   node.type === 'CallExpression' && | ||||
|                   node.callee.name === 'startSketchOn' && | ||||
|                   node.arguments[0].type === 'Identifier' && | ||||
|                   node.arguments[0].name === extrudeNameToDelete | ||||
|                 ) { | ||||
|                   pathsDependingOnExtrude.push({ | ||||
|                     path, | ||||
|                     sketchName: currentVariableName, | ||||
|                   }) | ||||
|                 } | ||||
|               })().catch(reportRejection) | ||||
|             }, | ||||
|           }) | ||||
|           const roundLiteral = (x: number) => createLiteral(roundOff(x)) | ||||
|           const modificationDetails: { | ||||
|             parent: PipeExpression['body'] | ||||
|             parentPipe: PipeExpression['body'] | ||||
|             parentInit: VariableDeclarator | ||||
|             faceDetails: Models['FaceIsPlanar_type'] | ||||
|             lastKey: number | ||||
|             lastKey: number | string | ||||
|           }[] = [] | ||||
|           for (const { path, sketchName } of pathsDependingOnExtrude) { | ||||
|             const parent = getNodeFromPath<PipeExpression['body']>( | ||||
|           const wallArtifact = | ||||
|             selection.artifact?.type === 'wall' | ||||
|               ? selection.artifact | ||||
|               : selection.artifact?.type === 'segment' && | ||||
|                 selection.artifact.surfaceId | ||||
|               ? getArtifactOfTypes( | ||||
|                   { key: selection.artifact.surfaceId, types: ['wall'] }, | ||||
|                   engineCommandManager.artifactGraph | ||||
|                 ) | ||||
|               : null | ||||
|           if (err(wallArtifact)) return | ||||
|           if (wallArtifact) { | ||||
|             const sweep = getArtifactOfTypes( | ||||
|               { key: wallArtifact.sweepId, types: ['sweep'] }, | ||||
|               engineCommandManager.artifactGraph | ||||
|             ) | ||||
|             if (err(sweep)) return | ||||
|             const wallsWithDependencies = Array.from( | ||||
|               getArtifactsOfTypes( | ||||
|                 { keys: sweep.surfaceIds, types: ['wall', 'cap'] }, | ||||
|                 engineCommandManager.artifactGraph | ||||
|               ).values() | ||||
|             ).filter((wall) => wall?.pathIds?.length) | ||||
|             const wallIds = wallsWithDependencies.map((wall) => wall.id) | ||||
|             Object.entries(variables).forEach(([key, _var]) => { | ||||
|               if ( | ||||
|                 _var?.type === 'Face' && | ||||
|                 wallIds.includes(_var.value.artifactId) | ||||
|               ) { | ||||
|                 const pathToStartSketchOn = getNodePathFromSourceRange( | ||||
|                   astClone, | ||||
|                   _var.value.__meta[0].sourceRange | ||||
|                 ) | ||||
|                 pathsDependingOnExtrude.push({ | ||||
|                   path: pathToStartSketchOn, | ||||
|                   variable: _var, | ||||
|                 }) | ||||
|               } | ||||
|               if ( | ||||
|                 _var?.type === 'Sketch' && | ||||
|                 _var.value.on.type === 'face' && | ||||
|                 wallIds.includes(_var.value.on.artifactId) | ||||
|               ) { | ||||
|                 const pathToStartSketchOn = getNodePathFromSourceRange( | ||||
|                   astClone, | ||||
|                   _var.value.on.__meta[0].sourceRange | ||||
|                 ) | ||||
|                 pathsDependingOnExtrude.push({ | ||||
|                   path: pathToStartSketchOn, | ||||
|                   variable: { | ||||
|                     type: 'Face', | ||||
|                     value: _var.value.on, | ||||
|                   }, | ||||
|                 }) | ||||
|               } | ||||
|             }) | ||||
|           } | ||||
|           for (const { path, variable } of pathsDependingOnExtrude) { | ||||
|             // `parentPipe` and `parentInit` are the exact same node, but because it could either be an array or on object node | ||||
|             // putting them in two different variables was the only way to get TypeScript to stop complaining | ||||
|             // the reason why we're grabbing the parent and the last key is because we want to mutate the ast | ||||
|             // so `parent[lastKey]` does the trick, if there's a better way of doing this I'm all years | ||||
|             const parentPipe = getNodeFromPath<PipeExpression['body']>( | ||||
|               astClone, | ||||
|               path.slice(0, -1) | ||||
|             ) | ||||
|             if (err(parent)) { | ||||
|             const parentInit = getNodeFromPath<VariableDeclarator>( | ||||
|               astClone, | ||||
|               path.slice(0, -1) | ||||
|             ) | ||||
|             if (err(parentPipe) || err(parentInit)) { | ||||
|               return | ||||
|             } | ||||
|             const sketchToPreserve = sketchFromKclValue( | ||||
|               variables[sketchName], | ||||
|               sketchName | ||||
|             ) | ||||
|             if (err(sketchToPreserve)) return sketchToPreserve | ||||
|             if (!variable) return new Error('Could not find sketch') | ||||
|             const artifactId = | ||||
|               variable.type === 'Sketch' | ||||
|                 ? variable.value.artifactId | ||||
|                 : variable.type === 'Face' | ||||
|                 ? variable.value.artifactId | ||||
|                 : '' | ||||
|             if (!artifactId) return new Error('Sketch not on anything') | ||||
|             const onId = | ||||
|               variable.type === 'Sketch' | ||||
|                 ? variable.value.on.id | ||||
|                 : variable.type === 'Face' | ||||
|                 ? variable.value.id | ||||
|                 : '' | ||||
|             if (!onId) return new Error('Sketch not on anything') | ||||
|             // Can't kick off multiple requests at once as getFaceDetails | ||||
|             // is three engine calls in one and they conflict | ||||
|             const faceDetails = await getFaceDetails(sketchToPreserve.on.id) | ||||
|             const faceDetails = await getFaceDetails(onId) | ||||
|             if ( | ||||
|               !( | ||||
|                 faceDetails.origin && | ||||
| @ -1515,14 +1617,20 @@ export async function deleteFromSelection( | ||||
|             ) { | ||||
|               return | ||||
|             } | ||||
|             const lastKey = Number(path.slice(-1)[0][0]) | ||||
|             const lastKey = path.slice(-1)[0][0] | ||||
|             modificationDetails.push({ | ||||
|               parent: parent.node, | ||||
|               parentPipe: parentPipe.node, | ||||
|               parentInit: parentInit.node, | ||||
|               faceDetails, | ||||
|               lastKey, | ||||
|             }) | ||||
|           } | ||||
|           for (const { parent, faceDetails, lastKey } of modificationDetails) { | ||||
|           for (const { | ||||
|             parentInit, | ||||
|             parentPipe, | ||||
|             faceDetails, | ||||
|             lastKey, | ||||
|           } of modificationDetails) { | ||||
|             if ( | ||||
|               !( | ||||
|                 faceDetails.origin && | ||||
| @ -1533,7 +1641,7 @@ export async function deleteFromSelection( | ||||
|             ) { | ||||
|               continue | ||||
|             } | ||||
|             parent[lastKey] = createCallExpressionStdLib('startSketchOn', [ | ||||
|             const expression = createCallExpressionStdLib('startSketchOn', [ | ||||
|               createObjectExpression({ | ||||
|                 plane: createObjectExpression({ | ||||
|                   origin: createObjectExpression({ | ||||
| @ -1559,6 +1667,14 @@ export async function deleteFromSelection( | ||||
|                 }), | ||||
|               }), | ||||
|             ]) | ||||
|             if ( | ||||
|               parentInit.type === 'VariableDeclarator' && | ||||
|               lastKey === 'init' | ||||
|             ) { | ||||
|               parentInit[lastKey] = expression | ||||
|             } else if (isArray(parentPipe) && typeof lastKey === 'number') { | ||||
|               parentPipe[lastKey] = expression | ||||
|             } | ||||
|           } | ||||
|           resolve(true) | ||||
|         })().catch(reportRejection) | ||||
| @ -1570,15 +1686,29 @@ export async function deleteFromSelection( | ||||
|     return deleteEdgeTreatment(astClone, selection) | ||||
|   } else if (varDec.node.init.type === 'PipeExpression') { | ||||
|     const pipeBody = varDec.node.init.body | ||||
|     const doNotDeleteProfileIfItHasBeenExtruded = !( | ||||
|       selection?.artifact?.type === 'segment' && selection?.artifact?.surfaceId | ||||
|     ) | ||||
|     if ( | ||||
|       pipeBody[0].type === 'CallExpression' && | ||||
|       pipeBody[0].callee.name === 'startSketchOn' | ||||
|       doNotDeleteProfileIfItHasBeenExtruded && | ||||
|       (pipeBody[0].callee.name === 'startSketchOn' || | ||||
|         pipeBody[0].callee.name === 'startProfileAt') | ||||
|     ) { | ||||
|       // remove varDec | ||||
|       const varDecIndex = varDec.shallowPath[1][0] as number | ||||
|       astClone.body.splice(varDecIndex, 1) | ||||
|       return astClone | ||||
|     } | ||||
|   } else if ( | ||||
|     // single expression profiles | ||||
|     (varDec.node.init.type === 'CallExpressionKw' || | ||||
|       varDec.node.init.type === 'CallExpression') && | ||||
|     ['circleThreePoint', 'circle'].includes(varDec.node.init.callee.name) | ||||
|   ) { | ||||
|     const varDecIndex = varDec.shallowPath[1][0] as number | ||||
|     astClone.body.splice(varDecIndex, 1) | ||||
|     return astClone | ||||
|   } | ||||
|  | ||||
|   return new Error('Selection not recognised, could not delete') | ||||
| @ -1588,6 +1718,167 @@ const nonCodeMetaEmpty = () => { | ||||
|   return { nonCodeNodes: {}, startNodes: [], start: 0, end: 0 } | ||||
| } | ||||
|  | ||||
| export const createLabeledArg = (name: string, arg: Expr): LabeledArg => { | ||||
|   return { label: createIdentifier(name), arg, type: 'LabeledArg' } | ||||
| export function getInsertIndex( | ||||
|   sketchNodePaths: PathToNode[], | ||||
|   planeNodePath: PathToNode, | ||||
|   insertType: 'start' | 'end' | ||||
| ) { | ||||
|   let minIndex = 0 | ||||
|   let maxIndex = 0 | ||||
|   for (const path of sketchNodePaths) { | ||||
|     const index = Number(path[1][0]) | ||||
|     if (index < minIndex) minIndex = index | ||||
|     if (index > maxIndex) maxIndex = index | ||||
|   } | ||||
|  | ||||
|   const insertIndex = !sketchNodePaths.length | ||||
|     ? Number(planeNodePath[1][0]) + 1 | ||||
|     : insertType === 'start' | ||||
|     ? minIndex | ||||
|     : maxIndex + 1 | ||||
|   return insertIndex | ||||
| } | ||||
|  | ||||
| export function updateSketchNodePathsWithInsertIndex({ | ||||
|   insertIndex, | ||||
|   insertType, | ||||
|   sketchNodePaths, | ||||
| }: { | ||||
|   insertIndex: number | ||||
|   insertType: 'start' | 'end' | ||||
|   sketchNodePaths: PathToNode[] | ||||
| }): { | ||||
|   updatedEntryNodePath: PathToNode | ||||
|   updatedSketchNodePaths: PathToNode[] | ||||
| } { | ||||
|   // TODO the rest of this function will not be robust to work for sketches defined within a function declaration | ||||
|   const newExpressionPathToNode: PathToNode = [ | ||||
|     ['body', ''], | ||||
|     [insertIndex, 'index'], | ||||
|     ['declaration', 'VariableDeclaration'], | ||||
|     ['init', 'VariableDeclarator'], | ||||
|   ] | ||||
|   let updatedSketchNodePaths = structuredClone(sketchNodePaths) | ||||
|   if (insertType === 'start') { | ||||
|     updatedSketchNodePaths = updatedSketchNodePaths.map((path) => { | ||||
|       path[1][0] = Number(path[1][0]) + 1 | ||||
|       return path | ||||
|     }) | ||||
|     updatedSketchNodePaths.unshift(newExpressionPathToNode) | ||||
|   } else { | ||||
|     updatedSketchNodePaths.push(newExpressionPathToNode) | ||||
|   } | ||||
|   return { | ||||
|     updatedSketchNodePaths, | ||||
|     updatedEntryNodePath: newExpressionPathToNode, | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  *  | ||||
|  * Split the following pipe expression into  | ||||
|  * ```ts | ||||
|  * part001 = startSketchOn('XZ') | ||||
|   |> startProfileAt([1, 2], %) | ||||
|   |> line([3, 4], %) | ||||
|   |> line([5, 6], %) | ||||
|   |> close(%) | ||||
| extrude001 = extrude(5, part001) | ||||
| ``` | ||||
| into | ||||
| ```ts | ||||
| sketch001 = startSketchOn('XZ') | ||||
| part001 = startProfileAt([1, 2], sketch001) | ||||
|   |> line([3, 4], %) | ||||
|   |> line([5, 6], %) | ||||
|   |> close(%) | ||||
| extrude001 = extrude(5, part001) | ||||
| ``` | ||||
| Notice that the `startSketchOn` is what gets the new variable name, this is so part001 still has the same data as before | ||||
| making it safe for later code that uses part001 (the extrude in this example) | ||||
|  *  | ||||
|  */ | ||||
| export function splitPipedProfile( | ||||
|   ast: Program, | ||||
|   pathToPipe: PathToNode | ||||
| ): | ||||
|   | { | ||||
|       modifiedAst: Program | ||||
|       pathToProfile: PathToNode | ||||
|       pathToPlane: PathToNode | ||||
|     } | ||||
|   | Error { | ||||
|   const _ast = structuredClone(ast) | ||||
|   const varDec = getNodeFromPath<VariableDeclaration>( | ||||
|     _ast, | ||||
|     pathToPipe, | ||||
|     'VariableDeclaration' | ||||
|   ) | ||||
|   if (err(varDec)) return varDec | ||||
|   if ( | ||||
|     varDec.node.type !== 'VariableDeclaration' || | ||||
|     varDec.node.declaration.init.type !== 'PipeExpression' | ||||
|   ) { | ||||
|     return new Error('pathToNode does not point to pipe') | ||||
|   } | ||||
|   const init = varDec.node.declaration.init | ||||
|   const firstCall = init.body[0] | ||||
|   if (!isCallExprWithName(firstCall, 'startSketchOn')) | ||||
|     return new Error('First call is not startSketchOn') | ||||
|   const secondCall = init.body[1] | ||||
|   if (!isCallExprWithName(secondCall, 'startProfileAt')) | ||||
|     return new Error('Second call is not startProfileAt') | ||||
|  | ||||
|   const varName = varDec.node.declaration.id.name | ||||
|   const newVarName = findUniqueName(_ast, 'sketch') | ||||
|   const secondCallArgs = structuredClone(secondCall.arguments) | ||||
|   secondCallArgs[1] = createIdentifier(newVarName) | ||||
|   const firstCallOfNewPipe = createCallExpression( | ||||
|     'startProfileAt', | ||||
|     secondCallArgs | ||||
|   ) | ||||
|   const newSketch = createVariableDeclaration( | ||||
|     newVarName, | ||||
|     varDec.node.declaration.init.body[0] | ||||
|   ) | ||||
|   const newProfile = createVariableDeclaration( | ||||
|     varName, | ||||
|     varDec.node.declaration.init.body.length <= 2 | ||||
|       ? firstCallOfNewPipe | ||||
|       : createPipeExpression([ | ||||
|           firstCallOfNewPipe, | ||||
|           ...varDec.node.declaration.init.body.slice(2), | ||||
|         ]) | ||||
|   ) | ||||
|   const index = getBodyIndex(pathToPipe) | ||||
|   if (err(index)) return index | ||||
|   _ast.body.splice(index, 1, newSketch, newProfile) | ||||
|   const pathToPlane = structuredClone(pathToPipe) | ||||
|   const pathToProfile = structuredClone(pathToPipe) | ||||
|   pathToProfile[1][0] = index + 1 | ||||
|  | ||||
|   return { | ||||
|     modifiedAst: _ast, | ||||
|     pathToProfile, | ||||
|     pathToPlane, | ||||
|   } | ||||
| } | ||||
|  | ||||
| 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,9 +5,9 @@ import { | ||||
|   PathToNode, | ||||
|   Expr, | ||||
|   CallExpression, | ||||
|   PipeExpression, | ||||
|   VariableDeclarator, | ||||
|   CallExpressionKw, | ||||
|   ArtifactGraph, | ||||
| } from 'lang/wasm' | ||||
| import { Selections } from 'lib/selections' | ||||
| import { Node } from 'wasm-lib/kcl/bindings/Node' | ||||
| @ -16,7 +16,6 @@ import { | ||||
|   createCallExpressionStdLib, | ||||
|   createObjectExpression, | ||||
|   createIdentifier, | ||||
|   createPipeExpression, | ||||
|   findUniqueName, | ||||
|   createVariableDeclaration, | ||||
| } from 'lang/modifyAst' | ||||
| @ -26,14 +25,18 @@ import { | ||||
|   mutateAstWithTagForSketchSegment, | ||||
|   getEdgeTagCall, | ||||
| } from 'lang/modifyAst/addEdgeTreatment' | ||||
| import { Artifact, getPathsFromArtifact } from 'lang/std/artifactGraph' | ||||
| import { kclManager } from 'lib/singletons' | ||||
|  | ||||
| export function revolveSketch( | ||||
|   ast: Node<Program>, | ||||
|   pathToSketchNode: PathToNode, | ||||
|   shouldPipe = false, | ||||
|   angle: Expr = createLiteral(4), | ||||
|   axisOrEdge: string, | ||||
|   axis: string, | ||||
|   edge: Selections | ||||
|   edge: Selections, | ||||
|   artifactGraph: ArtifactGraph, | ||||
|   artifact?: Artifact | ||||
| ): | ||||
|   | { | ||||
|       modifiedAst: Node<Program> | ||||
| @ -41,6 +44,13 @@ export function revolveSketch( | ||||
|       pathToRevolveArg: PathToNode | ||||
|     } | ||||
|   | Error { | ||||
|   const orderedSketchNodePaths = getPathsFromArtifact({ | ||||
|     artifact: artifact, | ||||
|     sketchPathToNode: pathToSketchNode, | ||||
|     artifactGraph, | ||||
|     ast: kclManager.ast, | ||||
|   }) | ||||
|   if (err(orderedSketchNodePaths)) return orderedSketchNodePaths | ||||
|   const clonedAst = structuredClone(ast) | ||||
|   const sketchNode = getNodeFromPath(clonedAst, pathToSketchNode) | ||||
|   if (err(sketchNode)) return sketchNode | ||||
| @ -82,29 +92,13 @@ export function revolveSketch( | ||||
|     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>( | ||||
|     clonedAst, | ||||
|     pathToSketchNode, | ||||
|     'VariableDeclarator' | ||||
|   ) | ||||
|   if (err(sketchVariableDeclaratorNode)) return sketchVariableDeclaratorNode | ||||
|   const { | ||||
|     node: sketchVariableDeclarator, | ||||
|     shallowPath: sketchPathToDecleration, | ||||
|   } = sketchVariableDeclaratorNode | ||||
|   const { node: sketchVariableDeclarator } = sketchVariableDeclaratorNode | ||||
|  | ||||
|   if (!generatedAxis) return new Error('Generated axis selection is missing.') | ||||
|  | ||||
| @ -116,41 +110,16 @@ export function revolveSketch( | ||||
|     createIdentifier(sketchVariableDeclarator.id.name), | ||||
|   ]) | ||||
|  | ||||
|   if (shouldPipe) { | ||||
|     const pipeChain = createPipeExpression( | ||||
|       isInPipeExpression | ||||
|         ? [...sketchPipeExpression.body, revolveCall] | ||||
|         : [sketchExpression as any, revolveCall] | ||||
|     ) | ||||
|  | ||||
|     sketchVariableDeclarator.init = pipeChain | ||||
|     const pathToRevolveArg: PathToNode = [ | ||||
|       ...sketchPathToDecleration, | ||||
|       ['init', 'VariableDeclarator'], | ||||
|       ['body', ''], | ||||
|       [pipeChain.body.length - 1, 'index'], | ||||
|       ['arguments', 'CallExpression'], | ||||
|       [0, 'index'], | ||||
|     ] | ||||
|  | ||||
|     return { | ||||
|       modifiedAst: clonedAst, | ||||
|       pathToSketchNode, | ||||
|       pathToRevolveArg, | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // We're not creating a pipe expression, | ||||
|   // but rather a separate constant for the extrusion | ||||
|   const name = findUniqueName(clonedAst, KCL_DEFAULT_CONSTANT_PREFIXES.REVOLVE) | ||||
|   const VariableDeclaration = createVariableDeclaration(name, revolveCall) | ||||
|   const sketchIndexInPathToNode = | ||||
|     sketchPathToDecleration.findIndex((a) => a[0] === 'body') + 1 | ||||
|   const sketchIndexInBody = sketchPathToDecleration[sketchIndexInPathToNode][0] | ||||
|   let insertIndex = sketchIndexInBody | ||||
|  | ||||
|   if (typeof insertIndex !== 'number') | ||||
|     return new Error('expected insertIndex to be a number') | ||||
|   const lastSketchNodePath = | ||||
|     orderedSketchNodePaths[orderedSketchNodePaths.length - 1] | ||||
|   let sketchIndexInBody = Number(lastSketchNodePath[1][0]) | ||||
|   if (typeof sketchIndexInBody !== 'number') { | ||||
|     return new Error('expected sketchIndexInBody to be a number') | ||||
|   } | ||||
|  | ||||
|   // If an axis was selected in KCL, find the max index to insert the revolve command | ||||
|   if (axisDeclaration) { | ||||
| @ -161,14 +130,14 @@ export function revolveSketch( | ||||
|     if (typeof axisIndex !== 'number') | ||||
|       return new Error('expected axisIndex to be a number') | ||||
|  | ||||
|     insertIndex = Math.max(insertIndex, axisIndex) | ||||
|     sketchIndexInBody = Math.max(sketchIndexInBody, axisIndex) | ||||
|   } | ||||
|  | ||||
|   clonedAst.body.splice(insertIndex + 1, 0, VariableDeclaration) | ||||
|   clonedAst.body.splice(sketchIndexInBody + 1, 0, VariableDeclaration) | ||||
|  | ||||
|   const pathToRevolveArg: PathToNode = [ | ||||
|     ['body', ''], | ||||
|     [insertIndex + 1, 'index'], | ||||
|     [sketchIndexInBody + 1, 'index'], | ||||
|     ['declaration', 'VariableDeclaration'], | ||||
|     ['init', 'VariableDeclarator'], | ||||
|     ['arguments', 'CallExpression'], | ||||
|  | ||||
| @ -2,7 +2,6 @@ import { ToolTip } from 'lang/langHelpers' | ||||
| import { Selection, Selections } from 'lib/selections' | ||||
| import { | ||||
|   ArrayExpression, | ||||
|   ArtifactGraph, | ||||
|   BinaryExpression, | ||||
|   CallExpression, | ||||
|   CallExpressionKw, | ||||
| @ -22,6 +21,7 @@ import { | ||||
|   VariableDeclaration, | ||||
|   VariableDeclarator, | ||||
|   recast, | ||||
|   ArtifactGraph, | ||||
|   kclSettings, | ||||
|   unitLenToUnitLength, | ||||
|   unitAngToUnitAngle, | ||||
| @ -37,10 +37,11 @@ import { | ||||
|   getConstraintType, | ||||
| } from './std/sketchcombos' | ||||
| import { err, Reason } from 'lib/trap' | ||||
| import { ImportStatement } from 'wasm-lib/kcl/bindings/ImportStatement' | ||||
| import { Node } from 'wasm-lib/kcl/bindings/Node' | ||||
| import { findKwArg } from './util' | ||||
| import { codeRefFromRange } from './std/artifactGraph' | ||||
| import { FunctionExpression } from 'wasm-lib/kcl/bindings/FunctionExpression' | ||||
| import { ImportStatement } from 'wasm-lib/kcl/bindings/ImportStatement' | ||||
| import { KclSettingsAnnotation } from 'lib/settings/settingsTypes' | ||||
|  | ||||
| export const LABELED_ARG_FIELD = 'LabeledArg -> Arg' | ||||
| @ -357,7 +358,13 @@ export function findAllPreviousVariables( | ||||
| type ReplacerFn = ( | ||||
|   _ast: Node<Program>, | ||||
|   varName: string | ||||
| ) => { modifiedAst: Node<Program>; pathToReplaced: PathToNode } | Error | ||||
| ) => | ||||
|   | { | ||||
|       modifiedAst: Node<Program> | ||||
|       pathToReplaced: PathToNode | ||||
|       exprInsertIndex: number | ||||
|     } | ||||
|   | Error | ||||
|  | ||||
| export function isNodeSafeToReplacePath( | ||||
|   ast: Program, | ||||
| @ -409,7 +416,7 @@ export function isNodeSafeToReplacePath( | ||||
|     if (err(_nodeToReplace)) return _nodeToReplace | ||||
|     const nodeToReplace = _nodeToReplace.node as any | ||||
|     nodeToReplace[last[0]] = identifier | ||||
|     return { modifiedAst: _ast, pathToReplaced } | ||||
|     return { modifiedAst: _ast, pathToReplaced, exprInsertIndex: index } | ||||
|   } | ||||
|  | ||||
|   const hasPipeSub = isTypeInValue(finVal as Expr, 'PipeSubstitution') | ||||
| @ -518,8 +525,15 @@ export function isLinesParallelAndConstrained( | ||||
|     if (err(_primarySegment)) return _primarySegment | ||||
|     const primarySegment = _primarySegment.segment | ||||
|  | ||||
|     const _varDec2 = getNodeFromPath(ast, secondaryPath, 'VariableDeclaration') | ||||
|     if (err(_varDec2)) return _varDec2 | ||||
|     const varDec2 = _varDec2.node | ||||
|     const varName2 = (varDec2 as VariableDeclaration)?.declaration.id?.name | ||||
|     const sg2 = sketchFromKclValue(memVars[varName2], varName2) | ||||
|     if (err(sg2)) return sg2 | ||||
|  | ||||
|     const _segment = getSketchSegmentFromSourceRange( | ||||
|       sg, | ||||
|       sg2, | ||||
|       secondaryLine?.codeRef?.range | ||||
|     ) | ||||
|     if (err(_segment)) return _segment | ||||
| @ -871,6 +885,59 @@ export function getObjExprProperty( | ||||
|   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 | ||||
| } | ||||
| /** | ||||
|  * Given KCL, returns the settings annotation object if it exists. | ||||
|  */ | ||||
|  | ||||
| @ -82,6 +82,7 @@ function moreNodePathFromSourceRange( | ||||
|           return moreNodePathFromSourceRange(arg, sourceRange, path) | ||||
|         } | ||||
|       } | ||||
|       return path | ||||
|     } | ||||
|     return path | ||||
|   } | ||||
|  | ||||
| @ -1,4 +1,5 @@ | ||||
| import { | ||||
|   Expr, | ||||
|   Artifact, | ||||
|   ArtifactGraph, | ||||
|   ArtifactId, | ||||
| @ -18,7 +19,8 @@ import { | ||||
| import { Models } from '@kittycad/lib' | ||||
| import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils' | ||||
| import { err } from 'lib/trap' | ||||
| import { codeManager } from 'lib/singletons' | ||||
| import { Cap, Plane, Wall } from 'wasm-lib/kcl/bindings/Artifact' | ||||
| import { CapSubType } from 'wasm-lib/kcl/bindings/Artifact' | ||||
|  | ||||
| export type { Artifact, ArtifactId, SegmentArtifact } from 'lang/wasm' | ||||
|  | ||||
| @ -37,10 +39,28 @@ export interface PlaneArtifactRich extends BaseArtifact { | ||||
|   codeRef: CodeRef | ||||
| } | ||||
|  | ||||
| export interface CapArtifactRich extends BaseArtifact { | ||||
|   type: 'cap' | ||||
|   subType: CapSubType | ||||
|   faceCodeRef: CodeRef | ||||
|   edgeCuts: Array<EdgeCut> | ||||
|   paths: Array<PathArtifact> | ||||
|   sweep?: SweepArtifact | ||||
| } | ||||
| export interface WallArtifactRich extends BaseArtifact { | ||||
|   type: 'wall' | ||||
|   id: ArtifactId | ||||
|   segment: PathArtifact | ||||
|   edgeCuts: Array<EdgeCut> | ||||
|   sweep: SweepArtifact | ||||
|   paths: Array<PathArtifact> | ||||
|   faceCodeRef: CodeRef | ||||
| } | ||||
|  | ||||
| export interface PathArtifactRich extends BaseArtifact { | ||||
|   type: 'path' | ||||
|   /** A path must always lie on a plane */ | ||||
|   plane: PlaneArtifact | WallArtifact | ||||
|   plane: PlaneArtifact | WallArtifact | CapArtifact | ||||
|   /** A path must always contain 0 or more segments */ | ||||
|   segments: Array<SegmentArtifact> | ||||
|   /** A path may not result in a sweep artifact */ | ||||
| @ -51,7 +71,7 @@ export interface PathArtifactRich extends BaseArtifact { | ||||
| interface SegmentArtifactRich extends BaseArtifact { | ||||
|   type: 'segment' | ||||
|   path: PathArtifact | ||||
|   surf?: WallArtifact | ||||
|   surf: WallArtifact | ||||
|   edges: Array<SweepEdge> | ||||
|   edgeCut?: EdgeCut | ||||
|   codeRef: CodeRef | ||||
| @ -151,6 +171,73 @@ export function expandPlane( | ||||
|   } | ||||
| } | ||||
|  | ||||
| export function expandWall( | ||||
|   wall: WallArtifact, | ||||
|   artifactGraph: ArtifactGraph | ||||
| ): WallArtifactRich { | ||||
|   const { pathIds, sweepId: _s, edgeCutEdgeIds, ...keptProperties } = wall | ||||
|   const paths = pathIds?.length | ||||
|     ? Array.from( | ||||
|         getArtifactsOfTypes( | ||||
|           { keys: wall.pathIds, types: ['path'] }, | ||||
|           artifactGraph | ||||
|         ).values() | ||||
|       ) | ||||
|     : [] | ||||
|   const sweep = artifactGraph.get(wall.sweepId) as SweepArtifact | ||||
|   const edgeCuts = edgeCutEdgeIds?.length | ||||
|     ? Array.from( | ||||
|         getArtifactsOfTypes( | ||||
|           { keys: wall.edgeCutEdgeIds, types: ['edgeCut'] }, | ||||
|           artifactGraph | ||||
|         ).values() | ||||
|       ) | ||||
|     : [] | ||||
|   const segment = artifactGraph.get(wall.segId) as PathArtifact | ||||
|   return { | ||||
|     type: 'wall', | ||||
|     ...keptProperties, | ||||
|     paths, | ||||
|     sweep, | ||||
|     segment, | ||||
|     edgeCuts, | ||||
|   } | ||||
| } | ||||
| export function expandCap( | ||||
|   cap: CapArtifact, | ||||
|   artifactGraph: ArtifactGraph | ||||
| ): CapArtifactRich { | ||||
|   const { pathIds, sweepId: _s, edgeCutEdgeIds, ...keptProperties } = cap | ||||
|   const paths = pathIds?.length | ||||
|     ? Array.from( | ||||
|         getArtifactsOfTypes( | ||||
|           { keys: cap.pathIds, types: ['path'] }, | ||||
|           artifactGraph | ||||
|         ).values() | ||||
|       ) | ||||
|     : [] | ||||
|   const maybeSweep = getArtifactOfTypes( | ||||
|     { key: cap.sweepId, types: ['sweep'] }, | ||||
|     artifactGraph | ||||
|   ) | ||||
|   const sweep = err(maybeSweep) ? undefined : maybeSweep | ||||
|   const edgeCuts = edgeCutEdgeIds?.length | ||||
|     ? Array.from( | ||||
|         getArtifactsOfTypes( | ||||
|           { keys: cap.edgeCutEdgeIds, types: ['edgeCut'] }, | ||||
|           artifactGraph | ||||
|         ).values() | ||||
|       ) | ||||
|     : [] | ||||
|   return { | ||||
|     type: 'cap', | ||||
|     ...keptProperties, | ||||
|     paths, | ||||
|     sweep, | ||||
|     edgeCuts, | ||||
|   } | ||||
| } | ||||
|  | ||||
| export function expandPath( | ||||
|   path: PathArtifact, | ||||
|   artifactGraph: ArtifactGraph | ||||
| @ -239,6 +326,7 @@ export function expandSegment( | ||||
|   if (err(path)) return path | ||||
|   if (err(surf)) return surf | ||||
|   if (err(edgeCut)) return edgeCut | ||||
|   if (!surf) return new Error('Segment does not have a surface') | ||||
|  | ||||
|   return { | ||||
|     type: 'segment', | ||||
| @ -410,6 +498,186 @@ 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 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 === 'wall' || artifact.type === 'cap') return artifact | ||||
|   if (artifact.type === 'sweepEdge') | ||||
|     return getPlaneFromSweepEdge(artifact, graph) | ||||
|   return new Error(`Artifact type ${artifact.type} does not have a plane`) | ||||
| } | ||||
|  | ||||
| const onlyConsecutivePaths = ( | ||||
|   orderedNodePaths: PathToNode[], | ||||
|   originalPath: PathToNode, | ||||
|   ast: Program | ||||
| ): PathToNode[] => { | ||||
|   const isExprSafe = (index: number, ast: Program): boolean => { | ||||
|     // we allow expressions between profiles, but only basic math expressions 5 + 6 etc | ||||
|     // because 5 + doSomeMath() might be okay, but we can't know if it's an abstraction on a stdlib | ||||
|     // call that involves a engine call, and we can't have that in sketch-mode/mock-execution | ||||
|     const expr = 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 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, ast)) { | ||||
|       break | ||||
|     } | ||||
|   } | ||||
|   for (let i = originalIndex - 1; i >= minIndex; i--) { | ||||
|     if (pathIndexMap[i]) { | ||||
|       safePaths.unshift(pathIndexMap[i]) | ||||
|     } else if (!isExprSafe(i, ast)) { | ||||
|       break | ||||
|     } | ||||
|   } | ||||
|   return safePaths | ||||
| } | ||||
|  | ||||
| export function getPathsFromPlaneArtifact( | ||||
|   planeArtifact: PlaneArtifact, | ||||
|   artifactGraph: ArtifactGraph, | ||||
|   ast: Program | ||||
| ): PathToNode[] { | ||||
|   const nodePaths: PathToNode[] = [] | ||||
|   for (const pathId of planeArtifact.pathIds) { | ||||
|     const path = 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(ast, path.codeRef.range) | ||||
|           : path.codeRef.pathToNode | ||||
|       ) | ||||
|     } | ||||
|   } | ||||
|   return onlyConsecutivePaths(nodePaths, nodePaths[0], ast) | ||||
| } | ||||
|  | ||||
| export function getPathsFromArtifact({ | ||||
|   sketchPathToNode, | ||||
|   artifact, | ||||
|   artifactGraph, | ||||
|   ast, | ||||
| }: { | ||||
|   sketchPathToNode: PathToNode | ||||
|   artifact?: Artifact | ||||
|   artifactGraph: ArtifactGraph | ||||
|   ast: Program | ||||
| }): PathToNode[] | Error { | ||||
|   const plane = getPlaneFromArtifact(artifact, artifactGraph) | ||||
|   if (err(plane)) return plane | ||||
|   const paths = getArtifactsOfTypes( | ||||
|     { keys: plane.pathIds, types: ['path'] }, | ||||
|     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, ast) | ||||
| } | ||||
|  | ||||
| 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 | ||||
|  */ | ||||
| @ -418,12 +686,24 @@ export function getArtifactFromRange( | ||||
|   artifactGraph: ArtifactGraph | ||||
| ): Artifact | null { | ||||
|   for (const artifact of artifactGraph.values()) { | ||||
|     if ('codeRef' in artifact) { | ||||
|     const codeRef = getFaceCodeRef(artifact) | ||||
|     if (codeRef) { | ||||
|       const match = | ||||
|         artifact.codeRef?.range[0] === range[0] && | ||||
|         artifact.codeRef.range[1] === range[1] | ||||
|         codeRef?.range[0] === range[0] && codeRef.range[1] === range[1] | ||||
|       if (match) return artifact | ||||
|     } | ||||
|   } | ||||
|   return null | ||||
| } | ||||
|  | ||||
| export function getFaceCodeRef( | ||||
|   artifact: Artifact | Plane | Wall | Cap | ||||
| ): CodeRef | null { | ||||
|   if ('faceCodeRef' in artifact) { | ||||
|     return artifact.faceCodeRef | ||||
|   } | ||||
|   if ('codeRef' in artifact) { | ||||
|     return artifact.codeRef | ||||
|   } | ||||
|   return null | ||||
| } | ||||
|  | ||||
| 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 { EdgeCutInfo } from 'machines/modelingMachine' | ||||
| 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_END = 'end' | ||||
| @ -76,6 +81,9 @@ const STRAIGHT_SEGMENT_ERR = new Error( | ||||
|   'Invalid input, expected "straight-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] | ||||
|  | ||||
| @ -171,7 +179,8 @@ const commonConstraintInfoHelper = ( | ||||
|     } | ||||
|   ], | ||||
|   code: string, | ||||
|   pathToNode: PathToNode | ||||
|   pathToNode: PathToNode, | ||||
|   filterValue?: string | ||||
| ) => { | ||||
|   if (callExp.type !== 'CallExpression' && callExp.type !== 'CallExpressionKw') | ||||
|     return [] | ||||
| @ -295,7 +304,8 @@ const horzVertConstraintInfoHelper = ( | ||||
|   stdLibFnName: ConstrainInfo['stdLibFnName'], | ||||
|   abbreviatedInput: AbbreviatedInput, | ||||
|   code: string, | ||||
|   pathToNode: PathToNode | ||||
|   pathToNode: PathToNode, | ||||
|   filterValue?: string | ||||
| ) => { | ||||
|   if (callExp.type !== 'CallExpression') return [] | ||||
|   const firstArg = callExp.arguments?.[0] | ||||
| @ -502,13 +512,14 @@ export const lineTo: SketchLineHelperKw = { | ||||
|   }) => { | ||||
|     if (segmentInput.type !== 'straight-segment') return STRAIGHT_SEGMENT_ERR | ||||
|     const to = segmentInput.to | ||||
|     const _node = { ...node } | ||||
|     const _node = structuredClone(node) | ||||
|     const nodeMeta = getNodeFromPath<PipeExpression | CallExpressionKw>( | ||||
|       _node, | ||||
|       pathToNode, | ||||
|       'PipeExpression' | ||||
|     ) | ||||
|     if (err(nodeMeta)) return nodeMeta | ||||
|  | ||||
|     const { node: pipe } = nodeMeta | ||||
|     const nodeMeta2 = getNodeFromPath<VariableDeclarator>( | ||||
|       _node, | ||||
| @ -783,11 +794,11 @@ export const xLine: SketchLineHelper = { | ||||
|   add: ({ node, pathToNode, segmentInput, replaceExistingCallback }) => { | ||||
|     if (segmentInput.type !== 'straight-segment') return STRAIGHT_SEGMENT_ERR | ||||
|     const { from, to } = segmentInput | ||||
|     const _node = { ...node } | ||||
|     const _node = structuredClone(node) | ||||
|     const getNode = getNodeFromPathCurry(_node, pathToNode) | ||||
|     const _node1 = getNode<PipeExpression>('PipeExpression') | ||||
|     if (err(_node1)) return _node1 | ||||
|     const { node: pipe } = _node1 | ||||
|     const varDec = getNode<VariableDeclaration>('VariableDeclaration') | ||||
|     if (err(varDec)) return varDec | ||||
|     const dec = varDec.node.declaration | ||||
|  | ||||
|     const newVal = createLiteral(roundOff(to[0] - from[0], 2)) | ||||
|  | ||||
| @ -802,7 +813,11 @@ export const xLine: SketchLineHelper = { | ||||
|       ]) | ||||
|       if (err(result)) return result | ||||
|       const { callExp, valueUsedInTransform } = result | ||||
|       pipe.body[callIndex] = callExp | ||||
|       if (dec.init.type === 'PipeExpression') { | ||||
|         dec.init.body[callIndex] = callExp | ||||
|       } else { | ||||
|         dec.init = callExp | ||||
|       } | ||||
|       return { | ||||
|         modifiedAst: _node, | ||||
|         pathToNode, | ||||
| @ -814,7 +829,11 @@ export const xLine: SketchLineHelper = { | ||||
|       newVal, | ||||
|       createPipeSubstitution(), | ||||
|     ]) | ||||
|     pipe.body = [...pipe.body, newLine] | ||||
|     if (dec.init.type === 'PipeExpression') { | ||||
|       dec.init.body = [...dec.init.body, newLine] | ||||
|     } else { | ||||
|       dec.init = createPipeExpression([dec.init, newLine]) | ||||
|     } | ||||
|     return { modifiedAst: _node, pathToNode } | ||||
|   }, | ||||
|   updateArgs: ({ node, pathToNode, input }) => { | ||||
| @ -851,11 +870,11 @@ export const yLine: SketchLineHelper = { | ||||
|   add: ({ node, pathToNode, segmentInput, replaceExistingCallback }) => { | ||||
|     if (segmentInput.type !== 'straight-segment') return STRAIGHT_SEGMENT_ERR | ||||
|     const { from, to } = segmentInput | ||||
|     const _node = { ...node } | ||||
|     const _node = structuredClone(node) | ||||
|     const getNode = getNodeFromPathCurry(_node, pathToNode) | ||||
|     const _node1 = getNode<PipeExpression>('PipeExpression') | ||||
|     if (err(_node1)) return _node1 | ||||
|     const { node: pipe } = _node1 | ||||
|     const varDec = getNode<VariableDeclaration>('VariableDeclaration') | ||||
|     if (err(varDec)) return varDec | ||||
|     const dec = varDec.node.declaration | ||||
|     const newVal = createLiteral(roundOff(to[1] - from[1], 2)) | ||||
|     if (replaceExistingCallback) { | ||||
|       const { index: callIndex } = splitPathAtPipeExpression(pathToNode) | ||||
| @ -868,7 +887,11 @@ export const yLine: SketchLineHelper = { | ||||
|       ]) | ||||
|       if (err(result)) return result | ||||
|       const { callExp, valueUsedInTransform } = result | ||||
|       pipe.body[callIndex] = callExp | ||||
|       if (dec.init.type === 'PipeExpression') { | ||||
|         dec.init.body[callIndex] = callExp | ||||
|       } else { | ||||
|         dec.init = callExp | ||||
|       } | ||||
|       return { | ||||
|         modifiedAst: _node, | ||||
|         pathToNode, | ||||
| @ -880,7 +903,11 @@ export const yLine: SketchLineHelper = { | ||||
|       newVal, | ||||
|       createPipeSubstitution(), | ||||
|     ]) | ||||
|     pipe.body = [...pipe.body, newLine] | ||||
|     if (dec.init.type === 'PipeExpression') { | ||||
|       dec.init.body = [...dec.init.body, newLine] | ||||
|     } else { | ||||
|       dec.init = createPipeExpression([dec.init, newLine]) | ||||
|     } | ||||
|     return { modifiedAst: _node, pathToNode } | ||||
|   }, | ||||
|   updateArgs: ({ node, pathToNode, input }) => { | ||||
| @ -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('replaceExistingCallback is missing') | ||||
|   }, | ||||
|   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 = { | ||||
|   add: ({ node, pathToNode, segmentInput, replaceExistingCallback }) => { | ||||
|     if (segmentInput.type !== 'straight-segment') return STRAIGHT_SEGMENT_ERR | ||||
| @ -1984,6 +2300,7 @@ export const sketchLineHelperMap: { [key: string]: SketchLineHelper } = { | ||||
| export const sketchLineHelperMapKw: { [key: string]: SketchLineHelperKw } = { | ||||
|   line, | ||||
|   lineTo, | ||||
|   circleThreePoint, | ||||
| } as const | ||||
|  | ||||
| export function changeSketchArguments( | ||||
| @ -2051,30 +2368,36 @@ export function changeSketchArguments( | ||||
| export function getConstraintInfo( | ||||
|   callExpression: Node<CallExpression>, | ||||
|   code: string, | ||||
|   pathToNode: PathToNode | ||||
|   pathToNode: PathToNode, | ||||
|   filterValue?: string | ||||
| ): ConstrainInfo[] { | ||||
|   const fnName = callExpression?.callee?.name || '' | ||||
|   if (!(fnName in sketchLineHelperMap)) return [] | ||||
|   return sketchLineHelperMap[fnName].getConstraintInfo( | ||||
|     callExpression, | ||||
|     code, | ||||
|     pathToNode | ||||
|     pathToNode, | ||||
|     filterValue | ||||
|   ) | ||||
| } | ||||
|  | ||||
| export function getConstraintInfoKw( | ||||
|   callExpression: Node<CallExpressionKw>, | ||||
|   code: string, | ||||
|   pathToNode: PathToNode | ||||
|   pathToNode: PathToNode, | ||||
|   filterValue?: string | ||||
| ): ConstrainInfo[] { | ||||
|   const fnName = callExpression?.callee?.name || '' | ||||
|   const isAbsolute = findKwArg('endAbsolute', callExpression) !== undefined | ||||
|   const isAbsolute = | ||||
|     fnName === 'circleThreePoint' || | ||||
|     findKwArg('endAbsolute', callExpression) !== undefined | ||||
|   if (!(fnName in sketchLineHelperMapKw)) return [] | ||||
|   const correctFnName = fnName === 'line' && isAbsolute ? 'lineTo' : fnName | ||||
|   return sketchLineHelperMapKw[correctFnName].getConstraintInfo( | ||||
|     callExpression, | ||||
|     code, | ||||
|     pathToNode | ||||
|     pathToNode, | ||||
|     filterValue | ||||
|   ) | ||||
| } | ||||
|  | ||||
| @ -2298,8 +2621,6 @@ function addTagToChamfer( | ||||
|   if (err(variableDec)) return variableDec | ||||
|   const isPipeExpression = pipeExpr.node.type === 'PipeExpression' | ||||
|  | ||||
|   console.log('pipeExpr', pipeExpr, variableDec) | ||||
|   // const callExpr = isPipeExpression ? pipeExpr.node.body[pipeIndex] : variableDec.node.init | ||||
|   const callExpr = isPipeExpression | ||||
|     ? pipeExpr.node.body[pipeIndex] | ||||
|     : variableDec.node.init | ||||
| @ -2380,7 +2701,6 @@ function addTagToChamfer( | ||||
|   if (isPipeExpression) { | ||||
|     pipeExpr.node.body.splice(pipeIndex, 0, newExpressionToInsert) | ||||
|   } else { | ||||
|     console.log('yo', createPipeExpression([newExpressionToInsert, callExpr])) | ||||
|     callExpr.arguments[1] = createPipeSubstitution() | ||||
|     variableDec.node.init = createPipeExpression([ | ||||
|       newExpressionToInsert, | ||||
| @ -2719,6 +3039,8 @@ export function isAbsoluteLine(lineCall: CallExpressionKw): boolean | Error { | ||||
|       return new Error( | ||||
|         `line call has neither ${ARG_END} nor ${ARG_END_ABSOLUTE} params` | ||||
|       ) | ||||
|     case 'circleThreePoint': | ||||
|       return false | ||||
|   } | ||||
|   return new Error(`Unknown sketch function ${name}`) | ||||
| } | ||||
|  | ||||
| @ -21,7 +21,6 @@ import { | ||||
|   Literal, | ||||
|   SourceRange, | ||||
|   LiteralValue, | ||||
|   recast, | ||||
|   LabeledArg, | ||||
|   VariableMap, | ||||
| } from '../wasm' | ||||
| @ -217,14 +216,19 @@ function createStdlibCallExpressionKw( | ||||
|   tool: ToolTip, | ||||
|   labeled: LabeledArg[], | ||||
|   tag?: Expr, | ||||
|   valueUsedInTransform?: number | ||||
|   valueUsedInTransform?: number, | ||||
|   unlabeled?: Expr | ||||
| ): CreatedSketchExprResult { | ||||
|   const args = labeled | ||||
|   if (tag) { | ||||
|     args.push(createLabeledArg(ARG_TAG, tag)) | ||||
|   } | ||||
|   return { | ||||
|     callExp: createCallExpressionStdLibKw(tool, null, args), | ||||
|     callExp: createCallExpressionStdLibKw( | ||||
|       tool, | ||||
|       unlabeled ? unlabeled : null, | ||||
|       args | ||||
|     ), | ||||
|     valueUsedInTransform, | ||||
|   } | ||||
| } | ||||
| @ -1306,6 +1310,12 @@ export function getRemoveConstraintsTransform( | ||||
|     }, | ||||
|   } | ||||
|  | ||||
|   if ( | ||||
|     sketchFnExp.type === 'CallExpressionKw' && | ||||
|     sketchFnExp.callee.name === 'circleThreePoint' | ||||
|   ) { | ||||
|     return false | ||||
|   } | ||||
|   const isAbsolute = | ||||
|     // isAbsolute doesn't matter if the call is positional. | ||||
|     sketchFnExp.type === 'CallExpression' ? false : isAbsoluteLine(sketchFnExp) | ||||
| @ -1320,7 +1330,6 @@ export function getRemoveConstraintsTransform( | ||||
|       ? getFirstArg(sketchFnExp) | ||||
|       : getArgForEnd(sketchFnExp) | ||||
|   if (err(firstArg)) { | ||||
|     console.error(firstArg) | ||||
|     return false | ||||
|   } | ||||
|  | ||||
| @ -1351,7 +1360,7 @@ export function getRemoveConstraintsTransform( | ||||
|  | ||||
| export function removeSingleConstraint({ | ||||
|   pathToCallExp, | ||||
|   inputDetails, | ||||
|   inputDetails: inputToReplace, | ||||
|   ast, | ||||
| }: { | ||||
|   pathToCallExp: PathToNode | ||||
| @ -1384,12 +1393,12 @@ export function removeSingleConstraint({ | ||||
|       // So we should update the call expression to use the inputs, except for | ||||
|       // the inputDetails, input where we should use the rawValue(s) | ||||
|  | ||||
|       if (inputDetails.type === 'arrayItem') { | ||||
|       if (inputToReplace.type === 'arrayItem') { | ||||
|         const values = inputs.map((arg) => { | ||||
|           if ( | ||||
|             !( | ||||
|               (arg.type === 'arrayItem' || arg.type === 'arrayOrObjItem') && | ||||
|               arg.index === inputDetails.index | ||||
|               arg.index === inputToReplace.index | ||||
|             ) | ||||
|           ) | ||||
|             return arg.expr | ||||
| @ -1397,9 +1406,9 @@ export function removeSingleConstraint({ | ||||
|             (rawValue) => | ||||
|               (rawValue.type === 'arrayItem' || | ||||
|                 rawValue.type === 'arrayOrObjItem') && | ||||
|               rawValue.index === inputDetails.index | ||||
|               rawValue.index === inputToReplace.index | ||||
|           )?.expr | ||||
|           return (arg.index === inputDetails.index && literal) || arg.expr | ||||
|           return (arg.index === inputToReplace.index && literal) || arg.expr | ||||
|         }) | ||||
|         if (callExp.node.type === 'CallExpression') { | ||||
|           return createStdlibCallExpression( | ||||
| @ -1428,66 +1437,110 @@ export function removeSingleConstraint({ | ||||
|         } | ||||
|       } | ||||
|       if ( | ||||
|         inputDetails.type === 'arrayInObject' || | ||||
|         inputDetails.type === 'objectProperty' | ||||
|         inputToReplace.type === 'arrayInObject' || | ||||
|         inputToReplace.type === 'objectProperty' | ||||
|       ) { | ||||
|         const arrayDetailsNameBetterLater: { | ||||
|         const arrayInput: { | ||||
|           [key: string]: Parameters<typeof createArrayExpression>[0] | ||||
|         } = {} | ||||
|         const otherThing: Parameters<typeof createObjectExpression>[0] = {} | ||||
|         inputs.forEach((arg) => { | ||||
|         const objInput: Parameters<typeof createObjectExpression>[0] = {} | ||||
|         const kwArgInput: ReturnType<typeof createLabeledArg>[] = [] | ||||
|         inputs.forEach((currentArg) => { | ||||
|           if ( | ||||
|             arg.type !== 'objectProperty' && | ||||
|             arg.type !== 'arrayOrObjItem' && | ||||
|             arg.type !== 'arrayInObject' | ||||
|             // should be one of these, return early to make TS happy. | ||||
|             currentArg.type !== 'objectProperty' && | ||||
|             currentArg.type !== 'arrayOrObjItem' && | ||||
|             currentArg.type !== 'arrayInObject' | ||||
|           ) | ||||
|             return | ||||
|           const rawLiteralArrayInObject = rawArgs.find( | ||||
|             (rawValue) => | ||||
|               rawValue.type === 'arrayInObject' && | ||||
|               rawValue.key === inputDetails.key && | ||||
|               rawValue.index === (arg.type === 'arrayInObject' ? arg.index : -1) | ||||
|               rawValue.key === currentArg.key && | ||||
|               rawValue.index === | ||||
|                 (currentArg.type === 'arrayInObject' ? currentArg.index : -1) | ||||
|           ) | ||||
|           const rawLiteralObjProp = rawArgs.find( | ||||
|             (rawValue) => | ||||
|               (rawValue.type === 'objectProperty' || | ||||
|                 rawValue.type === 'arrayOrObjItem' || | ||||
|                 rawValue.type === 'arrayInObject') && | ||||
|               rawValue.key === inputDetails.key | ||||
|               rawValue.key === inputToReplace.key | ||||
|           ) | ||||
|           if ( | ||||
|             inputDetails.type === 'arrayInObject' && | ||||
|             inputToReplace.type === 'arrayInObject' && | ||||
|             rawLiteralArrayInObject?.type === 'arrayInObject' && | ||||
|             rawLiteralArrayInObject?.index === inputDetails.index && | ||||
|             rawLiteralArrayInObject?.key === inputDetails.key | ||||
|             rawLiteralArrayInObject?.index === inputToReplace.index && | ||||
|             rawLiteralArrayInObject?.key === inputToReplace.key | ||||
|           ) { | ||||
|             if (!arrayDetailsNameBetterLater[arg.key]) | ||||
|               arrayDetailsNameBetterLater[arg.key] = [] | ||||
|             arrayDetailsNameBetterLater[inputDetails.key][inputDetails.index] = | ||||
|             if (!arrayInput[currentArg.key]) { | ||||
|               arrayInput[currentArg.key] = [] | ||||
|             } | ||||
|             arrayInput[inputToReplace.key][inputToReplace.index] = | ||||
|               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 ( | ||||
|             inputDetails.type === 'objectProperty' && | ||||
|             inputToReplace.type === 'objectProperty' && | ||||
|             (rawLiteralObjProp?.type === 'objectProperty' || | ||||
|               rawLiteralObjProp?.type === 'arrayOrObjItem') && | ||||
|             rawLiteralObjProp?.key === inputDetails.key && | ||||
|             arg.key === inputDetails.key | ||||
|             rawLiteralObjProp?.key === inputToReplace.key && | ||||
|             currentArg.key === inputToReplace.key | ||||
|           ) { | ||||
|             otherThing[inputDetails.key] = rawLiteralObjProp.expr | ||||
|           } else if (arg.type === 'arrayInObject') { | ||||
|             if (!arrayDetailsNameBetterLater[arg.key]) | ||||
|               arrayDetailsNameBetterLater[arg.key] = [] | ||||
|             arrayDetailsNameBetterLater[arg.key][arg.index] = arg.expr | ||||
|           } else if (arg.type === 'objectProperty') { | ||||
|             otherThing[arg.key] = arg.expr | ||||
|             objInput[inputToReplace.key] = rawLiteralObjProp.expr | ||||
|           } else if (currentArg.type === 'arrayInObject') { | ||||
|             if (!arrayInput[currentArg.key]) arrayInput[currentArg.key] = [] | ||||
|             arrayInput[currentArg.key][currentArg.index] = currentArg.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[currentArg.index] = currentArg.expr | ||||
|             } | ||||
|           } else if (currentArg.type === 'objectProperty') { | ||||
|             objInput[currentArg.key] = currentArg.expr | ||||
|           } | ||||
|         }) | ||||
|         const createObjParam: Parameters<typeof createObjectExpression>[0] = {} | ||||
|         Object.entries(arrayDetailsNameBetterLater).forEach(([key, value]) => { | ||||
|         Object.entries(arrayInput).forEach(([key, 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({ | ||||
|           ...createObjParam, | ||||
|           ...otherThing, | ||||
|           ...objInput, | ||||
|         }) | ||||
|         return createStdlibCallExpression( | ||||
|           callExp.node.callee.name as any, | ||||
| @ -1571,6 +1624,16 @@ function getTransformMapPathKw( | ||||
|     } | ||||
|   | false { | ||||
|   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 nameAbsolute = name === 'line' ? 'lineTo' : name | ||||
|   if (!toolTips.includes(name)) { | ||||
| @ -1989,6 +2052,13 @@ export function transformAstSketchLines({ | ||||
|               radius: seg.radius, | ||||
|               from, | ||||
|             } | ||||
|           : seg.type === 'CircleThreePoint' | ||||
|           ? { | ||||
|               type: 'circle-three-point-segment', | ||||
|               p1: seg.p1, | ||||
|               p2: seg.p2, | ||||
|               p3: seg.p3, | ||||
|             } | ||||
|           : { | ||||
|               type: 'straight-segment', | ||||
|               to, | ||||
|  | ||||
| @ -45,6 +45,13 @@ interface ArcSegmentInput { | ||||
|   center: [number, 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. | ||||
| @ -52,7 +59,10 @@ interface ArcSegmentInput { | ||||
|  * - 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. | ||||
|  */ | ||||
| export type SegmentInputs = StraightSegmentInput | ArcSegmentInput | ||||
| export type SegmentInputs = | ||||
|   | StraightSegmentInput | ||||
|   | ArcSegmentInput | ||||
|   | CircleThreePointSegmentInput | ||||
|  | ||||
| /** | ||||
|  * Interface for adding or replacing a sketch stblib call expression to a sketch. | ||||
| @ -85,6 +95,9 @@ export type InputArgKeys = | ||||
|   | 'intersectTag' | ||||
|   | 'radius' | ||||
|   | 'center' | ||||
|   | 'p1' | ||||
|   | 'p2' | ||||
|   | 'p3' | ||||
| export interface SingleValueInput<T> { | ||||
|   type: 'singleValue' | ||||
|   argType: LineInputsType | ||||
| @ -239,7 +252,8 @@ export interface SketchLineHelper { | ||||
|   getConstraintInfo: ( | ||||
|     callExp: Node<CallExpression>, | ||||
|     code: string, | ||||
|     pathToNode: PathToNode | ||||
|     pathToNode: PathToNode, | ||||
|     filterValue?: string | ||||
|   ) => ConstrainInfo[] | ||||
| } | ||||
|  | ||||
| @ -267,6 +281,7 @@ export interface SketchLineHelperKw { | ||||
|   getConstraintInfo: ( | ||||
|     callExp: Node<CallExpressionKw>, | ||||
|     code: string, | ||||
|     pathToNode: PathToNode | ||||
|     pathToNode: PathToNode, | ||||
|     filterValue?: string | ||||
|   ) => ConstrainInfo[] | ||||
| } | ||||
|  | ||||
| @ -11,23 +11,50 @@ import { | ||||
|   LiteralValue, | ||||
|   NumericSuffix, | ||||
| } from './wasm' | ||||
| import { filterArtifacts } from 'lang/std/artifactGraph' | ||||
| import { filterArtifacts, getFaceCodeRef } from 'lang/std/artifactGraph' | ||||
| import { isArray, isOverlap } from 'lib/utils' | ||||
|  | ||||
| export function updatePathToNodeFromMap( | ||||
|   oldPath: PathToNode, | ||||
|   pathToNodeMap: { [key: number]: PathToNode } | ||||
| /** | ||||
|  * Updates pathToNode body indices to account for the insertion of an expression | ||||
|  * PathToNode expression is after the insertion index, that the body index is incremented | ||||
|  * Negative insertion index means no insertion | ||||
|  */ | ||||
| export function updatePathToNodePostExprInjection( | ||||
|   pathToNode: PathToNode, | ||||
|   exprInsertIndex: number | ||||
| ): PathToNode { | ||||
|   const updatedPathToNode = structuredClone(oldPath) | ||||
|   let max = 0 | ||||
|   Object.values(pathToNodeMap).forEach((path) => { | ||||
|     const index = Number(path[1][0]) | ||||
|     if (index > max) { | ||||
|       max = index | ||||
|     } | ||||
|   }) | ||||
|   updatedPathToNode[1][0] = max | ||||
|   return updatedPathToNode | ||||
|   if (exprInsertIndex < 0) return pathToNode | ||||
|   const bodyIndex = Number(pathToNode[1][0]) | ||||
|   if (bodyIndex < exprInsertIndex) return pathToNode | ||||
|   const clone = structuredClone(pathToNode) | ||||
|   clone[1][0] = bodyIndex + 1 | ||||
|   return clone | ||||
| } | ||||
|  | ||||
| export function updateSketchDetailsNodePaths({ | ||||
|   sketchEntryNodePath, | ||||
|   sketchNodePaths, | ||||
|   planeNodePath, | ||||
|   exprInsertIndex, | ||||
| }: { | ||||
|   sketchEntryNodePath: PathToNode | ||||
|   sketchNodePaths: Array<PathToNode> | ||||
|   planeNodePath: PathToNode | ||||
|   exprInsertIndex: number | ||||
| }) { | ||||
|   return { | ||||
|     updatedSketchEntryNodePath: updatePathToNodePostExprInjection( | ||||
|       sketchEntryNodePath, | ||||
|       exprInsertIndex | ||||
|     ), | ||||
|     updatedSketchNodePaths: sketchNodePaths.map((path) => | ||||
|       updatePathToNodePostExprInjection(path, exprInsertIndex) | ||||
|     ), | ||||
|     updatedPlaneNodePath: updatePathToNodePostExprInjection( | ||||
|       planeNodePath, | ||||
|       exprInsertIndex | ||||
|     ), | ||||
|   } | ||||
| } | ||||
|  | ||||
| export function isCursorInSketchCommandRange( | ||||
| @ -36,20 +63,30 @@ export function isCursorInSketchCommandRange( | ||||
| ): string | false { | ||||
|   const overlappingEntries = filterArtifacts( | ||||
|     { | ||||
|       types: ['segment', 'path'], | ||||
|       types: ['segment', 'path', 'plane', 'cap', 'wall'], | ||||
|       predicate: (artifact) => { | ||||
|         const codeRefRange = getFaceCodeRef(artifact)?.range | ||||
|         return selectionRanges.graphSelections.some( | ||||
|           (selection) => | ||||
|             isArray(selection?.codeRef?.range) && | ||||
|             isArray(artifact?.codeRef?.range) && | ||||
|             isOverlap(selection?.codeRef?.range, artifact.codeRef.range) | ||||
|             isArray(codeRefRange) && | ||||
|             isOverlap(selection?.codeRef?.range, codeRefRange) | ||||
|         ) | ||||
|       }, | ||||
|     }, | ||||
|     artifactGraph | ||||
|   ) | ||||
|   const firstEntry = [...overlappingEntries.values()]?.[0] | ||||
|   const parentId = firstEntry?.type === 'segment' ? firstEntry.pathId : false | ||||
|   const parentId = | ||||
|     firstEntry?.type === 'segment' | ||||
|       ? firstEntry.pathId | ||||
|       : ((firstEntry?.type === 'plane' || | ||||
|           firstEntry?.type === 'cap' || | ||||
|           firstEntry?.type === 'wall') && | ||||
|           firstEntry.pathIds?.length) || | ||||
|         false | ||||
|       ? firstEntry.pathIds[0] | ||||
|       : false | ||||
|  | ||||
|   return parentId | ||||
|     ? parentId | ||||
| @ -81,11 +118,27 @@ export function findKwArg( | ||||
|   label: string, | ||||
|   call: CallExpressionKw | ||||
| ): Expr | undefined { | ||||
|   return call.arguments.find((arg) => { | ||||
|   return call?.arguments?.find((arg) => { | ||||
|     return arg.label.name === label | ||||
|   })?.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. | ||||
| */ | ||||
|  | ||||
| @ -58,7 +58,7 @@ export type ModelingCommandSchema = { | ||||
|   Revolve: { | ||||
|     selection: Selections | ||||
|     angle: KclCommandValue | ||||
|     axisOrEdge: string | ||||
|     axisOrEdge: 'Axis' | 'Edge' | ||||
|     axis: string | ||||
|     edge: Selections | ||||
|   } | ||||
|  | ||||
| @ -75,9 +75,9 @@ segAng(rectangleSegmentA001), | ||||
|  | ||||
|       // ast is edited in place from the updateCenterRectangleSketch | ||||
|       const expectedSourceCode = `sketch001 = startSketchOn('XZ') | ||||
|   |> startProfileAt([80, 120], %) | ||||
|   |> angledLine([0, 80], %, $rectangleSegmentA001) | ||||
|   |> angledLine([segAng(rectangleSegmentA001) + 90, 120], %, $rectangleSegmentB001) | ||||
|   |> startProfileAt([120.37, 80], %) | ||||
|   |> angledLine([0, 0], %, $rectangleSegmentA001) | ||||
|   |> angledLine([segAng(rectangleSegmentA001) + 90, 0], %, $rectangleSegmentB001) | ||||
|   |> angledLine([ | ||||
|        segAng(rectangleSegmentA001), | ||||
|        -segLen(rectangleSegmentA001) | ||||
|  | ||||
| @ -37,7 +37,7 @@ import { | ||||
|  */ | ||||
| export const getRectangleCallExpressions = ( | ||||
|   rectangleOrigin: [number, number], | ||||
|   tags: [string, string, string] | ||||
|   tag: string | ||||
| ) => [ | ||||
|   createCallExpressionStdLib('angledLine', [ | ||||
|     createArrayExpression([ | ||||
| @ -45,30 +45,28 @@ export const getRectangleCallExpressions = ( | ||||
|       createLiteral(0), // This will be the width of the rectangle | ||||
|     ]), | ||||
|     createPipeSubstitution(), | ||||
|     createTagDeclarator(tags[0]), | ||||
|     createTagDeclarator(tag), | ||||
|   ]), | ||||
|   createCallExpressionStdLib('angledLine', [ | ||||
|     createArrayExpression([ | ||||
|       createBinaryExpression([ | ||||
|         createCallExpressionStdLib('segAng', [createIdentifier(tags[0])]), | ||||
|         createCallExpressionStdLib('segAng', [createIdentifier(tag)]), | ||||
|         '+', | ||||
|         createLiteral(90), | ||||
|       ]), // 90 offset from the previous line | ||||
|       createLiteral(0), // This will be the height of the rectangle | ||||
|     ]), | ||||
|     createPipeSubstitution(), | ||||
|     createTagDeclarator(tags[1]), | ||||
|   ]), | ||||
|   createCallExpressionStdLib('angledLine', [ | ||||
|     createArrayExpression([ | ||||
|       createCallExpressionStdLib('segAng', [createIdentifier(tags[0])]), // same angle as the first line | ||||
|       createCallExpressionStdLib('segAng', [createIdentifier(tag)]), // same angle as the first line | ||||
|       createUnaryExpression( | ||||
|         createCallExpressionStdLib('segLen', [createIdentifier(tags[0])]), | ||||
|         createCallExpressionStdLib('segLen', [createIdentifier(tag)]), | ||||
|         '-' | ||||
|       ), // negative height | ||||
|     ]), | ||||
|     createPipeSubstitution(), | ||||
|     createTagDeclarator(tags[2]), | ||||
|   ]), | ||||
|   createCallExpressionStdLibKw('line', null, [ | ||||
|     createLabeledArg( | ||||
| @ -95,12 +93,12 @@ export function updateRectangleSketch( | ||||
|   y: number, | ||||
|   tag: string | ||||
| ) { | ||||
|   ;((pipeExpression.body[2] as CallExpression) | ||||
|   ;((pipeExpression.body[1] as CallExpression) | ||||
|     .arguments[0] as ArrayExpression) = createArrayExpression([ | ||||
|     createLiteral(x >= 0 ? 0 : 180), | ||||
|     createLiteral(Math.abs(x)), | ||||
|   ]) | ||||
|   ;((pipeExpression.body[3] as CallExpression) | ||||
|   ;((pipeExpression.body[2] as CallExpression) | ||||
|     .arguments[0] as ArrayExpression) = createArrayExpression([ | ||||
|     createBinaryExpression([ | ||||
|       createCallExpressionStdLib('segAng', [createIdentifier(tag)]), | ||||
| @ -129,8 +127,7 @@ export function updateCenterRectangleSketch( | ||||
|   let startX = originX - Math.abs(deltaX) | ||||
|   let startY = originY - Math.abs(deltaY) | ||||
|  | ||||
|   // pipeExpression.body[1] is startProfileAt | ||||
|   let callExpression = pipeExpression.body[1] | ||||
|   let callExpression = pipeExpression.body[0] | ||||
|   if (isCallExpression(callExpression)) { | ||||
|     const arrayExpression = callExpression.arguments[0] | ||||
|     if (isArrayExpression(arrayExpression)) { | ||||
| @ -144,7 +141,7 @@ export function updateCenterRectangleSketch( | ||||
|   const twoX = deltaX * 2 | ||||
|   const twoY = deltaY * 2 | ||||
|  | ||||
|   callExpression = pipeExpression.body[2] | ||||
|   callExpression = pipeExpression.body[1] | ||||
|   if (isCallExpression(callExpression)) { | ||||
|     const arrayExpression = callExpression.arguments[0] | ||||
|     if (isArrayExpression(arrayExpression)) { | ||||
| @ -160,7 +157,7 @@ export function updateCenterRectangleSketch( | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   callExpression = pipeExpression.body[3] | ||||
|   callExpression = pipeExpression.body[2] | ||||
|   if (isCallExpression(callExpression)) { | ||||
|     const arrayExpression = callExpression.arguments[0] | ||||
|     if (isArrayExpression(arrayExpression)) { | ||||
|  | ||||
| @ -40,6 +40,7 @@ import { | ||||
|   CodeRef, | ||||
|   getCodeRefsByArtifactId, | ||||
|   ArtifactId, | ||||
|   getFaceCodeRef, | ||||
| } from 'lang/std/artifactGraph' | ||||
| import { Node } from 'wasm-lib/kcl/bindings/Node' | ||||
| import { DefaultPlaneStr } from './planes' | ||||
| @ -276,18 +277,19 @@ export function getEventForSegmentSelection( | ||||
|   } | ||||
|   if (!id || !group) return null | ||||
|   const artifact = engineCommandManager.artifactGraph.get(id) | ||||
|   const codeRefs = getCodeRefsByArtifactId( | ||||
|     id, | ||||
|     engineCommandManager.artifactGraph | ||||
|   ) | ||||
|   if (!artifact || !codeRefs) return null | ||||
|   if (!artifact) return null | ||||
|   const node = getNodeFromPath<Expr>(kclManager.ast, group.userData.pathToNode) | ||||
|   if (err(node)) return null | ||||
|   return { | ||||
|     type: 'Set selection', | ||||
|     data: { | ||||
|       selectionType: 'singleCodeCursor', | ||||
|       selection: { | ||||
|         artifact, | ||||
|         codeRef: codeRefs[0], | ||||
|         codeRef: { | ||||
|           pathToNode: group?.userData?.pathToNode, | ||||
|           range: [node.node.start, node.node.end, 0], | ||||
|         }, | ||||
|       }, | ||||
|     }, | ||||
|   } | ||||
| @ -572,8 +574,7 @@ export function getSelectionTypeDisplayText( | ||||
|   const selectionsByType = getSelectionCountByType(selection) | ||||
|   if (selectionsByType === 'none') return null | ||||
|  | ||||
|   return selectionsByType | ||||
|     .entries() | ||||
|   return [...selectionsByType.entries()] | ||||
|     .map( | ||||
|       // Hack for showing "face" instead of "extrude-wall" in command bar text | ||||
|       ([type, count]) => | ||||
| @ -581,7 +582,6 @@ export function getSelectionTypeDisplayText( | ||||
|           count > 1 ? 's' : '' | ||||
|         }` | ||||
|     ) | ||||
|     .toArray() | ||||
|     .join(', ') | ||||
| } | ||||
|  | ||||
| @ -591,7 +591,7 @@ export function canSubmitSelectionArg( | ||||
| ) { | ||||
|   return ( | ||||
|     selectionsByType !== 'none' && | ||||
|     selectionsByType.entries().every(([type, count]) => { | ||||
|     [...selectionsByType.entries()].every(([type, count]) => { | ||||
|       const foundIndex = argument.selectionTypes.findIndex((s) => s === type) | ||||
|       return ( | ||||
|         foundIndex !== -1 && | ||||
| @ -614,8 +614,9 @@ export function codeToIdSelections( | ||||
|       // TODO #868: loops over all artifacts will become inefficient at a large scale | ||||
|       const overlappingEntries = Array.from(engineCommandManager.artifactGraph) | ||||
|         .map(([id, artifact]) => { | ||||
|           if (!('codeRef' in artifact && artifact.codeRef)) return null | ||||
|           return isOverlap(artifact.codeRef.range, selection.range) | ||||
|           const codeRef = getFaceCodeRef(artifact) | ||||
|           if (!codeRef) return null | ||||
|           return isOverlap(codeRef.range, selection.range) | ||||
|             ? { | ||||
|                 artifact, | ||||
|                 selection, | ||||
| @ -672,6 +673,27 @@ export function codeToIdSelections( | ||||
|             id: entry.artifact.solid2dId, | ||||
|           } | ||||
|         } | ||||
|         if (entry.artifact.type === 'plane') { | ||||
|           bestCandidate = { | ||||
|             artifact: entry.artifact, | ||||
|             selection, | ||||
|             id: entry.id, | ||||
|           } | ||||
|         } | ||||
|         if (entry.artifact.type === 'cap') { | ||||
|           bestCandidate = { | ||||
|             artifact: entry.artifact, | ||||
|             selection, | ||||
|             id: entry.id, | ||||
|           } | ||||
|         } | ||||
|         if (entry.artifact.type === 'wall') { | ||||
|           bestCandidate = { | ||||
|             artifact: entry.artifact, | ||||
|             selection, | ||||
|             id: entry.id, | ||||
|           } | ||||
|         } | ||||
|         if (type === 'extrude-wall' && entry.artifact.type === 'segment') { | ||||
|           if (!entry.artifact.surfaceId) return | ||||
|           const wall = engineCommandManager.artifactGraph.get( | ||||
| @ -867,7 +889,6 @@ export function updateSelections( | ||||
|             JSON.stringify(pathToNode) | ||||
|           ) { | ||||
|             artifact = a | ||||
|             console.log('found artifact', a) | ||||
|             break | ||||
|           } | ||||
|         } | ||||
|  | ||||
| @ -1,9 +1,7 @@ | ||||
| import { CustomIconName } from 'components/CustomIcon' | ||||
| import { DEV } from 'env' | ||||
| import { commandBarActor, commandBarMachine } from 'machines/commandBarMachine' | ||||
| import { commandBarActor } from 'machines/commandBarMachine' | ||||
| import { | ||||
|   canRectangleOrCircleTool, | ||||
|   isClosedSketch, | ||||
|   isEditingExistingSketch, | ||||
|   modelingMachine, | ||||
|   pipeHasCircle, | ||||
| @ -72,7 +70,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = { | ||||
|         icon: 'sketch', | ||||
|         status: 'available', | ||||
|         title: ({ sketchPathId }) => | ||||
|           `${sketchPathId ? 'Edit' : 'Start'} Sketch`, | ||||
|           sketchPathId ? 'Edit Sketch' : 'Start Sketch', | ||||
|         showTitle: true, | ||||
|         hotkey: 'S', | ||||
|         description: 'Start drawing a 2D sketch', | ||||
| @ -366,22 +364,14 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = { | ||||
|       { | ||||
|         id: 'line', | ||||
|         onClick: ({ modelingState, modelingSend }) => { | ||||
|           if (modelingState.matches({ Sketch: { 'Line tool': 'No Points' } })) { | ||||
|             // Exit the sketch state if there are no points and they press ESC | ||||
|             modelingSend({ | ||||
|               type: 'Cancel', | ||||
|             }) | ||||
|           } else { | ||||
|             // Exit the tool if there are points and they press ESC | ||||
|             modelingSend({ | ||||
|               type: 'change tool', | ||||
|               data: { | ||||
|                 tool: !modelingState.matches({ Sketch: 'Line tool' }) | ||||
|                   ? 'line' | ||||
|                   : 'none', | ||||
|               }, | ||||
|             }) | ||||
|           } | ||||
|           modelingSend({ | ||||
|             type: 'change tool', | ||||
|             data: { | ||||
|               tool: !modelingState.matches({ Sketch: 'Line tool' }) | ||||
|                 ? 'line' | ||||
|                 : 'none', | ||||
|             }, | ||||
|           }) | ||||
|         }, | ||||
|         icon: 'line', | ||||
|         status: 'available', | ||||
| @ -392,8 +382,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = { | ||||
|           }) || | ||||
|           state.matches({ | ||||
|             Sketch: { 'Circle tool': 'Awaiting Radius' }, | ||||
|           }) || | ||||
|           isClosedSketch(state.context), | ||||
|           }), | ||||
|         title: 'Line', | ||||
|         hotkey: (state) => | ||||
|           state.matches({ Sketch: 'Line tool' }) ? ['Esc', 'L'] : 'L', | ||||
| @ -473,14 +462,10 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = { | ||||
|           icon: 'circle', | ||||
|           status: 'available', | ||||
|           title: 'Center circle', | ||||
|           disabled: (state) => | ||||
|             state.matches('Sketch no face') || | ||||
|             (!canRectangleOrCircleTool(state.context) && | ||||
|               !state.matches({ Sketch: 'Circle tool' }) && | ||||
|               !state.matches({ Sketch: 'circle3PointToolSelect' })), | ||||
|           disabled: (state) => state.matches('Sketch no face'), | ||||
|           isActive: (state) => | ||||
|             state.matches({ Sketch: 'Circle tool' }) || | ||||
|             state.matches({ Sketch: 'circle3PointToolSelect' }), | ||||
|             state.matches({ Sketch: 'Circle three point tool' }), | ||||
|           hotkey: (state) => | ||||
|             state.matches({ Sketch: 'Circle tool' }) ? ['Esc', 'C'] : 'C', | ||||
|           showTitle: false, | ||||
| @ -494,9 +479,9 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = { | ||||
|               type: 'change tool', | ||||
|               data: { | ||||
|                 tool: !modelingState.matches({ | ||||
|                   Sketch: 'circle3PointToolSelect', | ||||
|                   Sketch: 'Circle three point tool', | ||||
|                 }) | ||||
|                   ? 'circle3Points' | ||||
|                   ? 'circleThreePointNeo' | ||||
|                   : 'none', | ||||
|               }, | ||||
|             }), | ||||
| @ -522,10 +507,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = { | ||||
|             }), | ||||
|           icon: 'rectangle', | ||||
|           status: 'available', | ||||
|           disabled: (state) => | ||||
|             state.matches('Sketch no face') || | ||||
|             (!canRectangleOrCircleTool(state.context) && | ||||
|               !state.matches({ Sketch: 'Rectangle tool' })), | ||||
|           disabled: (state) => state.matches('Sketch no face'), | ||||
|           title: 'Corner rectangle', | ||||
|           hotkey: (state) => | ||||
|             state.matches({ Sketch: 'Rectangle tool' }) ? ['Esc', 'R'] : 'R', | ||||
| @ -548,10 +530,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = { | ||||
|             }), | ||||
|           icon: 'arc', | ||||
|           status: 'available', | ||||
|           disabled: (state) => | ||||
|             state.matches('Sketch no face') || | ||||
|             (!canRectangleOrCircleTool(state.context) && | ||||
|               !state.matches({ Sketch: 'Center Rectangle tool' })), | ||||
|           disabled: (state) => state.matches('Sketch no face'), | ||||
|           title: 'Center rectangle', | ||||
|           hotkey: (state) => | ||||
|             state.matches({ Sketch: 'Center Rectangle tool' }) | ||||
|  | ||||
| @ -97,3 +97,7 @@ export function trap<T>( | ||||
|     }) | ||||
|   return true | ||||
| } | ||||
|  | ||||
| export function reject(errOrString: Error | string): Promise<never> { | ||||
|   return Promise.reject(errOrString) | ||||
| } | ||||
|  | ||||
| @ -188,6 +188,9 @@ pub struct Wall { | ||||
|     pub sweep_id: ArtifactId, | ||||
|     #[serde(default, skip_serializing_if = "Vec::is_empty")] | ||||
|     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)] | ||||
| @ -201,6 +204,9 @@ pub struct Cap { | ||||
|     pub sweep_id: ArtifactId, | ||||
|     #[serde(default, skip_serializing_if = "Vec::is_empty")] | ||||
|     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)] | ||||
| @ -584,7 +590,7 @@ fn artifacts_to_update( | ||||
|     responses: &FnvHashMap<Uuid, OkModelingCmdResponse>, | ||||
|     current_plane_id: Option<Uuid>, | ||||
|     _ast: &Node<Program>, | ||||
|     _exec_artifacts: &IndexMap<ArtifactId, Artifact>, | ||||
|     exec_artifacts: &IndexMap<ArtifactId, Artifact>, | ||||
| ) -> Result<Vec<Artifact>, KclError> { | ||||
|     // TODO: Build path-to-node from artifact_command source range.  Right now, | ||||
|     // we're serializing an empty array, and the TS wrapper fills it in with the | ||||
| @ -634,6 +640,17 @@ fn artifacts_to_update( | ||||
|                         edge_cut_edge_ids: wall.edge_cut_edge_ids.clone(), | ||||
|                         sweep_id: wall.sweep_id, | ||||
|                         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 => { | ||||
| @ -683,6 +700,17 @@ fn artifacts_to_update( | ||||
|                     edge_cut_edge_ids: wall.edge_cut_edge_ids.clone(), | ||||
|                     sweep_id: wall.sweep_id, | ||||
|                     path_ids: vec![id], | ||||
|                     face_code_ref: wall.face_code_ref.clone(), | ||||
|                 })); | ||||
|             } | ||||
|             if let Some(Artifact::Cap(cap)) = plane { | ||||
|                 return_arr.push(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: vec![id], | ||||
|                     face_code_ref: cap.face_code_ref.clone(), | ||||
|                 })); | ||||
|             } | ||||
|             return Ok(return_arr); | ||||
| @ -809,12 +837,31 @@ fn artifacts_to_update( | ||||
|                         source_ranges: vec![range], | ||||
|                     }) | ||||
|                 })?; | ||||
|                 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), | ||||
|                         // TODO: If we didn't find it, it's probably a bug. | ||||
|                         _ => None, | ||||
|                     }) | ||||
|                     .unwrap_or_default(); | ||||
|  | ||||
|                 return_arr.push(Artifact::Wall(Wall { | ||||
|                     id: face_id, | ||||
|                     seg_id: curve_id, | ||||
|                     edge_cut_edge_ids: Vec::new(), | ||||
|                     sweep_id: path_sweep_id, | ||||
|                     path_ids: vec![], | ||||
|                     path_ids: Vec::new(), | ||||
|                     face_code_ref: CodeRef { | ||||
|                         range: sketch_on_face_source_range, | ||||
|                         path_to_node: Vec::new(), | ||||
|                     }, | ||||
|                 })); | ||||
|                 let mut new_seg = seg.clone(); | ||||
|                 new_seg.surface_id = Some(face_id); | ||||
| @ -843,12 +890,29 @@ fn artifacts_to_update( | ||||
|                             source_ranges: vec![range], | ||||
|                         }) | ||||
|                     })?; | ||||
|                     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, | ||||
|                         }) | ||||
|                         .unwrap_or_default(); | ||||
|                     return_arr.push(Artifact::Cap(Cap { | ||||
|                         id: face_id, | ||||
|                         sub_type, | ||||
|                         edge_cut_edge_ids: Vec::new(), | ||||
|                         sweep_id: path_sweep_id, | ||||
|                         path_ids: Vec::new(), | ||||
|                         face_code_ref: CodeRef { | ||||
|                             range: sketch_on_face_source_range, | ||||
|                             path_to_node: Vec::new(), | ||||
|                         }, | ||||
|                     })); | ||||
|                     let Some(Artifact::Sweep(sweep)) = artifacts.get(&path_sweep_id) else { | ||||
|                         continue; | ||||
|  | ||||
| @ -253,9 +253,9 @@ pub struct Plane { | ||||
|     pub value: PlaneType, | ||||
|     /// Origin of the plane. | ||||
|     pub origin: Point3d, | ||||
|     /// What should the plane’s X axis be? | ||||
|     /// What should the plane's X axis be? | ||||
|     pub x_axis: Point3d, | ||||
|     /// What should the plane’s Y axis be? | ||||
|     /// What should the plane's Y axis be? | ||||
|     pub y_axis: Point3d, | ||||
|     /// The z-axis (normal). | ||||
|     pub z_axis: Point3d, | ||||
| @ -376,9 +376,9 @@ pub struct Face { | ||||
|     pub artifact_id: ArtifactId, | ||||
|     /// The tag of the face. | ||||
|     pub value: String, | ||||
|     /// What should the face’s X axis be? | ||||
|     /// What should the face's X axis be? | ||||
|     pub x_axis: Point3d, | ||||
|     /// What should the face’s Y axis be? | ||||
|     /// What should the face's Y axis be? | ||||
|     pub y_axis: Point3d, | ||||
|     /// The z-axis (normal). | ||||
|     pub z_axis: Point3d, | ||||
| @ -764,6 +764,19 @@ pub enum Path { | ||||
|         /// This is used to compute the tangential angle. | ||||
|         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. | ||||
|     Horizontal { | ||||
|         #[serde(flatten)] | ||||
| @ -806,6 +819,7 @@ enum PathType { | ||||
|     TangentialArc, | ||||
|     TangentialArcTo, | ||||
|     Circle, | ||||
|     CircleThreePoint, | ||||
|     Horizontal, | ||||
|     AngledLineTo, | ||||
|     Arc, | ||||
| @ -818,6 +832,7 @@ impl From<&Path> for PathType { | ||||
|             Path::TangentialArcTo { .. } => Self::TangentialArcTo, | ||||
|             Path::TangentialArc { .. } => Self::TangentialArc, | ||||
|             Path::Circle { .. } => Self::Circle, | ||||
|             Path::CircleThreePoint { .. } => Self::CircleThreePoint, | ||||
|             Path::Horizontal { .. } => Self::Horizontal, | ||||
|             Path::AngledLineTo { .. } => Self::AngledLineTo, | ||||
|             Path::Base { .. } => Self::Base, | ||||
| @ -836,6 +851,7 @@ impl Path { | ||||
|             Path::TangentialArcTo { base, .. } => base.geo_meta.id, | ||||
|             Path::TangentialArc { 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, | ||||
|         } | ||||
|     } | ||||
| @ -849,6 +865,7 @@ impl Path { | ||||
|             Path::TangentialArcTo { base, .. } => base.tag.clone(), | ||||
|             Path::TangentialArc { base, .. } => base.tag.clone(), | ||||
|             Path::Circle { base, .. } => base.tag.clone(), | ||||
|             Path::CircleThreePoint { base, .. } => base.tag.clone(), | ||||
|             Path::Arc { base, .. } => base.tag.clone(), | ||||
|         } | ||||
|     } | ||||
| @ -862,6 +879,7 @@ impl Path { | ||||
|             Path::TangentialArcTo { base, .. } => base, | ||||
|             Path::TangentialArc { base, .. } => base, | ||||
|             Path::Circle { base, .. } => base, | ||||
|             Path::CircleThreePoint { base, .. } => base, | ||||
|             Path::Arc { base, .. } => base, | ||||
|         } | ||||
|     } | ||||
| @ -899,6 +917,15 @@ impl Path { | ||||
|                 linear_distance(self.get_from(), self.get_to()) | ||||
|             } | ||||
|             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 { .. } => { | ||||
|                 // TODO: Call engine utils to figure this out. | ||||
|                 linear_distance(self.get_from(), self.get_to()) | ||||
| @ -915,6 +942,7 @@ impl Path { | ||||
|             Path::TangentialArcTo { base, .. } => Some(base), | ||||
|             Path::TangentialArc { base, .. } => Some(base), | ||||
|             Path::Circle { base, .. } => Some(base), | ||||
|             Path::CircleThreePoint { base, .. } => Some(base), | ||||
|             Path::Arc { base, .. } => Some(base), | ||||
|         } | ||||
|     } | ||||
| @ -934,6 +962,17 @@ impl Path { | ||||
|                 ccw: *ccw, | ||||
|                 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 { .. } => { | ||||
|                 let base = self.get_base(); | ||||
|                 GetTangentialInfoFromPathsResult::PreviousPoint(base.from) | ||||
|  | ||||
| @ -243,7 +243,8 @@ pub(crate) async fn do_post_extrude( | ||||
|                     Path::Arc { .. } | ||||
|                     | Path::TangentialArc { .. } | ||||
|                     | Path::TangentialArcTo { .. } | ||||
|                     | Path::Circle { .. } => { | ||||
|                     | Path::Circle { .. } | ||||
|                     | Path::CircleThreePoint { .. } => { | ||||
|                         let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc { | ||||
|                             face_id: *actual_face_id, | ||||
|                             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."}, | ||||
|     } | ||||
| }] | ||||
|  | ||||
| // 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( | ||||
|     p1: [f64; 2], | ||||
|     p2: [f64; 2], | ||||
| @ -191,18 +194,69 @@ async fn inner_circle_three_point( | ||||
|     args: Args, | ||||
| ) -> Result<Sketch, KclError> { | ||||
|     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. | ||||
|             radius: distance(center.into(), p2.into()), | ||||
|         }, | ||||
|         sketch_surface_or_group, | ||||
|         tag, | ||||
|     // It can be the distance to any of the 3 points - they all lay on the circumference. | ||||
|     let radius = distance(center.into(), p2.into()); | ||||
|  | ||||
|     let sketch_surface = match sketch_surface_or_group { | ||||
|         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, | ||||
|         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 | ||||
|  | ||||
| @ -49,7 +49,7 @@ flowchart LR | ||||
|   27[Wall] | ||||
|   28[Wall] | ||||
|   29[Wall] | ||||
|   30["Plane<br>[544, 571, 0]"] | ||||
|   30["Cap End"] | ||||
|   31["SweepEdge Opposite"] | ||||
|   32["SweepEdge Adjacent"] | ||||
|   33["SweepEdge Opposite"] | ||||
| @ -124,7 +124,7 @@ flowchart LR | ||||
|   26 --- 27 | ||||
|   26 --- 28 | ||||
|   26 --- 29 | ||||
|   26 x--> 30 | ||||
|   26 --- 30 | ||||
|   26 --- 31 | ||||
|   26 --- 32 | ||||
|   26 --- 33 | ||||
|  | ||||
| @ -55,22 +55,28 @@ description: Variables in memory after executing circle_three_point.kcl | ||||
|                 0 | ||||
|               ] | ||||
|             }, | ||||
|             "ccw": true, | ||||
|             "center": [ | ||||
|               24.749999999999996, | ||||
|               19.749999999999996 | ||||
|             ], | ||||
|             "from": [ | ||||
|               30.0059, | ||||
|               19.75 | ||||
|             ], | ||||
|             "radius": 5.255949010407163, | ||||
|             "p1": [ | ||||
|               25.0, | ||||
|               25.0 | ||||
|             ], | ||||
|             "p2": [ | ||||
|               30.0, | ||||
|               20.0 | ||||
|             ], | ||||
|             "p3": [ | ||||
|               27.0, | ||||
|               15.0 | ||||
|             ], | ||||
|             "tag": null, | ||||
|             "to": [ | ||||
|               30.0059, | ||||
|               19.75 | ||||
|             ], | ||||
|             "type": "Circle" | ||||
|             "type": "CircleThreePoint" | ||||
|           } | ||||
|         ], | ||||
|         "on": { | ||||
|  | ||||
| Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 55 KiB | 
| @ -20,7 +20,7 @@ flowchart LR | ||||
|   11[Wall] | ||||
|   12[Wall] | ||||
|   13["Cap Start"] | ||||
|   14["Plane<br>[298, 351, 0]"] | ||||
|   14["Cap End"] | ||||
|   15["SweepEdge Opposite"] | ||||
|   16["SweepEdge Adjacent"] | ||||
|   17["SweepEdge Opposite"] | ||||
| @ -59,7 +59,7 @@ flowchart LR | ||||
|   8 --- 11 | ||||
|   8 --- 12 | ||||
|   8 --- 13 | ||||
|   8 x--> 14 | ||||
|   8 --- 14 | ||||
|   8 --- 15 | ||||
|   8 --- 16 | ||||
|   8 --- 17 | ||||
|  | ||||
| @ -23,7 +23,7 @@ flowchart LR | ||||
|   11[Wall] | ||||
|   12[Wall] | ||||
|   13["Cap Start"] | ||||
|   14["Plane<br>[298, 323, 0]"] | ||||
|   14["Cap End"] | ||||
|   15["SweepEdge Opposite"] | ||||
|   16["SweepEdge Adjacent"] | ||||
|   17["SweepEdge Opposite"] | ||||
| @ -71,7 +71,7 @@ flowchart LR | ||||
|   8 --- 11 | ||||
|   8 --- 12 | ||||
|   8 --- 13 | ||||
|   8 x--> 14 | ||||
|   8 --- 14 | ||||
|   8 --- 15 | ||||
|   8 --- 16 | ||||
|   8 --- 17 | ||||
|  | ||||
| @ -23,7 +23,7 @@ flowchart LR | ||||
|   11[Wall] | ||||
|   12[Wall] | ||||
|   13["Cap Start"] | ||||
|   14["Plane<br>[298, 323, 0]"] | ||||
|   14["Cap End"] | ||||
|   15["SweepEdge Opposite"] | ||||
|   16["SweepEdge Adjacent"] | ||||
|   17["SweepEdge Opposite"] | ||||
| @ -71,7 +71,7 @@ flowchart LR | ||||
|   8 --- 11 | ||||
|   8 --- 12 | ||||
|   8 --- 13 | ||||
|   8 x--> 14 | ||||
|   8 --- 14 | ||||
|   8 --- 15 | ||||
|   8 --- 16 | ||||
|   8 --- 17 | ||||
|  | ||||
| @ -22,7 +22,7 @@ flowchart LR | ||||
|   10[Wall] | ||||
|   11[Wall] | ||||
|   12[Wall] | ||||
|   13["Plane<br>[303, 328, 0]"] | ||||
|   13["Cap Start"] | ||||
|   14["Cap End"] | ||||
|   15["SweepEdge Opposite"] | ||||
|   16["SweepEdge Adjacent"] | ||||
| @ -70,7 +70,7 @@ flowchart LR | ||||
|   8 --- 10 | ||||
|   8 --- 11 | ||||
|   8 --- 12 | ||||
|   8 x--> 13 | ||||
|   8 --- 13 | ||||
|   8 --- 14 | ||||
|   8 --- 15 | ||||
|   8 --- 16 | ||||
|  | ||||
