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