Compare commits

...

21 Commits

Author SHA1 Message Date
71b0787dcb Fix TS type error from missing import 2025-01-07 20:26:16 -05:00
a3cbb8bf90 Fix artifacts missing on ExecOutcome 2025-01-07 20:23:29 -05:00
f26f8e13df Merge branch 'main' into kurt-bring-back-multi-profile 2025-01-07 20:23:06 -05:00
2bb548c000 Trigger CI 2024-12-20 11:11:39 -05:00
b09c240e36 A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores) 2024-12-20 12:32:43 +00:00
6c9d14af93 partial fixes 2024-12-20 23:24:15 +11:00
0642e49189 A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores) 2024-12-20 23:21:32 +11:00
6add1d73ad chore: disabled file watcher which prevents faster file write (#4835) 2024-12-20 23:21:21 +11:00
68c89746c7 trigger CI 2024-12-20 22:14:49 +11:00
9f323c207c A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-16-cores) 2024-12-20 11:06:49 +00:00
7197b6c85d A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores) 2024-12-20 10:54:43 +00:00
913f2641c3 A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-macos-8-cores) 2024-12-20 10:53:02 +00:00
e9086c54ba lint 2024-12-20 21:46:48 +11:00
9f93346dc6 lint 2024-12-20 21:45:04 +11:00
1b9f5f20f5 Merge remote-tracking branch 'origin' into kurt-bring-back-multi-profile 2024-12-20 21:43:41 +11:00
3865637c61 Add Rust side artifacts for startSketchOn face or plane (#4834)
* Add Rust side artifacts for startSketchOn face or plane

* move ast digging

---------

Co-authored-by: Kurt Hutten Irev-Dev <k.hutten@protonmail.ch>
2024-12-19 11:03:21 +11:00
2c40e8a97c trigger CI 2024-12-18 10:12:18 +11:00
c696f0837a A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest-8-cores) 2024-12-17 22:58:41 +00:00
30edf2ad56 Merge remote-tracking branch 'origin' into kurt-bring-back-multi-profile 2024-12-18 09:53:34 +11:00
e60cabb193 fix poor 1000ms wait UX 2024-12-18 08:44:35 +11:00
1e9cf6f256 Revert "Revert multi-profile (#4812)"
This reverts commit efe8089b08.
2024-12-18 07:08:41 +11:00
50 changed files with 3840 additions and 1414 deletions

View File

@ -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), %)`)

View File

@ -38,8 +38,7 @@ test.describe('Can create sketches on all planes and their back sides', () => {
}, },
} }
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()

View File

@ -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 {
@ -72,17 +74,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,
@ -92,7 +103,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,
@ -109,7 +120,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,
@ -126,7 +137,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,
@ -214,7 +225,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
) => { ) => {
@ -236,22 +247,36 @@ export class SceneFixture {
} }
} }
function isColourArray(
colour: [number, number, number] | [number, number, number][]
): colour is [number, number, number][] {
return Array.isArray(colour[0])
}
export async function expectPixelColor( export async function expectPixelColor(
page: Page, page: Page,
colour: [number, number, number], colour: [number, number, number] | [number, number, number][],
coords: { x: number; y: number }, coords: { x: number; y: number },
diff: number diff: number
) { ) {
let finalValue = colour let finalValue = colour
await expect await expect
.poll(async () => { .poll(
async () => {
const pixel = (await getPixelRGBs(page)(coords, 1))[0] const pixel = (await getPixelRGBs(page)(coords, 1))[0]
if (!pixel) return null if (!pixel) return null
finalValue = pixel finalValue = pixel
if (!isColourArray(colour)) {
return pixel.every( return pixel.every(
(channel, index) => Math.abs(channel - colour[index]) < diff (channel, index) => Math.abs(channel - colour[index]) < diff
) )
}) }
return colour.some((c) =>
c.every((channel, index) => Math.abs(pixel[index] - channel) < diff)
)
},
{ timeout: 10_000 }
)
.toBeTruthy() .toBeTruthy()
.catch((cause) => { .catch((cause) => {
throw new Error( throw new Error(

View File

@ -18,7 +18,10 @@ export class ToolbarFixture {
offsetPlaneButton!: Locator offsetPlaneButton!: Locator
startSketchBtn!: Locator startSketchBtn!: Locator
lineBtn!: Locator lineBtn!: Locator
tangentialArcBtn!: Locator
circleBtn!: Locator
rectangleBtn!: Locator rectangleBtn!: Locator
lengthConstraintBtn!: Locator
exitSketchBtn!: Locator exitSketchBtn!: Locator
editSketchBtn!: Locator editSketchBtn!: Locator
fileTreeBtn!: Locator fileTreeBtn!: Locator
@ -44,7 +47,10 @@ export class ToolbarFixture {
this.offsetPlaneButton = page.getByTestId('plane-offset') this.offsetPlaneButton = page.getByTestId('plane-offset')
this.startSketchBtn = page.getByTestId('sketch') this.startSketchBtn = page.getByTestId('sketch')
this.lineBtn = page.getByTestId('line') this.lineBtn = page.getByTestId('line')
this.tangentialArcBtn = page.getByTestId('tangential-arc')
this.circleBtn = page.getByTestId('circle-center')
this.rectangleBtn = page.getByTestId('corner-rectangle') this.rectangleBtn = page.getByTestId('corner-rectangle')
this.lengthConstraintBtn = page.getByTestId('constraint-length')
this.exitSketchBtn = page.getByTestId('sketch-exit') this.exitSketchBtn = page.getByTestId('sketch-exit')
this.editSketchBtn = page.getByText('Edit Sketch') this.editSketchBtn = page.getByText('Edit Sketch')
this.fileTreeBtn = page.locator('[id="files-button-holder"]') this.fileTreeBtn = page.locator('[id="files-button-holder"]')
@ -103,6 +109,15 @@ export class ToolbarFixture {
await expect(this.exeIndicator).toBeVisible({ timeout: 15_000 }) await expect(this.exeIndicator).toBeVisible({ timeout: 15_000 })
} }
} }
selectCenterRectangle = async () => {
await this.page
.getByRole('button', { name: 'caret down Corner rectangle:' })
.click()
await expect(
this.page.getByTestId('dropdown-center-rectangle')
).toBeVisible()
await this.page.getByTestId('dropdown-center-rectangle').click()
}
async closePane(paneId: SidebarType) { async closePane(paneId: SidebarType) {
return closePane(this.page, paneId + SIDEBAR_BUTTON_SUFFIX) return closePane(this.page, paneId + SIDEBAR_BUTTON_SUFFIX)

View File

@ -437,7 +437,7 @@ test.describe('Onboarding tests', () => {
) )
}) })
test.fixme( test(
'Restarting onboarding on desktop takes one attempt', 'Restarting onboarding on desktop takes one attempt',
{ {
appSettings: { appSettings: {

View File

@ -218,16 +218,11 @@ test.describe('verify sketch on chamfer works', () => {
]}, %)`, ]}, %)`,
afterChamferSelectSnippet: 'sketch002 = startSketchOn(extrude001, seg03)', afterChamferSelectSnippet: 'sketch002 = startSketchOn(extrude001, seg03)',
afterRectangle1stClickSnippet: 'startProfileAt([205.96, 254.59], %)', afterRectangle1stClickSnippet:
'startProfileAt([205.96, 254.59], sketch002)',
afterRectangle2ndClickSnippet: `angledLine([0,11.39],%,$rectangleSegmentA002) afterRectangle2ndClickSnippet: `angledLine([0,11.39],%,$rectangleSegmentA002)
|> angledLine([ |>angledLine([segAng(rectangleSegmentA002)-90,105.26],%)
segAng(rectangleSegmentA002) - 90, |>angledLine([segAng(rectangleSegmentA002),-segLen(rectangleSegmentA002)],%)
105.26
], %, $rectangleSegmentB001)
|> angledLine([
segAng(rectangleSegmentA002),
-segLen(rectangleSegmentA002)
], %, $rectangleSegmentC001)
|>lineTo([profileStartX(%),profileStartY(%)],%) |>lineTo([profileStartX(%),profileStartY(%)],%)
|>close(%)`, |>close(%)`,
}) })
@ -249,19 +244,15 @@ test.describe('verify sketch on chamfer works', () => {
}, %)`, }, %)`,
afterChamferSelectSnippet: 'sketch003 = startSketchOn(extrude001, seg04)', afterChamferSelectSnippet: 'sketch003 = startSketchOn(extrude001, seg04)',
afterRectangle1stClickSnippet: 'startProfileAt([-209.64, 255.28], %)', afterRectangle1stClickSnippet:
'startProfileAt([-209.64, 255.28], sketch003)',
afterRectangle2ndClickSnippet: `angledLine([0,11.56],%,$rectangleSegmentA003) afterRectangle2ndClickSnippet: `angledLine([0,11.56],%,$rectangleSegmentA003)
|> angledLine([ |>angledLine([segAng(rectangleSegmentA003)-90,106.84],%)
segAng(rectangleSegmentA003) - 90, |>angledLine([segAng(rectangleSegmentA003),-segLen(rectangleSegmentA003)],%)
106.84
], %, $rectangleSegmentB002)
|> angledLine([
segAng(rectangleSegmentA003),
-segLen(rectangleSegmentA003)
], %, $rectangleSegmentC002)
|>lineTo([profileStartX(%),profileStartY(%)],%) |>lineTo([profileStartX(%),profileStartY(%)],%)
|>close(%)`, |>close(%)`,
}) })
await sketchOnAChamfer({ await sketchOnAChamfer({
clickCoords: { x: 677, y: 87 }, clickCoords: { x: 677, y: 87 },
cameraPos: { x: -6200, y: 1500, z: 6200 }, cameraPos: { x: -6200, y: 1500, z: 6200 },
@ -273,19 +264,14 @@ test.describe('verify sketch on chamfer works', () => {
getNextAdjacentEdge(seg02) getNextAdjacentEdge(seg02)
] ]
}, %)`, }, %)`,
afterChamferSelectSnippet: 'sketch003 = startSketchOn(extrude001, seg04)', afterChamferSelectSnippet: '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) |>lineTo([profileStartX(%),profileStartY(%)],%)|
|> angledLine([ >close(%)`,
segAng(rectangleSegmentA003),
-segLen(rectangleSegmentA003)
], %, $rectangleSegmentC002)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)`,
}) })
/// last one /// last one
await sketchOnAChamfer({ await sketchOnAChamfer({
@ -297,17 +283,11 @@ test.describe('verify sketch on chamfer works', () => {
tags = [getNextAdjacentEdge(yo)] tags = [getNextAdjacentEdge(yo)]
}, %)`, }, %)`,
afterChamferSelectSnippet: 'sketch005 = startSketchOn(extrude001, seg06)', afterChamferSelectSnippet: 'sketch005 = startSketchOn(extrude001, seg06)',
afterRectangle1stClickSnippet: 'startProfileAt([-23.43, 19.69], %)', afterRectangle1stClickSnippet:
'startProfileAt([-23.43, 19.69], sketch005)',
afterRectangle2ndClickSnippet: `angledLine([0,9.1],%,$rectangleSegmentA005) afterRectangle2ndClickSnippet: `angledLine([0,9.1],%,$rectangleSegmentA005)
|>angledLine([segAng(rectangleSegmentA005)-90,84.07],%)
|> angledLine([ |>angledLine([segAng(rectangleSegmentA005),-segLen(rectangleSegmentA005)],%)
segAng(rectangleSegmentA005) - 90,
84.07
], %, $rectangleSegmentB004)
|> angledLine([
segAng(rectangleSegmentA005),
-segLen(rectangleSegmentA005)
], %, $rectangleSegmentC004)
|>lineTo([profileStartX(%),profileStartY(%)],%) |>lineTo([profileStartX(%),profileStartY(%)],%)
|>close(%)`, |>close(%)`,
}) })
@ -315,7 +295,6 @@ test.describe('verify sketch on chamfer works', () => {
await test.step('verify at the end of the test that final code is what is expected', async () => { await test.step('verify at the end of the test that final code is what is expected', async () => {
await editor.expectEditor.toContain( await editor.expectEditor.toContain(
`sketch001 = startSketchOn('XZ') `sketch001 = startSketchOn('XZ')
|> startProfileAt([75.8, 317.2], %) // [$startCapTag, $EndCapTag] |> startProfileAt([75.8, 317.2], %) // [$startCapTag, $EndCapTag]
|> angledLine([0, 268.43], %, $rectangleSegmentA001) |> angledLine([0, 268.43], %, $rectangleSegmentA001)
|> angledLine([ |> angledLine([
@ -343,55 +322,55 @@ test.describe('verify sketch on chamfer works', () => {
tags = [getNextAdjacentEdge(yo)] tags = [getNextAdjacentEdge(yo)]
}, %, $seg06) }, %, $seg06)
sketch005 = startSketchOn(extrude001, seg06) sketch005 = startSketchOn(extrude001, seg06)
|> startProfileAt([-23.43,19.69], %) profile004 = startProfileAt([-23.43, 19.69], sketch005)
|> angledLine([0, 9.1], %, $rectangleSegmentA005) |> angledLine([0, 9.1], %, $rectangleSegmentA005)
|> angledLine([ |> angledLine([
segAng(rectangleSegmentA005) - 90, segAng(rectangleSegmentA005) - 90,
84.07 84.07
], %, $rectangleSegmentB004) ], %)
|> angledLine([ |> angledLine([
segAng(rectangleSegmentA005), segAng(rectangleSegmentA005),
-segLen(rectangleSegmentA005) -segLen(rectangleSegmentA005)
], %, $rectangleSegmentC004) ], %)
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%) |> close(%)
sketch004 = startSketchOn(extrude001, seg05) sketch004 = startSketchOn(extrude001, seg05)
|> startProfileAt([82.57,322.96], %) profile003 = startProfileAt([82.57, 322.96], sketch004)
|> angledLine([0, 11.16], %, $rectangleSegmentA004) |> angledLine([0, 11.16], %, $rectangleSegmentA004)
|> angledLine([ |> angledLine([
segAng(rectangleSegmentA004) - 90, segAng(rectangleSegmentA004) - 90,
103.07 103.07
], %, $rectangleSegmentB003) ], %)
|> angledLine([ |> angledLine([
segAng(rectangleSegmentA004), segAng(rectangleSegmentA004),
-segLen(rectangleSegmentA004) -segLen(rectangleSegmentA004)
], %, $rectangleSegmentC003) ], %)
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%) |> close(%)
sketch003 = startSketchOn(extrude001, seg04) sketch003 = startSketchOn(extrude001, seg04)
|> startProfileAt([-209.64,255.28], %) profile002 = startProfileAt([-209.64, 255.28], sketch003)
|> angledLine([0, 11.56], %, $rectangleSegmentA003) |> angledLine([0, 11.56], %, $rectangleSegmentA003)
|> angledLine([ |> angledLine([
segAng(rectangleSegmentA003) - 90, segAng(rectangleSegmentA003) - 90,
106.84 106.84
], %, $rectangleSegmentB002) ], %)
|> angledLine([ |> angledLine([
segAng(rectangleSegmentA003), segAng(rectangleSegmentA003),
-segLen(rectangleSegmentA003) -segLen(rectangleSegmentA003)
], %, $rectangleSegmentC002) ], %)
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%) |> close(%)
sketch002 = startSketchOn(extrude001, seg03) sketch002 = startSketchOn(extrude001, seg03)
|> startProfileAt([205.96,254.59], %) profile001 = startProfileAt([205.96, 254.59], sketch002)
|> angledLine([0, 11.39], %, $rectangleSegmentA002) |> angledLine([0, 11.39], %, $rectangleSegmentA002)
|> angledLine([ |> angledLine([
segAng(rectangleSegmentA002) - 90, segAng(rectangleSegmentA002) - 90,
105.26 105.26
], %, $rectangleSegmentB001) ], %)
|> angledLine([ |> angledLine([
segAng(rectangleSegmentA002), segAng(rectangleSegmentA002),
-segLen(rectangleSegmentA002) -segLen(rectangleSegmentA002)
], %, $rectangleSegmentC001) ], %)
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%) |> close(%)
`, `,
@ -437,16 +416,11 @@ test.describe('verify sketch on chamfer works', () => {
]}, extrude001)`, ]}, extrude001)`,
beforeChamferSnippetEnd: '}, extrude001)', beforeChamferSnippetEnd: '}, extrude001)',
afterChamferSelectSnippet: 'sketch002 = startSketchOn(extrude001, seg03)', afterChamferSelectSnippet: 'sketch002 = startSketchOn(extrude001, seg03)',
afterRectangle1stClickSnippet: 'startProfileAt([205.96, 254.59], %)', afterRectangle1stClickSnippet:
'startProfileAt([205.96, 254.59], sketch002)',
afterRectangle2ndClickSnippet: `angledLine([0,11.39],%,$rectangleSegmentA002) afterRectangle2ndClickSnippet: `angledLine([0,11.39],%,$rectangleSegmentA002)
|> angledLine([ |>angledLine([segAng(rectangleSegmentA002)-90,105.26],%)
segAng(rectangleSegmentA002) - 90, |>angledLine([segAng(rectangleSegmentA002),-segLen(rectangleSegmentA002)],%)
105.26
], %, $rectangleSegmentB001)
|> angledLine([
segAng(rectangleSegmentA002),
-segLen(rectangleSegmentA002)
], %, $rectangleSegmentC001)
|>lineTo([profileStartX(%),profileStartY(%)],%) |>lineTo([profileStartX(%),profileStartY(%)],%)
|>close(%)`, |>close(%)`,
}) })
@ -478,16 +452,16 @@ 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) ], %)
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%) |> close(%)
`, `,
@ -555,10 +529,10 @@ test(`Verify axis, origin, and horizontal snapping`, async ({
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 () => {

File diff suppressed because it is too large Load Diff

View File

@ -451,8 +451,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)
@ -474,6 +473,10 @@ test(
.getByRole('button', { name: 'arc Tangential Arc', exact: true }) .getByRole('button', { name: 'arc Tangential Arc', exact: true })
.click() .click()
// click to continue profile
await page.mouse.move(813, 392, { steps: 10 })
await page.waitForTimeout(100)
await page.mouse.move(startXPx + PUR * 30, 500 - PUR * 20, { steps: 10 }) await page.mouse.move(startXPx + PUR * 30, 500 - PUR * 20, { steps: 10 })
await page.waitForTimeout(1000) await page.waitForTimeout(1000)
@ -596,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 }, %)`
) )
} }
) )
@ -641,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)
@ -660,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 += `
@ -746,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)
@ -765,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 += `

View File

@ -1,6 +1,7 @@
import { test, expect } from './zoo-test' import { test, expect } from './zoo-test'
import { commonPoints, getUtils } from './test-utils' import { commonPoints, getUtils } from './test-utils'
import { EngineCommand } from 'lang/std/artifactGraph'
import { uuidv4 } from 'lib/utils'
test.describe('Test network and connection issues', () => { test.describe('Test network and connection issues', () => {
test('simulate network down and network little widget', async ({ test('simulate network down and network little widget', async ({
@ -110,17 +111,16 @@ test.describe('Test network and connection issues', () => {
const startXPx = 600 const startXPx = 600
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content')).toHaveText(
.toHaveText(`sketch001 = startSketchOn('XZ') `sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${commonPoints.startAt}, sketch001)`
|> startProfileAt(${commonPoints.startAt}, %)`) )
await page.waitForTimeout(100) await page.waitForTimeout(100)
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await page.waitForTimeout(100) await page.waitForTimeout(100)
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`sketch001 = startSketchOn('XZ') .toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${commonPoints.startAt}, sketch001)
|> startProfileAt(${commonPoints.startAt}, %)
|> xLine(${commonPoints.num1}, %)`) |> xLine(${commonPoints.num1}, %)`)
// Expect the network to be up // Expect the network to be up
@ -168,7 +168,9 @@ test.describe('Test network and connection issues', () => {
await page.mouse.click(100, 100) await page.mouse.click(100, 100)
// select a line // select a line
await page.getByText(`startProfileAt(${commonPoints.startAt}, %)`).click() await page
.getByText(`startProfileAt(${commonPoints.startAt}, sketch001)`)
.click()
// enter sketch again // enter sketch again
await u.doAndWaitForCmd( await u.doAndWaitForCmd(
@ -182,11 +184,36 @@ test.describe('Test network and connection issues', () => {
await page.waitForTimeout(150) await page.waitForTimeout(150)
const camCommand: EngineCommand = {
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
center: { x: 109, y: 0, z: -152 },
vantage: { x: 115, y: -505, z: -152 },
up: { x: 0, y: 0, z: 1 },
},
}
const updateCamCommand: EngineCommand = {
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
}
await u.sendCustomCmd(camCommand)
await page.waitForTimeout(100)
await u.sendCustomCmd(updateCamCommand)
await page.waitForTimeout(100)
// click to continue profile
await page.mouse.click(1007, 400)
await page.waitForTimeout(100)
// Ensure we can continue sketching // Ensure we can continue sketching
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20) await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
await expect.poll(u.normalisedEditorCode) await expect.poll(u.normalisedEditorCode)
.toBe(`sketch001 = startSketchOn('XZ') .toBe(`sketch001 = startSketchOn('XZ')
|> startProfileAt([12.34, -12.34], %) profile001 = startProfileAt([12.34, -12.34], sketch001)
|> xLine(12.34, %) |> xLine(12.34, %)
|> line([-12.34, 12.34], %) |> line([-12.34, 12.34], %)
@ -196,7 +223,7 @@ test.describe('Test network and connection issues', () => {
await expect.poll(u.normalisedEditorCode) await expect.poll(u.normalisedEditorCode)
.toBe(`sketch001 = startSketchOn('XZ') .toBe(`sketch001 = startSketchOn('XZ')
|> startProfileAt([12.34, -12.34], %) profile001 = startProfileAt([12.34, -12.34], sketch001)
|> xLine(12.34, %) |> xLine(12.34, %)
|> line([-12.34, 12.34], %) |> line([-12.34, 12.34], %)
|> xLine(-12.34, %) |> xLine(-12.34, %)

View File

@ -69,30 +69,31 @@ test.describe('Testing selections', () => {
const startXPx = 600 const startXPx = 600
await u.closeDebugPanel() await u.closeDebugPanel()
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content')).toHaveText(
.toHaveText(`sketch001 = startSketchOn('XZ') `sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${commonPoints.startAt}, sketch001)`
|> startProfileAt(${commonPoints.startAt}, %)`) )
await page.waitForTimeout(100) await page.waitForTimeout(100)
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`sketch001 = startSketchOn('XZ') .toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${commonPoints.startAt}, sketch001)
|> startProfileAt(${commonPoints.startAt}, %)
|> xLine(${commonPoints.num1}, %)`) |> xLine(${commonPoints.num1}, %)`)
await page.waitForTimeout(100) await page.waitForTimeout(100)
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20) await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`sketch001 = startSketchOn('XZ') .toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${
|> startProfileAt(${commonPoints.startAt}, %) commonPoints.startAt
}, sketch001)
|> xLine(${commonPoints.num1}, %) |> xLine(${commonPoints.num1}, %)
|> yLine(${commonPoints.num1 + 0.01}, %)`) |> yLine(${commonPoints.num1 + 0.01}, %)`)
await page.waitForTimeout(100) await page.waitForTimeout(100)
await page.mouse.click(startXPx, 500 - PUR * 20) await page.mouse.click(startXPx, 500 - PUR * 20)
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`sketch001 = startSketchOn('XZ') .toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${
|> startProfileAt(${commonPoints.startAt}, %) commonPoints.startAt
}, sketch001)
|> xLine(${commonPoints.num1}, %) |> xLine(${commonPoints.num1}, %)
|> yLine(${commonPoints.num1 + 0.01}, %) |> yLine(${commonPoints.num1 + 0.01}, %)
|> xLine(${commonPoints.num2 * -1}, %)`) |> xLine(${commonPoints.num2 * -1}, %)`)
@ -321,7 +322,29 @@ test.describe('Testing selections', () => {
|> line([0, pipeLength], %) |> line([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)
], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
profile003 = startProfileAt([40.16, -120.48], sketch006)
|> line([26.95, 24.21], %)
|> line([20.91, -28.61], %)
|> line([32.46, 18.71], %)
` `
) )
}, KCL_DEFAULT_LENGTH) }, KCL_DEFAULT_LENGTH)
@ -354,9 +377,10 @@ test.describe('Testing selections', () => {
}) })
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)
@ -422,6 +446,20 @@ test.describe('Testing selections', () => {
await u.expectCmdLog('[data-message-type="execution-done"]', 10_000) await u.expectCmdLog('[data-message-type="execution-done"]', 10_000)
await page.waitForTimeout(200) await page.waitForTimeout(200)
await expect(u.codeLocator).not.toContainText(`sketch005 = startSketchOn({`) await expect(u.codeLocator).not.toContainText(`sketch005 = startSketchOn({`)
// Delete a single profile
await page.mouse.click(individualProfile.x, individualProfile.y)
await page.waitForTimeout(100)
const codeToBeDeletedSnippet =
'profile003 = startProfileAt([40.16, -120.48], sketch006)'
await expect(page.locator('.cm-activeLine')).toHaveText(
' |> line([20.91, -28.61], %)'
)
await u.clearCommandLogs()
await page.keyboard.press('Backspace')
await u.expectCmdLog('[data-message-type="execution-done"]', 10_000)
await page.waitForTimeout(200)
await expect(u.codeLocator).not.toContainText(codeToBeDeletedSnippet)
}) })
test("Deleting solid that the AST mod can't handle results in a toast message", async ({ test("Deleting solid that the AST mod can't handle results in a toast message", async ({
page, page,
@ -1273,12 +1311,15 @@ test.describe('Testing selections', () => {
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()
@ -1290,14 +1331,23 @@ test.describe('Testing selections', () => {
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(

View File

@ -205,8 +205,13 @@ test('First escape in tool pops you out of tool, second exits sketch mode', asyn
// Draw a line // Draw a line
await page.mouse.move(700, 200, { steps: 5 }) await page.mouse.move(700, 200, { steps: 5 })
await page.mouse.click(700, 200) await page.mouse.click(700, 200)
await page.mouse.move(800, 250, { steps: 5 })
await page.mouse.click(800, 250) const secondMousePosition = { x: 800, y: 250 }
await page.mouse.move(secondMousePosition.x, secondMousePosition.y, {
steps: 5,
})
await page.mouse.click(secondMousePosition.x, secondMousePosition.y)
// Unequip line tool // Unequip line tool
await page.keyboard.press('Escape') await page.keyboard.press('Escape')
// Make sure we didn't pop out of sketch mode. // Make sure we didn't pop out of sketch mode.
@ -215,9 +220,17 @@ test('First escape in tool pops you out of tool, second exits sketch mode', asyn
// Equip arc tool // Equip arc tool
await page.keyboard.press('a') await page.keyboard.press('a')
await expect(arcButton).toHaveAttribute('aria-pressed', 'true') await expect(arcButton).toHaveAttribute('aria-pressed', 'true')
// click in the same position again to continue the profile
await page.mouse.move(secondMousePosition.x, secondMousePosition.y, {
steps: 5,
})
await page.mouse.click(secondMousePosition.x, secondMousePosition.y)
await page.mouse.move(1000, 100, { steps: 5 }) await page.mouse.move(1000, 100, { steps: 5 })
await page.mouse.click(1000, 100) await page.mouse.click(1000, 100)
await page.keyboard.press('Escape') await page.keyboard.press('Escape')
await expect(arcButton).toHaveAttribute('aria-pressed', 'false')
await page.keyboard.press('l') await page.keyboard.press('l')
await expect(lineButton).toHaveAttribute('aria-pressed', 'true') await expect(lineButton).toHaveAttribute('aria-pressed', 'true')
@ -519,9 +532,9 @@ extrude001 = extrude(5 + 7, sketch001)`
await expect.poll(u.normalisedEditorCode).toContain( await expect.poll(u.normalisedEditorCode).toContain(
u.normalisedCode(`sketch002 = startSketchOn(extrude001, seg01) u.normalisedCode(`sketch002 = startSketchOn(extrude001, seg01)
|> startProfileAt([-12.94, 6.6], %) profile001 = startProfileAt([-12.88, 6.66], sketch002)
|> line([2.45, -0.2], %) |> line([2.71, -0.22], %)
|> line([-2.6, -1.25], %) |> line([-2.87, -1.38], %)
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%) |> close(%)
`) `)
@ -537,9 +550,8 @@ extrude001 = extrude(5 + 7, sketch001)`
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()

View File

@ -6,7 +6,6 @@ import { useCommandsContext } from 'hooks/useCommandsContext'
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'
@ -22,6 +21,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'
export function Toolbar({ export function Toolbar({
className = '', className = '',
@ -38,7 +38,12 @@ export function Toolbar({
'!border-transparent hover:!border-chalkboard-20 dark:enabled:hover:!border-primary pressed:!border-primary ui-open:!border-primary' '!border-transparent hover:!border-chalkboard-20 dark:enabled:hover:!border-primary pressed:!border-primary ui-open:!border-primary'
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,

View File

@ -438,6 +438,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,

File diff suppressed because it is too large Load Diff

View File

@ -696,19 +696,21 @@ export function createProfileStartHandle({
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 })

View File

@ -21,7 +21,6 @@ import { ContextMenu, ContextMenuItem } from './ContextMenu'
import usePlatform from 'hooks/usePlatform' import usePlatform from 'hooks/usePlatform'
import { FileEntry } from 'lib/project' import { FileEntry } from 'lib/project'
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher' import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
import { normalizeLineEndings } from 'lib/codeEditor'
import { reportRejection } from 'lib/trap' import { reportRejection } from 'lib/trap'
function getIndentationCSS(level: number) { function getIndentationCSS(level: number) {
@ -188,24 +187,25 @@ const FileTreeItem = ({
// Because subtrees only render when they are opened, that means this // Because subtrees only render when they are opened, that means this
// only listens when they open. Because this acts like a useEffect, when // only listens when they open. Because this acts like a useEffect, when
// the ReactNodes are destroyed, so is this listener :) // the ReactNodes are destroyed, so is this listener :)
useFileSystemWatcher( /** Disabling this in favor of faster file writes until we fix file writing **/
async (eventType, path) => { /* useFileSystemWatcher(
// Prevents a cyclic read / write causing editor problems such as * async (eventType, path) => {
// misplaced cursor positions. * // Prevents a cyclic read / write causing editor problems such as
if (codeManager.writeCausedByAppCheckedInFileTreeFileSystemWatcher) { * // misplaced cursor positions.
codeManager.writeCausedByAppCheckedInFileTreeFileSystemWatcher = false * if (codeManager.writeCausedByAppCheckedInFileTreeFileSystemWatcher) {
return * codeManager.writeCausedByAppCheckedInFileTreeFileSystemWatcher = false
} * return
* }
if (isCurrentFile && eventType === 'change') { * if (isCurrentFile && eventType === 'change') {
let code = await window.electron.readFile(path, { encoding: 'utf-8' }) * let code = await window.electron.readFile(path, { encoding: 'utf-8' })
code = normalizeLineEndings(code) * code = normalizeLineEndings(code)
codeManager.updateCodeStateEditor(code) * codeManager.updateCodeStateEditor(code)
} * }
fileSend({ type: 'Refresh' }) * fileSend({ type: 'Refresh' })
}, * },
[fileOrDir.path] * [fileOrDir.path]
) * ) */
const showNewTreeEntry = const showNewTreeEntry =
newTreeEntry !== undefined && newTreeEntry !== undefined &&

View File

@ -24,7 +24,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,
@ -64,20 +64,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 {
PathToNode,
Program,
VariableDeclaration,
parse,
recast,
resultIsOk,
} from 'lang/wasm'
import { import {
artifactIsPlaneWithPaths, artifactIsPlaneWithPaths,
getNodePathFromSourceRange, doesSketchPipeNeedSplitting,
isSingleCursorInPipe, getNodeFromPath,
isCursorInFunctionDefinition,
traverse,
} from 'lang/queryAst' } 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 { useCommandsContext } from 'hooks/useCommandsContext' import { useCommandsContext } from 'hooks/useCommandsContext'
import { import {
ExportIntent, ExportIntent,
@ -89,6 +99,10 @@ import { useFileContext } from 'hooks/useFileContext'
import { uuidv4 } from 'lib/utils' import { uuidv4 } from 'lib/utils'
import { IndexLoaderData } from 'lib/types' import { IndexLoaderData } from 'lib/types'
import { Node } from 'wasm-lib/kcl/bindings/Node' import { Node } from 'wasm-lib/kcl/bindings/Node'
import {
getPathsFromArtifact,
getPlaneFromArtifact,
} from 'lang/std/artifactGraph'
import { promptToEditFlow } from 'lib/promptToEdit' import { promptToEditFlow } from 'lib/promptToEdit'
import { kclEditorActor } from 'machines/kclEditorMachine' import { kclEditorActor } from 'machines/kclEditorMachine'
@ -281,7 +295,7 @@ export const ModelingMachineProvider = ({
return { return {
sketchDetails: { sketchDetails: {
...sketchDetails, ...sketchDetails,
sketchPathToNode: event.data, sketchEntryNodePath: event.data,
}, },
} }
}), }),
@ -396,9 +410,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 ||
[], [],
}, },
} }
@ -552,7 +574,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,
@ -583,10 +610,32 @@ export const ModelingMachineProvider = ({
// this assumes no changes have been made to the sketch besides what we did when entering the sketch // this assumes no changes have been made to the sketch besides what we did when entering the sketch
// i.e. doesn't account for user's adding code themselves, maybe we need store a flag userEditedSinceSketchMode? // i.e. doesn't account for user's adding code themselves, maybe we need store a flag userEditedSinceSketchMode?
const newAst = structuredClone(kclManager.ast) const newAst = structuredClone(kclManager.ast)
const varDecIndex = sketchDetails.sketchPathToNode[1][0] const varDecIndex = sketchDetails.planeNodePath[1][0]
const varDec = getNodeFromPath<VariableDeclaration>(
newAst,
sketchDetails.planeNodePath,
'VariableDeclaration'
)
if (err(varDec)) return reject(new Error('No varDec'))
const variableName = varDec.node.declaration.id.name
let isIdentifierUsed = false
traverse(newAst, {
enter: (node) => {
if (
node.type === 'Identifier' &&
node.name === variableName
) {
isIdentifierUsed = true
}
},
})
if (isIdentifierUsed) return
// remove body item at varDecIndex // remove body item at varDecIndex
newAst.body = newAst.body.filter((_, i) => i !== varDecIndex) newAst.body = newAst.body.filter((_, i) => i !== varDecIndex)
await kclManager.executeAstMock(newAst) await kclManager.executeAstMock(newAst)
await codeManager.updateEditorWithAstAndWriteToFile(newAst)
} }
sceneInfra.setCallbacks({ sceneInfra.setCallbacks({
onClick: () => {}, onClick: () => {},
@ -596,7 +645,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'
@ -623,7 +672,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,
@ -643,7 +694,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],
@ -651,12 +704,14 @@ export const ModelingMachineProvider = ({
}), }),
'animate-to-sketch': fromPromise( 'animate-to-sketch': fromPromise(
async ({ input: { selectionRanges } }) => { async ({ input: { selectionRanges } }) => {
const sourceRange = const sketchPathToNode =
selectionRanges.graphSelections[0]?.codeRef?.range selectionRanges.graphSelections[0]?.codeRef?.pathToNode
const sketchPathToNode = getNodePathFromSourceRange( const plane = getPlaneFromArtifact(
kclManager.ast, selectionRanges.graphSelections[0].artifact,
sourceRange engineCommandManager.artifactGraph
) )
if (err(plane)) return Promise.reject(plane)
const info = await getSketchOrientationDetails( const info = await getSketchOrientationDetails(
sketchPathToNode || [] sketchPathToNode || []
) )
@ -664,8 +719,17 @@ export const ModelingMachineProvider = ({
engineCommandManager, engineCommandManager,
info?.sketchDetails?.faceId || '' info?.sketchDetails?.faceId || ''
) )
return { const sketchPaths = getPathsFromArtifact({
artifact: selectionRanges.graphSelections[0].artifact,
sketchPathToNode: sketchPathToNode || [], sketchPathToNode: sketchPathToNode || [],
})
if (err(sketchPaths)) return Promise.reject(sketchPaths)
if (!plane.codeRef)
return Promise.reject(new Error('No plane codeRef'))
return {
sketchEntryNodePath: sketchPathToNode || [],
sketchNodePaths: sketchPaths,
planeNodePath: plane.codeRef.pathToNode,
zAxis: info.sketchDetails.zAxis || null, 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(
@ -677,7 +741,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,
@ -689,13 +753,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,
@ -716,13 +790,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,
@ -733,13 +809,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,
@ -760,7 +846,9 @@ export const ModelingMachineProvider = ({
return { return {
selectionType: 'completeSelection', selectionType: 'completeSelection',
selection, selection,
updatedPathToNode, updatedSketchEntryNodePath,
updatedSketchNodePaths,
updatedPlaneNodePath,
} }
} }
), ),
@ -770,7 +858,8 @@ export const ModelingMachineProvider = ({
selectionRanges, selectionRanges,
}) })
if (err(info)) return Promise.reject(info) if (err(info)) return Promise.reject(info)
const { modifiedAst, pathToNodeMap } = await (info.enabled const { modifiedAst, pathToNodeMap, exprInsertIndex } =
await (info.enabled
? applyConstraintAngleBetween({ ? applyConstraintAngleBetween({
selectionRanges, selectionRanges,
}) })
@ -786,13 +875,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,
@ -813,7 +912,9 @@ export const ModelingMachineProvider = ({
return { return {
selectionType: 'completeSelection', selectionType: 'completeSelection',
selection, selection,
updatedPathToNode, updatedSketchEntryNodePath,
updatedSketchNodePaths,
updatedPlaneNodePath,
} }
} }
), ),
@ -828,20 +929,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,
@ -862,13 +973,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,
}) })
@ -878,13 +991,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,
@ -905,13 +1027,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,
@ -922,13 +1046,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,
@ -949,13 +1082,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,
@ -966,13 +1101,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,
@ -993,7 +1137,9 @@ export const ModelingMachineProvider = ({
return { return {
selectionType: 'completeSelection', selectionType: 'completeSelection',
selection, selection,
updatedPathToNode, updatedSketchEntryNodePath,
updatedSketchNodePaths,
updatedPlaneNodePath,
} }
} }
), ),
@ -1013,9 +1159,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
@ -1045,6 +1193,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,
@ -1075,10 +1224,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,
@ -1099,7 +1260,140 @@ 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')
await sceneEntitiesManager.tearDownSketch({ removeAxis: false })
const result = await sceneEntitiesManager.setupDraftCircle(
sketchDetails.sketchEntryNodePath,
sketchDetails.sketchNodePaths,
sketchDetails.planeNodePath,
sketchDetails.zAxis,
sketchDetails.yAxis,
sketchDetails.origin,
data
)
if (err(result)) return reject(result)
await codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
return result
}
),
'set-up-draft-rectangle': fromPromise(
async ({ input: { sketchDetails, data } }) => {
if (!sketchDetails || !data)
return reject('No sketch details or data')
await sceneEntitiesManager.tearDownSketch({ removeAxis: false })
const result = await sceneEntitiesManager.setupDraftRectangle(
sketchDetails.sketchEntryNodePath,
sketchDetails.sketchNodePaths,
sketchDetails.planeNodePath,
sketchDetails.zAxis,
sketchDetails.yAxis,
sketchDetails.origin,
data
)
if (err(result)) return reject(result)
await codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
return result
}
),
'set-up-draft-center-rectangle': fromPromise(
async ({ input: { sketchDetails, data } }) => {
if (!sketchDetails || !data)
return reject('No sketch details or data')
await sceneEntitiesManager.tearDownSketch({ removeAxis: false })
const result = await sceneEntitiesManager.setupDraftCenterRectangle(
sketchDetails.sketchEntryNodePath,
sketchDetails.sketchNodePaths,
sketchDetails.planeNodePath,
sketchDetails.zAxis,
sketchDetails.yAxis,
sketchDetails.origin,
data
)
if (err(result)) return reject(result)
await codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
return result
}
),
'setup-client-side-sketch-segments': fromPromise(
async ({ input: { sketchDetails, selectionRanges } }) => {
if (!sketchDetails) return
if (!sketchDetails.sketchEntryNodePath.length) return
if (Object.keys(sceneEntitiesManager.activeSegments).length > 0) {
sceneEntitiesManager.tearDownSketch({ removeAxis: false })
}
sceneInfra.resetMouseListeners()
await sceneEntitiesManager.setupSketch({
sketchEntryNodePath: sketchDetails?.sketchEntryNodePath || [],
sketchNodePaths: sketchDetails.sketchNodePaths,
forward: sketchDetails.zAxis,
up: sketchDetails.yAxis,
position: sketchDetails.origin,
maybeModdedAst: kclManager.ast,
selectionRanges,
})
sceneInfra.resetMouseListeners()
sceneEntitiesManager.setupSketchIdleCallbacks({
sketchEntryNodePath: sketchDetails?.sketchEntryNodePath || [],
forward: sketchDetails.zAxis,
up: sketchDetails.yAxis,
position: sketchDetails.origin,
sketchNodePaths: sketchDetails.sketchNodePaths,
planeNodePath: sketchDetails.planeNodePath,
})
return undefined
}
),
'split-sketch-pipe-if-needed': fromPromise(
async ({ input: { sketchDetails } }) => {
if (!sketchDetails) return reject('No sketch details')
const existingSketchInfoNoOp = {
updatedEntryNodePath: sketchDetails.sketchEntryNodePath,
updatedSketchNodePaths: sketchDetails.sketchNodePaths,
updatedPlaneNodePath: sketchDetails.planeNodePath,
} as const
if (
!sketchDetails.sketchNodePaths.length &&
sketchDetails.planeNodePath.length
) {
// new sketch, no profiles yet
return existingSketchInfoNoOp
}
const doesNeedSplitting = doesSketchPipeNeedSplitting(
kclManager.ast,
sketchDetails.sketchEntryNodePath
)
if (err(doesNeedSplitting)) return reject(doesNeedSplitting)
if (!doesNeedSplitting) return existingSketchInfoNoOp
const splitResult = splitPipedProfile(
kclManager.ast,
sketchDetails.sketchEntryNodePath
)
if (err(splitResult)) return reject(splitResult)
await kclManager.executeAstMock(splitResult.modifiedAst)
await codeManager.updateEditorWithAstAndWriteToFile(
splitResult.modifiedAst
)
return {
updatedEntryNodePath: splitResult.pathToProfile,
updatedSketchNodePaths: [splitResult.pathToProfile],
updatedPlaneNodePath: sketchDetails.planeNodePath,
} }
} }
), ),

View File

@ -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"

View File

@ -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,
} }
} }

View File

@ -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)

View File

@ -93,6 +93,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,
@ -132,6 +133,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(
@ -144,8 +146,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({

View File

@ -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,
} }
} }

View File

@ -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,
} }
} }
} }

View File

@ -70,10 +70,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
@ -94,7 +98,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({
@ -112,6 +116,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,
} }
} }
@ -124,6 +134,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)
@ -208,5 +219,6 @@ export async function applyConstraintAngleLength({
return { return {
modifiedAst: _modifiedAst, modifiedAst: _modifiedAst,
pathToNodeMap, pathToNodeMap,
exprInsertIndex: variableName ? newVariableInsertIndex : -1,
} }
} }

View File

@ -376,12 +376,13 @@ export class KclManager {
} }
this.ast = { ...ast } this.ast = { ...ast }
// updateArtifactGraph relies on updated executeState/programMemory // updateArtifactGraph relies on updated executeState/programMemory
await this.engineCommandManager.updateArtifactGraph(this.ast) await this.engineCommandManager.updateArtifactGraph(
this.ast,
execState.artifacts
)
this._executeCallback() this._executeCallback()
if (!isInterrupted) { if (!isInterrupted)
sceneInfra.modelingSend({ type: 'code edit during sketch' }) sceneInfra.modelingSend({ type: 'code edit during sketch' })
}
this.engineCommandManager.addCommandLog({ this.engineCommandManager.addCommandLog({
type: 'execution-done', type: 'execution-done',
data: null, data: null,
@ -423,6 +424,7 @@ export class KclManager {
this._logs = logs this._logs = logs
this.addDiagnostics(kclErrorsToDiagnostics(errors)) this.addDiagnostics(kclErrorsToDiagnostics(errors))
this._execState = execState this._execState = execState
this._programMemory = execState.memory this._programMemory = execState.memory
if (!errors.length) { if (!errors.length) {
@ -435,7 +437,7 @@ export class KclManager {
// problem this solves, but either way we should strive to remove it. // problem this solves, but either way we should strive to remove it.
Array.from(this.engineCommandManager.artifactGraph).forEach( Array.from(this.engineCommandManager.artifactGraph).forEach(
([commandId, artifact]) => { ([commandId, artifact]) => {
if (!('codeRef' in artifact)) return if (!('codeRef' in artifact && artifact.codeRef)) return
const _node1 = getNodeFromPath<Node<CallExpression>>( const _node1 = getNodeFromPath<Node<CallExpression>>(
this.ast, this.ast,
artifact.codeRef.pathToNode, artifact.codeRef.pathToNode,

View File

@ -153,7 +153,7 @@ export default class CodeManager {
toast.error('Error saving file, please check file permissions') toast.error('Error saving file, please check file permissions')
reject(err) reject(err)
}) })
}, 1000) }, 10)
}) })
} else { } else {
safeLSSetItem(PERSIST_CODE_KEY, this.code) safeLSSetItem(PERSIST_CODE_KEY, this.code)

View File

@ -1,6 +1,6 @@
import { import {
Program, Program,
_executor, executor,
ProgramMemory, ProgramMemory,
kclLint, kclLint,
emptyExecState, emptyExecState,
@ -64,10 +64,9 @@ export async function executeAst({
try { try {
const execState = await (programMemoryOverride const execState = await (programMemoryOverride
? enginelessExecutor(ast, programMemoryOverride) ? enginelessExecutor(ast, programMemoryOverride)
: _executor(ast, engineCommandManager)) : executor(ast, engineCommandManager))
await engineCommandManager.waitForAllCommands() await engineCommandManager.waitForAllCommands()
return { return {
logs: [], logs: [],
errors: [], errors: [],

View File

@ -16,6 +16,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, getNodePathFromSourceRange } from './queryAst' import { findUsesOfTagInPipe, getNodePathFromSourceRange } from './queryAst'
@ -918,3 +919,63 @@ sketch002 = startSketchOn({
} }
) )
}) })
describe('Testing splitPipedProfile', () => {
it('should split the pipe expression correctly', () => {
const codeBefore = `part001 = startSketchOn('XZ')
|> startProfileAt([1, 2], %)
|> line([3, 4], %)
|> line([5, 6], %)
|> close(%)
extrude001 = extrude(5, part001)
`
const expectedCodeAfter = `sketch001 = startSketchOn('XZ')
part001 = startProfileAt([1, 2], sketch001)
|> line([3, 4], %)
|> line([5, 6], %)
|> close(%)
extrude001 = extrude(5, part001)
`
const ast = assertParse(codeBefore)
const codeOfInterest = `startSketchOn('XZ')`
const range: [number, number, boolean] = [
codeBefore.indexOf(codeOfInterest),
codeBefore.indexOf(codeOfInterest) + codeOfInterest.length,
true,
]
const pathToPipe = getNodePathFromSourceRange(ast, range)
const result = splitPipedProfile(ast, pathToPipe)
if (err(result)) throw result
const newCode = recast(result.modifiedAst)
if (err(newCode)) throw newCode
expect(newCode.trim()).toBe(expectedCodeAfter.trim())
})
it('should return error for already split pipe', () => {
const codeBefore = `sketch001 = startSketchOn('XZ')
part001 = startProfileAt([1, 2], sketch001)
|> line([3, 4], %)
|> line([5, 6], %)
|> close(%)
extrude001 = extrude(5, part001)
`
const ast = assertParse(codeBefore)
const codeOfInterest = `startProfileAt([1, 2], sketch001)`
const range: [number, number, boolean] = [
codeBefore.indexOf(codeOfInterest),
codeBefore.indexOf(codeOfInterest) + codeOfInterest.length,
true,
]
const pathToPipe = getNodePathFromSourceRange(ast, range)
const result = splitPipedProfile(ast, pathToPipe)
expect(result instanceof Error).toBe(true)
})
})

View File

@ -29,6 +29,8 @@ import {
getNodePathFromSourceRange, getNodePathFromSourceRange,
isNodeSafeToReplace, isNodeSafeToReplace,
traverse, traverse,
getBodyIndex,
isCallExprWithName,
} from './queryAst' } from './queryAst'
import { addTagForSketchOnFace, getConstraintInfo } from './std/sketch' import { addTagForSketchOnFace, getConstraintInfo } from './std/sketch'
import { import {
@ -46,6 +48,7 @@ import { Models } from '@kittycad/lib'
import { ExtrudeFacePlane } from 'machines/modelingMachine' import { ExtrudeFacePlane } from 'machines/modelingMachine'
import { Node } from 'wasm-lib/kcl/bindings/Node' import { Node } from 'wasm-lib/kcl/bindings/Node'
import { KclExpressionWithVariable } from 'lib/commandTypes' import { KclExpressionWithVariable } from 'lib/commandTypes'
import { Artifact, getPathsFromArtifact } from './std/artifactGraph'
export function startSketchOnDefault( export function startSketchOnDefault(
node: Node<Program>, node: Node<Program>,
@ -78,41 +81,54 @@ export function startSketchOnDefault(
} }
} }
export function addStartProfileAt( export function insertNewStartProfileAt(
node: Node<Program>, node: Node<Program>,
pathToNode: PathToNode, sketchEntryNodePath: PathToNode,
at: [number, number] sketchNodePaths: PathToNode[],
): { modifiedAst: Node<Program>; pathToNode: PathToNode } | Error { planeNodePath: PathToNode,
const _node1 = getNodeFromPath<VariableDeclaration>( at: [number, number],
node, insertType: 'start' | 'end' = 'end'
pathToNode, ):
'VariableDeclaration' | {
) modifiedAst: Node<Program>
if (err(_node1)) return _node1 updatedSketchNodePaths: PathToNode[]
const variableDeclaration = _node1.node updatedEntryNodePath: PathToNode
if (variableDeclaration.type !== 'VariableDeclaration') {
return new Error('variableDeclaration.init.type !== PipeExpression')
} }
const _node = { ...node } | Error {
const init = variableDeclaration.declaration.init const varDec = getNodeFromPath<VariableDeclarator>(
const startProfileAt = createCallExpressionStdLib('startProfileAt', [ node,
planeNodePath,
'VariableDeclarator'
)
if (err(varDec)) return varDec
if (varDec.node.type !== 'VariableDeclarator') return new Error('not a var')
const newExpression = createVariableDeclaration(
findUniqueName(node, 'profile'),
createCallExpressionStdLib('startProfileAt', [
createArrayExpression([ createArrayExpression([
createLiteral(roundOff(at[0])), createLiteral(roundOff(at[0])),
createLiteral(roundOff(at[1])), createLiteral(roundOff(at[1])),
]), ]),
createPipeSubstitution(), createIdentifier(varDec.node.id.name),
]) ])
if (init.type === 'PipeExpression') { )
init.body.splice(1, 0, startProfileAt) const insertIndex = getInsertIndex(sketchNodePaths, planeNodePath, insertType)
} else {
variableDeclaration.declaration.init = createPipeExpression([ const _node = structuredClone(node)
init, // TODO the rest of this function will not be robust to work for sketches defined within a function declaration
startProfileAt, _node.body.splice(insertIndex, 0, newExpression)
])
} const { updatedEntryNodePath, updatedSketchNodePaths } =
updateSketchNodePathsWithInsertIndex({
insertIndex,
insertType,
sketchNodePaths,
})
return { return {
modifiedAst: _node, modifiedAst: _node,
pathToNode, updatedSketchNodePaths,
updatedEntryNodePath,
} }
} }
@ -253,7 +269,7 @@ export function mutateObjExpProp(
export function extrudeSketch( export function extrudeSketch(
node: Node<Program>, node: Node<Program>,
pathToNode: PathToNode, pathToNode: PathToNode,
shouldPipe = false, artifact?: Artifact,
distance: Expr = createLiteral(4) distance: Expr = createLiteral(4)
): ):
| { | {
@ -262,10 +278,14 @@ export function extrudeSketch(
pathToExtrudeArg: PathToNode pathToExtrudeArg: PathToNode
} }
| Error { | Error {
const orderedSketchNodePaths = getPathsFromArtifact({
artifact: artifact,
sketchPathToNode: pathToNode,
})
if (err(orderedSketchNodePaths)) return orderedSketchNodePaths
const _node = structuredClone(node) const _node = structuredClone(node)
const _node1 = getNodeFromPath(_node, pathToNode) const _node1 = getNodeFromPath(_node, pathToNode)
if (err(_node1)) return _node1 if (err(_node1)) return _node1
const { node: sketchExpression } = _node1
// determine if sketchExpression is in a pipeExpression or not // determine if sketchExpression is in a pipeExpression or not
const _node2 = getNodeFromPath<PipeExpression>( const _node2 = getNodeFromPath<PipeExpression>(
@ -274,9 +294,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,
@ -284,49 +301,23 @@ 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 extrudeCall = createCallExpressionStdLib('extrude', [ const extrudeCall = createCallExpressionStdLib('extrude', [
distance, distance,
shouldPipe createIdentifier(variableDeclarator.id.name),
? createPipeSubstitution()
: createIdentifier(variableDeclarator.id.name),
]) ])
if (shouldPipe) {
const pipeChain = createPipeExpression(
isInPipeExpression
? [...pipeExpression.body, extrudeCall]
: [sketchExpression as any, extrudeCall]
)
variableDeclarator.init = pipeChain
const pathToExtrudeArg: PathToNode = [
...pathToDecleration,
['init', 'VariableDeclarator'],
['body', ''],
[pipeChain.body.length - 1, 'index'],
['arguments', 'CallExpression'],
[0, 'index'],
]
return {
modifiedAst: _node,
pathToNode,
pathToExtrudeArg,
}
}
// We're not creating a pipe expression, // 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(node, KCL_DEFAULT_CONSTANT_PREFIXES.EXTRUDE) const name = 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 console.log('lastSketchNodePath', lastSketchNodePath, orderedSketchNodePaths)
][0] as number const sketchIndexInBody = Number(lastSketchNodePath[1][0])
_node.body.splice(sketchIndexInBody + 1, 0, VariableDeclaration) _node.body.splice(sketchIndexInBody + 1, 0, VariableDeclaration)
const pathToExtrudeArg: PathToNode = [ const pathToExtrudeArg: PathToNode = [
@ -1307,7 +1298,8 @@ export async function deleteFromSelection(
const pipeBody = varDec.node.init.body const pipeBody = varDec.node.init.body
if ( if (
pipeBody[0].type === 'CallExpression' && pipeBody[0].type === 'CallExpression' &&
pipeBody[0].callee.name === 'startSketchOn' (pipeBody[0].callee.name === 'startSketchOn' ||
pipeBody[0].callee.name === 'startProfileAt')
) { ) {
// remove varDec // remove varDec
const varDecIndex = varDec.shallowPath[1][0] as number const varDecIndex = varDec.shallowPath[1][0] as number
@ -1322,3 +1314,149 @@ export async function deleteFromSelection(
const nonCodeMetaEmpty = () => { const nonCodeMetaEmpty = () => {
return { nonCodeNodes: {}, startNodes: [], start: 0, end: 0 } return { nonCodeNodes: {}, startNodes: [], start: 0, end: 0 }
} }
export function getInsertIndex(
sketchNodePaths: PathToNode[],
planeNodePath: PathToNode,
insertType: 'start' | 'end'
) {
let minIndex = 0
let maxIndex = 0
for (const path of sketchNodePaths) {
const index = Number(path[1][0])
if (index < minIndex) minIndex = index
if (index > maxIndex) maxIndex = index
}
const insertIndex = !sketchNodePaths.length
? Number(planeNodePath[1][0]) + 1
: insertType === 'start'
? minIndex
: maxIndex + 1
return insertIndex
}
export function updateSketchNodePathsWithInsertIndex({
insertIndex,
insertType,
sketchNodePaths,
}: {
insertIndex: number
insertType: 'start' | 'end'
sketchNodePaths: PathToNode[]
}): {
updatedEntryNodePath: PathToNode
updatedSketchNodePaths: PathToNode[]
} {
// TODO the rest of this function will not be robust to work for sketches defined within a function declaration
const newExpressionPathToNode: PathToNode = [
['body', ''],
[insertIndex, 'index'],
['declaration', 'VariableDeclaration'],
['init', 'VariableDeclarator'],
]
let updatedSketchNodePaths = structuredClone(sketchNodePaths)
if (insertType === 'start') {
updatedSketchNodePaths = updatedSketchNodePaths.map((path) => {
path[1][0] = Number(path[1][0]) + 1
return path
})
updatedSketchNodePaths.unshift(newExpressionPathToNode)
} else {
updatedSketchNodePaths.push(newExpressionPathToNode)
}
return {
updatedSketchNodePaths,
updatedEntryNodePath: newExpressionPathToNode,
}
}
/**
*
* Split the following pipe expression into
* ```ts
* part001 = startSketchOn('XZ')
|> startProfileAt([1, 2], %)
|> line([3, 4], %)
|> line([5, 6], %)
|> close(%)
extrude001 = extrude(5, part001)
```
into
```ts
sketch001 = startSketchOn('XZ')
part001 = startProfileAt([1, 2], sketch001)
|> line([3, 4], %)
|> line([5, 6], %)
|> close(%)
extrude001 = extrude(5, part001)
```
Notice that the `startSketchOn` is what gets the new variable name, this is so part001 still has the same data as before
making it safe for later code that uses part001 (the extrude in this example)
*
*/
export function splitPipedProfile(
ast: Program,
pathToPipe: PathToNode
):
| {
modifiedAst: Program
pathToProfile: PathToNode
pathToPlane: PathToNode
}
| Error {
const _ast = structuredClone(ast)
const varDec = getNodeFromPath<VariableDeclaration>(
_ast,
pathToPipe,
'VariableDeclaration'
)
if (err(varDec)) return varDec
if (
varDec.node.type !== 'VariableDeclaration' ||
varDec.node.declaration.init.type !== 'PipeExpression'
) {
return new Error('pathToNode does not point to pipe')
}
const init = varDec.node.declaration.init
const firstCall = init.body[0]
if (!isCallExprWithName(firstCall, 'startSketchOn'))
return new Error('First call is not startSketchOn')
const secondCall = init.body[1]
if (!isCallExprWithName(secondCall, 'startProfileAt'))
return new Error('Second call is not startProfileAt')
const varName = varDec.node.declaration.id.name
const newVarName = findUniqueName(_ast, 'sketch')
const secondCallArgs = structuredClone(secondCall.arguments)
secondCallArgs[1] = createIdentifier(newVarName)
const firstCallOfNewPipe = createCallExpression(
'startProfileAt',
secondCallArgs
)
const newSketch = createVariableDeclaration(
newVarName,
varDec.node.declaration.init.body[0]
)
const newProfile = createVariableDeclaration(
varName,
varDec.node.declaration.init.body.length <= 2
? firstCallOfNewPipe
: createPipeExpression([
firstCallOfNewPipe,
...varDec.node.declaration.init.body.slice(2),
])
)
const index = getBodyIndex(pathToPipe)
if (err(index)) return index
_ast.body.splice(index, 1, newSketch, newProfile)
const pathToPlane = structuredClone(pathToPipe)
const pathToProfile = structuredClone(pathToPipe)
pathToProfile[1][0] = index + 1
return {
modifiedAst: _ast,
pathToProfile,
pathToPlane,
}
}

View File

@ -275,7 +275,7 @@ const runModifyAstCloneWithEdgeTreatmentAndTag = async (
const selection: Selections = { const selection: Selections = {
graphSelections: segmentRanges.map((segmentRange) => { graphSelections: segmentRanges.map((segmentRange) => {
const maybeArtifact = [...artifactGraph].find(([, a]) => { const maybeArtifact = [...artifactGraph].find(([, a]) => {
if (!('codeRef' in a)) return false if (!('codeRef' in a && a.codeRef)) return false
return isOverlap(a.codeRef.range, segmentRange) return isOverlap(a.codeRef.range, segmentRange)
}) })
return { return {

View File

@ -5,7 +5,6 @@ import {
PathToNode, PathToNode,
Expr, Expr,
CallExpression, CallExpression,
PipeExpression,
VariableDeclarator, VariableDeclarator,
} from 'lang/wasm' } from 'lang/wasm'
import { Selections } from 'lib/selections' import { Selections } from 'lib/selections'
@ -15,7 +14,6 @@ import {
createCallExpressionStdLib, createCallExpressionStdLib,
createObjectExpression, createObjectExpression,
createIdentifier, createIdentifier,
createPipeExpression,
findUniqueName, findUniqueName,
createVariableDeclaration, createVariableDeclaration,
} from 'lang/modifyAst' } from 'lang/modifyAst'
@ -24,12 +22,13 @@ import {
mutateAstWithTagForSketchSegment, mutateAstWithTagForSketchSegment,
getEdgeTagCall, getEdgeTagCall,
} from 'lang/modifyAst/addEdgeTreatment' } from 'lang/modifyAst/addEdgeTreatment'
import { Artifact, getPathsFromArtifact } from 'lang/std/artifactGraph'
export function revolveSketch( export function revolveSketch(
ast: Node<Program>, ast: Node<Program>,
pathToSketchNode: PathToNode, pathToSketchNode: PathToNode,
shouldPipe = false,
angle: Expr = createLiteral(4), angle: Expr = createLiteral(4),
axis: Selections axis: Selections,
artifact?: Artifact
): ):
| { | {
modifiedAst: Node<Program> modifiedAst: Node<Program>
@ -37,6 +36,11 @@ export function revolveSketch(
pathToRevolveArg: PathToNode pathToRevolveArg: PathToNode
} }
| Error { | Error {
const orderedSketchNodePaths = getPathsFromArtifact({
artifact: artifact,
sketchPathToNode: pathToSketchNode,
})
if (err(orderedSketchNodePaths)) return orderedSketchNodePaths
const clonedAst = structuredClone(ast) const clonedAst = structuredClone(ast)
const sketchNode = getNodeFromPath(clonedAst, pathToSketchNode) const sketchNode = getNodeFromPath(clonedAst, pathToSketchNode)
if (err(sketchNode)) return sketchNode if (err(sketchNode)) return sketchNode
@ -67,29 +71,13 @@ export function revolveSketch(
if (err(tagResult)) return tagResult if (err(tagResult)) return tagResult
const { tag } = tagResult const { tag } = tagResult
/* 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
const axisSelection = axis?.graphSelections[0]?.artifact const axisSelection = axis?.graphSelections[0]?.artifact
@ -103,37 +91,13 @@ export function revolveSketch(
createIdentifier(sketchVariableDeclarator.id.name), createIdentifier(sketchVariableDeclarator.id.name),
]) ])
if (shouldPipe) {
const pipeChain = createPipeExpression(
isInPipeExpression
? [...sketchPipeExpression.body, revolveCall]
: [sketchExpression as any, revolveCall]
)
sketchVariableDeclarator.init = pipeChain
const pathToRevolveArg: PathToNode = [
...sketchPathToDecleration,
['init', 'VariableDeclarator'],
['body', ''],
[pipeChain.body.length - 1, 'index'],
['arguments', 'CallExpression'],
[0, 'index'],
]
return {
modifiedAst: clonedAst,
pathToSketchNode,
pathToRevolveArg,
}
}
// We're not creating a pipe expression, // We're not creating a pipe expression,
// but rather a separate constant for the extrusion // but rather a separate constant for the extrusion
const name = findUniqueName(clonedAst, KCL_DEFAULT_CONSTANT_PREFIXES.REVOLVE) const name = findUniqueName(clonedAst, KCL_DEFAULT_CONSTANT_PREFIXES.REVOLVE)
const VariableDeclaration = createVariableDeclaration(name, revolveCall) const VariableDeclaration = createVariableDeclaration(name, revolveCall)
const sketchIndexInPathToNode = const lastSketchNodePath =
sketchPathToDecleration.findIndex((a) => a[0] === 'body') + 1 orderedSketchNodePaths[orderedSketchNodePaths.length - 1]
const sketchIndexInBody = sketchPathToDecleration[sketchIndexInPathToNode][0] const sketchIndexInBody = Number(lastSketchNodePath[1][0])
if (typeof sketchIndexInBody !== 'number') if (typeof sketchIndexInBody !== 'number')
return new Error('expected sketchIndexInBody to be a number') return new Error('expected sketchIndexInBody to be a number')
clonedAst.body.splice(sketchIndexInBody + 1, 0, VariableDeclaration) clonedAst.body.splice(sketchIndexInBody + 1, 0, VariableDeclaration)

View File

@ -33,6 +33,7 @@ import { err, Reason } from 'lib/trap'
import { ImportStatement } from 'wasm-lib/kcl/bindings/ImportStatement' 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 { ArtifactGraph, codeRefFromRange } from './std/artifactGraph' import { ArtifactGraph, codeRefFromRange } from './std/artifactGraph'
import { FunctionExpression } from 'wasm-lib/kcl/bindings/FunctionExpression'
/** /**
* Retrieves a node from a given path within a Program node structure, optionally stopping at a specified node type. * Retrieves a node from a given path within a Program node structure, optionally stopping at a specified node type.
@ -597,7 +598,13 @@ export function findAllPreviousVariables(
type ReplacerFn = ( 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,
@ -649,7 +656,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')
@ -768,8 +775,15 @@ export function isLinesParallelAndConstrained(
if (err(_primarySegment)) return _primarySegment if (err(_primarySegment)) return _primarySegment
const primarySegment = _primarySegment.segment const primarySegment = _primarySegment.segment
const _varDec2 = getNodeFromPath(ast, secondaryPath, 'VariableDeclaration')
if (err(_varDec2)) return _varDec2
const varDec2 = _varDec2.node
const varName2 = (varDec2 as VariableDeclaration)?.declaration.id?.name
const sg2 = sketchFromKclValue(programMemory?.get(varName2), varName2)
if (err(sg2)) return sg2
const _segment = getSketchSegmentFromSourceRange( const _segment = getSketchSegmentFromSourceRange(
sg, sg2,
secondaryLine?.codeRef?.range secondaryLine?.codeRef?.range
) )
if (err(_segment)) return _segment if (err(_segment)) return _segment
@ -1083,3 +1097,57 @@ export function getObjExprProperty(
if (index === -1) return null if (index === -1) return null
return { expr: node.properties[index].value, index } return { expr: node.properties[index].value, index }
} }
export function isCursorInFunctionDefinition(
ast: Node<Program>,
selectionRanges: Selection
): boolean {
if (!selectionRanges?.codeRef?.pathToNode) return false
const node = getNodeFromPath<FunctionExpression>(
ast,
selectionRanges.codeRef.pathToNode,
'FunctionExpression'
)
if (err(node)) return false
if (node.node.type === 'FunctionExpression') return true
return false
}
export function getBodyIndex(pathToNode: PathToNode): number | Error {
const index = Number(pathToNode[1][0])
if (Number.isInteger(index)) return index
return new Error('Expected number index')
}
export function isCallExprWithName(
expr: Expr | CallExpression,
name: string
): expr is CallExpression {
if (expr.type === 'CallExpression' && expr.callee.type === 'Identifier') {
return expr.callee.name === name
}
return false
}
export function doesSketchPipeNeedSplitting(
ast: Node<Program>,
pathToPipe: PathToNode
): boolean | Error {
const varDec = getNodeFromPath<VariableDeclarator>(
ast,
pathToPipe,
'VariableDeclarator'
)
if (err(varDec)) return varDec
if (varDec.node.type !== 'VariableDeclarator') return new Error('Not a var')
const pipeExpression = varDec.node.init
if (pipeExpression.type !== 'PipeExpression') return false
const [firstPipe, secondPipe] = pipeExpression.body
if (!firstPipe || !secondPipe) return false
if (
isCallExprWithName(firstPipe, 'startSketchOn') &&
isCallExprWithName(secondPipe, 'startProfileAt')
)
return true
return false
}

View File

@ -212,6 +212,19 @@ Map {
"type": "wall", "type": "wall",
}, },
"UUID-10" => { "UUID-10" => {
"codeRef": {
"pathToNode": [
[
"body",
"",
],
],
"range": [
312,
344,
true,
],
},
"edgeCutEdgeIds": [], "edgeCutEdgeIds": [],
"id": "UUID", "id": "UUID",
"pathIds": [ "pathIds": [

View File

@ -1,4 +1,10 @@
import { makeDefaultPlanes, assertParse, initPromise, Program } from 'lang/wasm' import {
makeDefaultPlanes,
assertParse,
initPromise,
Program,
ExecState,
} from 'lang/wasm'
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import { import {
OrderedCommand, OrderedCommand,
@ -22,6 +28,7 @@ import * as d3 from 'd3-force'
import path from 'path' import path from 'path'
import pixelmatch from 'pixelmatch' import pixelmatch from 'pixelmatch'
import { PNG } from 'pngjs' import { PNG } from 'pngjs'
import { Node } from 'wasm-lib/kcl/bindings/Node'
/* /*
Note this is an integration test, these tests connect to our real dev server and make websocket commands. Note this is an integration test, these tests connect to our real dev server and make websocket commands.
@ -122,6 +129,7 @@ type CacheShape = {
[key in CodeKey]: { [key in CodeKey]: {
orderedCommands: OrderedCommand[] orderedCommands: OrderedCommand[]
responseMap: ResponseMap responseMap: ResponseMap
execStateArtifacts: ExecState['artifacts']
} }
} }
@ -153,6 +161,7 @@ beforeAll(async () => {
cacheToWriteToFileTemp[codeKey] = { cacheToWriteToFileTemp[codeKey] = {
orderedCommands: engineCommandManager.orderedCommands, orderedCommands: engineCommandManager.orderedCommands,
responseMap: engineCommandManager.responseMap, responseMap: engineCommandManager.responseMap,
execStateArtifacts: kclManager.execState.artifacts,
} }
} }
const cache = JSON.stringify(cacheToWriteToFileTemp) const cache = JSON.stringify(cacheToWriteToFileTemp)
@ -171,7 +180,7 @@ afterAll(() => {
describe('testing createArtifactGraph', () => { describe('testing createArtifactGraph', () => {
describe('code with offset planes and a sketch:', () => { describe('code with offset planes and a sketch:', () => {
let ast: Program let ast: Node<Program>
let theMap: ReturnType<typeof createArtifactGraph> let theMap: ReturnType<typeof createArtifactGraph>
it('setup', () => { it('setup', () => {
@ -180,9 +189,15 @@ describe('testing createArtifactGraph', () => {
orderedCommands, orderedCommands,
responseMap, responseMap,
ast: _ast, ast: _ast,
execStateArtifacts,
} = getCommands('exampleCodeOffsetPlanes') } = getCommands('exampleCodeOffsetPlanes')
ast = _ast ast = _ast
theMap = createArtifactGraph({ orderedCommands, responseMap, ast }) theMap = createArtifactGraph({
orderedCommands,
responseMap,
ast,
execStateArtifacts,
})
}) })
it(`there should be one sketch`, () => { it(`there should be one sketch`, () => {
@ -217,7 +232,7 @@ describe('testing createArtifactGraph', () => {
}) })
}) })
describe('code with an extrusion, fillet and sketch of face:', () => { describe('code with an extrusion, fillet and sketch of face:', () => {
let ast: Program let ast: Node<Program>
let theMap: ReturnType<typeof createArtifactGraph> let theMap: ReturnType<typeof createArtifactGraph>
it('setup', () => { it('setup', () => {
// putting this logic in here because describe blocks runs before beforeAll has finished // putting this logic in here because describe blocks runs before beforeAll has finished
@ -225,9 +240,15 @@ describe('testing createArtifactGraph', () => {
orderedCommands, orderedCommands,
responseMap, responseMap,
ast: _ast, ast: _ast,
execStateArtifacts,
} = getCommands('exampleCode1') } = getCommands('exampleCode1')
ast = _ast ast = _ast
theMap = createArtifactGraph({ orderedCommands, responseMap, ast }) theMap = createArtifactGraph({
orderedCommands,
responseMap,
ast,
execStateArtifacts,
})
}) })
it('there should be two planes for the extrusion and the sketch on face', () => { it('there should be two planes for the extrusion and the sketch on face', () => {
@ -312,7 +333,7 @@ describe('testing createArtifactGraph', () => {
}) })
describe(`code with sketches but no extrusions or other 3D elements`, () => { describe(`code with sketches but no extrusions or other 3D elements`, () => {
let ast: Program let ast: Node<Program>
let theMap: ReturnType<typeof createArtifactGraph> let theMap: ReturnType<typeof createArtifactGraph>
it(`setup`, () => { it(`setup`, () => {
// putting this logic in here because describe blocks runs before beforeAll has finished // putting this logic in here because describe blocks runs before beforeAll has finished
@ -320,9 +341,15 @@ describe('testing createArtifactGraph', () => {
orderedCommands, orderedCommands,
responseMap, responseMap,
ast: _ast, ast: _ast,
execStateArtifacts,
} = getCommands('exampleCodeNo3D') } = getCommands('exampleCodeNo3D')
ast = _ast ast = _ast
theMap = createArtifactGraph({ orderedCommands, responseMap, ast }) theMap = createArtifactGraph({
orderedCommands,
responseMap,
ast,
execStateArtifacts,
})
}) })
it('there should be two planes, one for each sketch path', () => { it('there should be two planes, one for each sketch path', () => {
@ -377,7 +404,7 @@ describe('testing createArtifactGraph', () => {
describe('capture graph of sketchOnFaceOnFace...', () => { describe('capture graph of sketchOnFaceOnFace...', () => {
describe('code with an extrusion, fillet and sketch of face:', () => { describe('code with an extrusion, fillet and sketch of face:', () => {
let ast: Program let ast: Node<Program>
let theMap: ReturnType<typeof createArtifactGraph> let theMap: ReturnType<typeof createArtifactGraph>
it('setup', async () => { it('setup', async () => {
// putting this logic in here because describe blocks runs before beforeAll has finished // putting this logic in here because describe blocks runs before beforeAll has finished
@ -385,9 +412,15 @@ describe('capture graph of sketchOnFaceOnFace...', () => {
orderedCommands, orderedCommands,
responseMap, responseMap,
ast: _ast, ast: _ast,
execStateArtifacts,
} = getCommands('sketchOnFaceOnFaceEtc') } = getCommands('sketchOnFaceOnFaceEtc')
ast = _ast ast = _ast
theMap = createArtifactGraph({ orderedCommands, responseMap, ast }) theMap = createArtifactGraph({
orderedCommands,
responseMap,
ast,
execStateArtifacts,
})
// Ostensibly this takes a screen shot of the graph of the artifactGraph // Ostensibly this takes a screen shot of the graph of the artifactGraph
// but it's it also tests that all of the id links are correct because if one // but it's it also tests that all of the id links are correct because if one
@ -399,17 +432,21 @@ describe('capture graph of sketchOnFaceOnFace...', () => {
}) })
}) })
function getCommands(codeKey: CodeKey): CacheShape[CodeKey] & { ast: Program } { function getCommands(
codeKey: CodeKey
): CacheShape[CodeKey] & { ast: Node<Program> } {
const ast = assertParse(codeKey) const ast = assertParse(codeKey)
const file = fs.readFileSync(fullPath, 'utf-8') const file = fs.readFileSync(fullPath, 'utf-8')
const parsed: CacheShape = JSON.parse(file) const parsed: CacheShape = JSON.parse(file)
// these either already exist from the last run, or were created in // these either already exist from the last run, or were created in
const orderedCommands = parsed[codeKey].orderedCommands const orderedCommands = parsed[codeKey].orderedCommands
const responseMap = parsed[codeKey].responseMap const responseMap = parsed[codeKey].responseMap
const execStateArtifacts = parsed[codeKey].execStateArtifacts
return { return {
orderedCommands, orderedCommands,
responseMap, responseMap,
ast, ast,
execStateArtifacts,
} }
} }
@ -635,8 +672,14 @@ async function GraphTheGraph(
describe('testing getArtifactsToUpdate', () => { describe('testing getArtifactsToUpdate', () => {
it('should return an array of artifacts to update', () => { it('should return an array of artifacts to update', () => {
const { orderedCommands, responseMap, ast } = getCommands('exampleCode1') const { orderedCommands, responseMap, ast, execStateArtifacts } =
const map = createArtifactGraph({ orderedCommands, responseMap, ast }) getCommands('exampleCode1')
const map = createArtifactGraph({
orderedCommands,
responseMap,
ast,
execStateArtifacts,
})
const getArtifact = (id: string) => map.get(id) const getArtifact = (id: string) => map.get(id)
const currentPlaneId = 'UUID-1' const currentPlaneId = 'UUID-1'
const getUpdateObjects = (type: Models['ModelingCmd_type']['type']) => { const getUpdateObjects = (type: Models['ModelingCmd_type']['type']) => {
@ -649,6 +692,7 @@ describe('testing getArtifactsToUpdate', () => {
getArtifact, getArtifact,
currentPlaneId, currentPlaneId,
ast, ast,
execStateArtifacts,
}) })
return artifactsToUpdate.map(({ artifact }) => artifact) return artifactsToUpdate.map(({ artifact }) => artifact)
} }
@ -776,6 +820,10 @@ describe('testing getArtifactsToUpdate', () => {
}, },
{ {
type: 'wall', type: 'wall',
codeRef: {
pathToNode: [['body', '']],
range: [312, 344, true],
},
id: expect.any(String), id: expect.any(String),
segId: expect.any(String), segId: expect.any(String),
edgeCutEdgeIds: [], edgeCutEdgeIds: [],

View File

@ -1,7 +1,9 @@
import { PathToNode, Program, SourceRange } from 'lang/wasm' import { ExecState, Expr, PathToNode, Program, SourceRange } from 'lang/wasm'
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import { getNodePathFromSourceRange } from 'lang/queryAst' import { getNodePathFromSourceRange } from 'lang/queryAst'
import { err } from 'lib/trap' import { err } from 'lib/trap'
import { engineCommandManager, kclManager } from 'lib/singletons'
import { Node } from 'wasm-lib/kcl/bindings/Node'
export type ArtifactId = string export type ArtifactId = string
@ -34,14 +36,14 @@ export interface PathArtifact extends BaseArtifact {
codeRef: CodeRef codeRef: CodeRef
} }
interface solid2D extends BaseArtifact { interface Solid2DArtifact extends BaseArtifact {
type: 'solid2D' type: 'solid2D'
pathId: ArtifactId pathId: ArtifactId
} }
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 */
@ -61,7 +63,7 @@ interface SegmentArtifactRich extends BaseArtifact {
type: 'segment' type: 'segment'
path: PathArtifact path: PathArtifact
surf: WallArtifact surf: WallArtifact
edges: Array<SweepEdge> edges: Array<SweepEdgeArtifact>
edgeCut?: EdgeCut edgeCut?: EdgeCut
codeRef: CodeRef codeRef: CodeRef
} }
@ -80,7 +82,7 @@ interface SweepArtifactRich extends BaseArtifact {
subType: 'extrusion' | 'revolve' subType: 'extrusion' | 'revolve'
path: PathArtifact path: PathArtifact
surfaces: Array<WallArtifact | CapArtifact> surfaces: Array<WallArtifact | CapArtifact>
edges: Array<SweepEdge> edges: Array<SweepEdgeArtifact>
codeRef: CodeRef codeRef: CodeRef
} }
@ -90,6 +92,9 @@ interface WallArtifact extends BaseArtifact {
edgeCutEdgeIds: Array<ArtifactId> edgeCutEdgeIds: Array<ArtifactId>
sweepId: ArtifactId sweepId: ArtifactId
pathIds: Array<ArtifactId> pathIds: Array<ArtifactId>
// codeRef is for the sketchOnFace plane, not for the wall itself
// traverse to the extrude and or segment to get the wall's codeRef
codeRef?: CodeRef
} }
interface CapArtifact extends BaseArtifact { interface CapArtifact extends BaseArtifact {
type: 'cap' type: 'cap'
@ -97,9 +102,12 @@ interface CapArtifact extends BaseArtifact {
edgeCutEdgeIds: Array<ArtifactId> edgeCutEdgeIds: Array<ArtifactId>
sweepId: ArtifactId sweepId: ArtifactId
pathIds: Array<ArtifactId> pathIds: Array<ArtifactId>
// codeRef is for the sketchOnFace plane, not for the wall itself
// traverse to the extrude and or segment to get the wall's codeRef
codeRef?: CodeRef
} }
interface SweepEdge extends BaseArtifact { interface SweepEdgeArtifact extends BaseArtifact {
type: 'sweepEdge' type: 'sweepEdge'
segId: ArtifactId segId: ArtifactId
sweepId: ArtifactId sweepId: ArtifactId
@ -129,10 +137,10 @@ export type Artifact =
| SweepArtifact | SweepArtifact
| WallArtifact | WallArtifact
| CapArtifact | CapArtifact
| SweepEdge | SweepEdgeArtifact
| EdgeCut | EdgeCut
| EdgeCutEdge | EdgeCutEdge
| solid2D | Solid2DArtifact
export type ArtifactGraph = Map<ArtifactId, Artifact> export type ArtifactGraph = Map<ArtifactId, Artifact>
@ -156,10 +164,12 @@ export function createArtifactGraph({
orderedCommands, orderedCommands,
responseMap, responseMap,
ast, ast,
execStateArtifacts,
}: { }: {
orderedCommands: Array<OrderedCommand> orderedCommands: Array<OrderedCommand>
responseMap: ResponseMap responseMap: ResponseMap
ast: Program ast: Node<Program>
execStateArtifacts: ExecState['artifacts']
}) { }) {
const myMap = new Map<ArtifactId, Artifact>() const myMap = new Map<ArtifactId, Artifact>()
@ -181,6 +191,7 @@ export function createArtifactGraph({
getArtifact: (id: ArtifactId) => myMap.get(id), getArtifact: (id: ArtifactId) => myMap.get(id),
currentPlaneId, currentPlaneId,
ast, ast,
execStateArtifacts,
}) })
artifactsToUpdate.forEach(({ id, artifact }) => { artifactsToUpdate.forEach(({ id, artifact }) => {
const mergedArtifact = mergeArtifacts(myMap.get(id), artifact) const mergedArtifact = mergeArtifacts(myMap.get(id), artifact)
@ -232,13 +243,15 @@ export function getArtifactsToUpdate({
responseMap, responseMap,
currentPlaneId, currentPlaneId,
ast, ast,
execStateArtifacts,
}: { }: {
orderedCommand: OrderedCommand orderedCommand: OrderedCommand
responseMap: ResponseMap responseMap: ResponseMap
/** Passing in a getter because we don't wan this function to update the map directly */ /** Passing in a getter because we don't wan this function to update the map directly */
getArtifact: (id: ArtifactId) => Artifact | undefined getArtifact: (id: ArtifactId) => Artifact | undefined
currentPlaneId: ArtifactId currentPlaneId: ArtifactId
ast: Program ast: Node<Program>
execStateArtifacts: ExecState['artifacts']
}): Array<{ }): Array<{
id: ArtifactId id: ArtifactId
artifact: Artifact artifact: Artifact
@ -284,6 +297,22 @@ export function getArtifactsToUpdate({
edgeCutEdgeIds: existingPlane.edgeCutEdgeIds, edgeCutEdgeIds: existingPlane.edgeCutEdgeIds,
sweepId: existingPlane.sweepId, sweepId: existingPlane.sweepId,
pathIds: existingPlane.pathIds, pathIds: existingPlane.pathIds,
codeRef: existingPlane.codeRef,
},
},
]
} else if (existingPlane?.type === 'cap') {
return [
{
id: currentPlaneId,
artifact: {
type: 'cap',
subType: existingPlane.subType,
id: currentPlaneId,
edgeCutEdgeIds: existingPlane.edgeCutEdgeIds,
sweepId: existingPlane.sweepId,
pathIds: existingPlane.pathIds,
codeRef: existingPlane.codeRef,
}, },
}, },
] ]
@ -328,6 +357,18 @@ export function getArtifactsToUpdate({
pathIds: [id], pathIds: [id],
}, },
}) })
} else if (plane?.type === 'cap') {
returnArr.push({
id: currentPlaneId,
artifact: {
type: 'cap',
id: currentPlaneId,
subType: plane.subType,
edgeCutEdgeIds: plane.edgeCutEdgeIds,
sweepId: plane.sweepId,
pathIds: [id],
},
})
} }
return returnArr return returnArr
} else if (cmd.type === 'extend_path' || cmd.type === 'close_path') { } else if (cmd.type === 'extend_path' || cmd.type === 'close_path') {
@ -407,16 +448,33 @@ export function getArtifactsToUpdate({
const path = getArtifact(seg.pathId) const path = getArtifact(seg.pathId)
if (path?.type === 'path' && seg?.type === 'segment') { if (path?.type === 'path' && seg?.type === 'segment') {
lastPath = path lastPath = path
returnArr.push({ const extraArtifact = Object.values(execStateArtifacts).find(
id: face_id, (a) => a?.type === 'StartSketchOnFace' && a.faceId === face_id
artifact: { )
const sketchOnFaceSourceRange = extraArtifact?.sourceRange
const wallArtifact: Artifact = {
type: 'wall', type: 'wall',
id: face_id, id: face_id,
segId: curve_id, segId: curve_id,
edgeCutEdgeIds: [], edgeCutEdgeIds: [],
sweepId: path.sweepId, sweepId: path.sweepId,
pathIds: [], pathIds: [],
}, }
if (sketchOnFaceSourceRange) {
const range: SourceRange = [
sketchOnFaceSourceRange[0],
sketchOnFaceSourceRange[1],
true,
]
wallArtifact.codeRef = {
range,
pathToNode: getNodePathFromSourceRange(ast, range),
}
}
returnArr.push({
id: face_id,
artifact: wallArtifact,
}) })
returnArr.push({ returnArr.push({
id: curve_id, id: curve_id,
@ -440,16 +498,33 @@ export function getArtifactsToUpdate({
if ((cap === 'top' || cap === 'bottom') && face_id) { if ((cap === 'top' || cap === 'bottom') && face_id) {
const path = lastPath const path = lastPath
if (path?.type === 'path') { if (path?.type === 'path') {
returnArr.push({ const extraArtifact = Object.values(execStateArtifacts).find(
id: face_id, (a) => a?.type === 'StartSketchOnFace' && a.faceId === face_id
artifact: { )
const sketchOnFaceSourceRange = extraArtifact?.sourceRange
const capArtifact: Artifact = {
type: 'cap', type: 'cap',
id: face_id, id: face_id,
subType: cap === 'bottom' ? 'start' : 'end', subType: cap === 'bottom' ? 'start' : 'end',
edgeCutEdgeIds: [], edgeCutEdgeIds: [],
sweepId: path.sweepId, sweepId: path.sweepId,
pathIds: [], pathIds: [],
}, }
if (sketchOnFaceSourceRange) {
const range: SourceRange = [
sketchOnFaceSourceRange[0],
sketchOnFaceSourceRange[1],
true,
]
capArtifact.codeRef = {
range,
pathToNode: getNodePathFromSourceRange(ast, range),
}
}
returnArr.push({
id: face_id,
artifact: capArtifact,
}) })
const sweep = getArtifact(path.sweepId) const sweep = getArtifact(path.sweepId)
if (sweep?.type !== 'sweep') return if (sweep?.type !== 'sweep') return
@ -733,7 +808,7 @@ export function getCapCodeRef(
} }
export function getSolid2dCodeRef( export function getSolid2dCodeRef(
solid2D: solid2D, solid2D: Solid2DArtifact,
artifactGraph: ArtifactGraph artifactGraph: ArtifactGraph
): CodeRef | Error { ): CodeRef | Error {
const path = getArtifactOfTypes( const path = getArtifactOfTypes(
@ -757,7 +832,7 @@ export function getWallCodeRef(
} }
export function getSweepEdgeCodeRef( export function getSweepEdgeCodeRef(
edge: SweepEdge, edge: SweepEdgeArtifact,
artifactGraph: ArtifactGraph artifactGraph: ArtifactGraph
): CodeRef | Error { ): CodeRef | Error {
const seg = getArtifactOfTypes( const seg = getArtifactOfTypes(
@ -872,6 +947,205 @@ export function codeRefFromRange(range: SourceRange, ast: Program): CodeRef {
} }
} }
function getPlaneFromPath(
path: PathArtifact,
graph: ArtifactGraph
): PlaneArtifact | WallArtifact | CapArtifact | Error {
const plane = getArtifactOfTypes(
{ key: path.planeId, types: ['plane', 'wall', 'cap'] },
graph
)
if (err(plane)) return plane
return plane
}
function getPlaneFromSegment(
segment: SegmentArtifact,
graph: ArtifactGraph
): PlaneArtifact | WallArtifact | CapArtifact | Error {
const path = getArtifactOfTypes(
{ key: segment.pathId, types: ['path'] },
graph
)
if (err(path)) return path
return getPlaneFromPath(path, graph)
}
function getPlaneFromSolid2D(
solid2D: Solid2DArtifact,
graph: ArtifactGraph
): PlaneArtifact | WallArtifact | CapArtifact | Error {
const path = getArtifactOfTypes(
{ key: solid2D.pathId, types: ['path'] },
graph
)
if (err(path)) return path
return getPlaneFromPath(path, graph)
}
function getPlaneFromCap(
cap: CapArtifact,
graph: ArtifactGraph
): PlaneArtifact | WallArtifact | CapArtifact | Error {
const sweep = getArtifactOfTypes(
{ key: cap.sweepId, types: ['sweep'] },
graph
)
if (err(sweep)) return sweep
const path = getArtifactOfTypes({ key: sweep.pathId, types: ['path'] }, graph)
if (err(path)) return path
return getPlaneFromPath(path, graph)
}
function getPlaneFromWall(
wall: WallArtifact,
graph: ArtifactGraph
): PlaneArtifact | WallArtifact | CapArtifact | Error {
const sweep = getArtifactOfTypes(
{ key: wall.sweepId, types: ['sweep'] },
graph
)
if (err(sweep)) return sweep
const path = getArtifactOfTypes({ key: sweep.pathId, types: ['path'] }, graph)
if (err(path)) return path
return getPlaneFromPath(path, graph)
}
function getPlaneFromSweepEdge(edge: SweepEdgeArtifact, graph: ArtifactGraph) {
const sweep = getArtifactOfTypes(
{ key: edge.sweepId, types: ['sweep'] },
graph
)
if (err(sweep)) return sweep
const path = getArtifactOfTypes({ key: sweep.pathId, types: ['path'] }, graph)
if (err(path)) return path
return getPlaneFromPath(path, graph)
}
export function getPlaneFromArtifact(
artifact: Artifact | undefined,
graph: ArtifactGraph
): PlaneArtifact | WallArtifact | CapArtifact | Error {
if (!artifact) return new Error(`Artifact is undefined`)
if (artifact.type === 'plane') return artifact
if (artifact.type === 'path') return getPlaneFromPath(artifact, graph)
if (artifact.type === 'segment') return getPlaneFromSegment(artifact, graph)
if (artifact.type === 'solid2D') return getPlaneFromSolid2D(artifact, graph)
if (artifact.type === 'cap') return getPlaneFromCap(artifact, graph)
if (artifact.type === 'wall') return getPlaneFromWall(artifact, graph)
if (artifact.type === 'sweepEdge')
return getPlaneFromSweepEdge(artifact, graph)
return new Error(`Artifact type ${artifact.type} does not have a plane`)
}
const isExprSafe = (index: number): boolean => {
const expr = kclManager.ast.body?.[index]
if (!expr) {
return false
}
if (expr.type === 'ImportStatement' || expr.type === 'ReturnStatement') {
return false
}
if (expr.type === 'VariableDeclaration') {
const init = expr.declaration?.init
if (!init) return false
if (init.type === 'CallExpression') {
return false
}
if (init.type === 'BinaryExpression' && isNodeSafe(init)) {
return true
}
if (init.type === 'Literal' || init.type === 'MemberExpression') {
return true
}
}
return false
}
const onlyConsecutivePaths = (
orderedNodePaths: PathToNode[],
originalPath: PathToNode
): PathToNode[] => {
const originalIndex = Number(
orderedNodePaths.find(
(path) => path[1][0] === originalPath[1][0]
)?.[1]?.[0] || 0
)
const minIndex = Number(orderedNodePaths[0][1][0])
const maxIndex = Number(orderedNodePaths[orderedNodePaths.length - 1][1][0])
const pathIndexMap: any = {}
orderedNodePaths.forEach((path) => {
const bodyIndex = Number(path[1][0])
pathIndexMap[bodyIndex] = path
})
const safePaths: PathToNode[] = []
// traverse expressions in either direction from the profile selected
// when the user entered sketch mode
for (let i = originalIndex; i <= maxIndex; i++) {
if (pathIndexMap[i]) {
safePaths.push(pathIndexMap[i])
} else if (!isExprSafe(i)) {
break
}
}
for (let i = originalIndex - 1; i >= minIndex; i--) {
if (pathIndexMap[i]) {
safePaths.unshift(pathIndexMap[i])
} else if (!isExprSafe(i)) {
break
}
}
return safePaths
}
export function getPathsFromPlaneArtifact(planeArtifact: PlaneArtifact) {
const nodePaths: PathToNode[] = []
for (const pathId of planeArtifact.pathIds) {
const path = engineCommandManager.artifactGraph.get(pathId)
if (!path) continue
if ('codeRef' in path && path.codeRef) {
// TODO should figure out why upstream the path is bad
const isNodePathBad = path.codeRef.pathToNode.length < 2
nodePaths.push(
isNodePathBad
? getNodePathFromSourceRange(kclManager.ast, path.codeRef.range)
: path.codeRef.pathToNode
)
}
}
return onlyConsecutivePaths(nodePaths, nodePaths[0])
}
export function getPathsFromArtifact({
sketchPathToNode,
artifact,
}: {
sketchPathToNode: PathToNode
artifact?: Artifact
}): PathToNode[] | Error {
const plane = getPlaneFromArtifact(
artifact,
engineCommandManager.artifactGraph
)
if (err(plane)) return plane
const paths = getArtifactsOfTypes(
{ keys: plane.pathIds, types: ['path'] },
engineCommandManager.artifactGraph
)
let nodePaths = [...paths.values()]
.map((path) => path.codeRef.pathToNode)
.sort((a, b) => Number(a[1][0]) - Number(b[1][0]))
return onlyConsecutivePaths(nodePaths, sketchPathToNode)
}
function isNodeSafe(node: Expr): boolean {
if (node.type === 'Literal' || node.type === 'MemberExpression') {
return true
}
if (node.type === 'BinaryExpression') {
return isNodeSafe(node.left) && isNodeSafe(node.right)
}
return false
}
/** /**
* Get an artifact from a code source range * Get an artifact from a code source range
*/ */
@ -880,7 +1154,7 @@ export function getArtifactFromRange(
artifactGraph: ArtifactGraph artifactGraph: ArtifactGraph
): Artifact | null { ): Artifact | null {
for (const artifact of artifactGraph.values()) { for (const artifact of artifactGraph.values()) {
if ('codeRef' in artifact) { if ('codeRef' in artifact && artifact.codeRef) {
const match = const match =
artifact.codeRef?.range[0] === range[0] && artifact.codeRef?.range[0] === range[0] &&
artifact.codeRef.range[1] === range[1] artifact.codeRef.range[1] === range[1]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 568 KiB

After

Width:  |  Height:  |  Size: 560 KiB

View File

@ -1,6 +1,7 @@
import { import {
defaultRustSourceRange, defaultRustSourceRange,
defaultSourceRange, defaultSourceRange,
ExecState,
Program, Program,
RustSourceRange, RustSourceRange,
SourceRange, SourceRange,
@ -37,6 +38,7 @@ import { KclManager } from 'lang/KclSingleton'
import { reportRejection } from 'lib/trap' import { reportRejection } from 'lib/trap'
import { markOnce } from 'lib/performance' import { markOnce } from 'lib/performance'
import { MachineManager } from 'components/MachineManagerProvider' import { MachineManager } from 'components/MachineManagerProvider'
import { Node } from 'wasm-lib/kcl/bindings/Node'
// TODO(paultag): This ought to be tweakable. // TODO(paultag): This ought to be tweakable.
const pingIntervalMs = 5_000 const pingIntervalMs = 5_000
@ -2115,11 +2117,15 @@ export class EngineCommandManager extends EventTarget {
Object.values(this.pendingCommands).map((a) => a.promise) Object.values(this.pendingCommands).map((a) => a.promise)
) )
} }
updateArtifactGraph(ast: Program) { updateArtifactGraph(
ast: Node<Program>,
execStateArtifacts: ExecState['artifacts']
) {
this.artifactGraph = createArtifactGraph({ this.artifactGraph = createArtifactGraph({
orderedCommands: this.orderedCommands, orderedCommands: this.orderedCommands,
responseMap: this.responseMap, responseMap: this.responseMap,
ast, ast,
execStateArtifacts,
}) })
// TODO check if these still need to be deferred once e2e tests are working again. // TODO check if these still need to be deferred once e2e tests are working again.
if (this.artifactGraph.size) { if (this.artifactGraph.size) {
@ -2213,7 +2219,11 @@ export class EngineCommandManager extends EventTarget {
commandTypeToTarget: string commandTypeToTarget: string
): string | undefined { ): string | undefined {
for (const [artifactId, artifact] of this.artifactGraph) { for (const [artifactId, artifact] of this.artifactGraph) {
if ('codeRef' in artifact && isOverlap(range, artifact.codeRef.range)) { if (
'codeRef' in artifact &&
artifact.codeRef &&
isOverlap(range, artifact.codeRef.range)
) {
if (commandTypeToTarget === artifact.type) return artifactId if (commandTypeToTarget === artifact.type) return artifactId
} }
} }

View File

@ -297,14 +297,20 @@ export const lineTo: 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 to = segmentInput.to const to = segmentInput.to
const _node = { ...node } const _node = structuredClone(node)
const nodeMeta = getNodeFromPath<PipeExpression>( const nodeMeta = getNodeFromPath<PipeExpression>(
_node, _node,
pathToNode, pathToNode,
'PipeExpression' 'PipeExpression'
) )
if (err(nodeMeta)) return nodeMeta if (err(nodeMeta)) return nodeMeta
const { node: pipe } = nodeMeta const varDec = getNodeFromPath<VariableDeclaration>(
_node,
pathToNode,
'VariableDeclaration'
)
if (err(varDec)) return varDec
const dec = varDec.node.declaration
const newVals: [Expr, Expr] = [ const newVals: [Expr, Expr] = [
createLiteral(roundOff(to[0], 2)), createLiteral(roundOff(to[0], 2)),
@ -333,14 +339,20 @@ export const lineTo: 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,
valueUsedInTransform: valueUsedInTransform, valueUsedInTransform: valueUsedInTransform,
} }
} else if (dec.init.type === 'PipeExpression') {
dec.init.body = [...dec.init.body, newLine]
} else { } else {
pipe.body = [...pipe.body, newLine] dec.init = createPipeExpression([dec.init, newLine])
} }
return { return {
modifiedAst: _node, modifiedAst: _node,
@ -663,11 +675,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))
@ -682,7 +694,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,
@ -694,7 +710,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 }) => {
@ -731,11 +751,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)
@ -748,7 +768,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,
@ -760,7 +784,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 }) => {
@ -2145,8 +2173,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
@ -2227,7 +2253,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,

View File

@ -9,20 +9,47 @@ import {
import { ArtifactGraph, filterArtifacts } from 'lang/std/artifactGraph' import { ArtifactGraph, filterArtifacts } from 'lang/std/artifactGraph'
import { isOverlap } from 'lib/utils' import { isOverlap } from 'lib/utils'
export function updatePathToNodeFromMap( /**
oldPath: PathToNode, * Updates pathToNode body indices to account for the insertion of an expression
pathToNodeMap: { [key: number]: PathToNode } * PathToNode expression is after the insertion index, that the body index is incremented
* Negative insertion index means no insertion
*/
export function updatePathToNodePostExprInjection(
pathToNode: PathToNode,
exprInsertIndex: number
): PathToNode { ): PathToNode {
const updatedPathToNode = structuredClone(oldPath) if (exprInsertIndex < 0) return pathToNode
let max = 0 const bodyIndex = Number(pathToNode[1][0])
Object.values(pathToNodeMap).forEach((path) => { if (bodyIndex < exprInsertIndex) return pathToNode
const index = Number(path[1][0]) const clone = structuredClone(pathToNode)
if (index > max) { clone[1][0] = bodyIndex + 1
max = index return clone
}
export function updateSketchDetailsNodePaths({
sketchEntryNodePath,
sketchNodePaths,
planeNodePath,
exprInsertIndex,
}: {
sketchEntryNodePath: PathToNode
sketchNodePaths: Array<PathToNode>
planeNodePath: PathToNode
exprInsertIndex: number
}) {
return {
updatedSketchEntryNodePath: updatePathToNodePostExprInjection(
sketchEntryNodePath,
exprInsertIndex
),
updatedSketchNodePaths: sketchNodePaths.map((path) =>
updatePathToNodePostExprInjection(path, exprInsertIndex)
),
updatedPlaneNodePath: updatePathToNodePostExprInjection(
planeNodePath,
exprInsertIndex
),
} }
})
updatedPathToNode[1][0] = max
return updatedPathToNode
} }
export function isCursorInSketchCommandRange( export function isCursorInSketchCommandRange(
@ -31,7 +58,7 @@ export function isCursorInSketchCommandRange(
): string | false { ): string | false {
const overlappingEntries = filterArtifacts( const overlappingEntries = filterArtifacts(
{ {
types: ['segment', 'path'], types: ['segment', 'path', 'plane'],
predicate: (artifact) => { predicate: (artifact) => {
return selectionRanges.graphSelections.some( return selectionRanges.graphSelections.some(
(selection) => (selection) =>

View File

@ -45,7 +45,11 @@ import { SourceRange as RustSourceRange } from 'wasm-lib/kcl/bindings/SourceRang
import { getAllCurrentSettings } from 'lib/settings/settingsUtils' import { getAllCurrentSettings } from 'lib/settings/settingsUtils'
import { Operation } from 'wasm-lib/kcl/bindings/Operation' import { Operation } from 'wasm-lib/kcl/bindings/Operation'
import { KclErrorWithOutputs } from 'wasm-lib/kcl/bindings/KclErrorWithOutputs' import { KclErrorWithOutputs } from 'wasm-lib/kcl/bindings/KclErrorWithOutputs'
import { Artifact } from 'wasm-lib/kcl/bindings/Artifact'
import { ArtifactId } from 'wasm-lib/kcl/bindings/ArtifactId'
export type { Artifact } from 'wasm-lib/kcl/bindings/Artifact'
export type { ArtifactId } from 'wasm-lib/kcl/bindings/ArtifactId'
export type { Configuration } from 'wasm-lib/kcl/bindings/Configuration' export type { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
export type { Program } from '../wasm-lib/kcl/bindings/Program' export type { Program } from '../wasm-lib/kcl/bindings/Program'
export type { Expr } from '../wasm-lib/kcl/bindings/Expr' export type { Expr } from '../wasm-lib/kcl/bindings/Expr'
@ -247,6 +251,7 @@ export const isPathToNodeNumber = (
export interface ExecState { export interface ExecState {
memory: ProgramMemory memory: ProgramMemory
operations: Operation[] operations: Operation[]
artifacts: { [key in ArtifactId]?: Artifact }
} }
/** /**
@ -257,6 +262,7 @@ export function emptyExecState(): ExecState {
return { return {
memory: ProgramMemory.empty(), memory: ProgramMemory.empty(),
operations: [], operations: [],
artifacts: {},
} }
} }
@ -264,6 +270,7 @@ function execStateFromRust(execOutcome: RustExecOutcome): ExecState {
return { return {
memory: ProgramMemory.fromRaw(execOutcome.memory), memory: ProgramMemory.fromRaw(execOutcome.memory),
operations: execOutcome.operations, operations: execOutcome.operations,
artifacts: execOutcome.artifacts,
} }
} }
@ -505,26 +512,6 @@ export const executor = async (
if (programMemoryOverride !== null && err(programMemoryOverride)) if (programMemoryOverride !== null && err(programMemoryOverride))
return Promise.reject(programMemoryOverride) return Promise.reject(programMemoryOverride)
// eslint-disable-next-line @typescript-eslint/no-floating-promises
engineCommandManager.startNewSession()
const _programMemory = await _executor(
node,
engineCommandManager,
programMemoryOverride
)
await engineCommandManager.waitForAllCommands()
return _programMemory
}
export const _executor = async (
node: Node<Program>,
engineCommandManager: EngineCommandManager,
programMemoryOverride: ProgramMemory | Error | null = null
): Promise<ExecState> => {
if (programMemoryOverride !== null && err(programMemoryOverride))
return Promise.reject(programMemoryOverride)
try { try {
let jsAppSettings = default_app_settings() let jsAppSettings = default_app_settings()
if (!TEST) { if (!TEST) {

View File

@ -29,7 +29,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([
@ -37,30 +37,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]),
]), ]),
createCallExpressionStdLib('lineTo', [ createCallExpressionStdLib('lineTo', [
createArrayExpression([ createArrayExpression([
@ -85,12 +83,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)]),
@ -120,7 +118,7 @@ export function updateCenterRectangleSketch(
let startY = originY - Math.abs(deltaY) let startY = originY - Math.abs(deltaY)
// pipeExpression.body[1] is startProfileAt // pipeExpression.body[1] is startProfileAt
let callExpression = pipeExpression.body[1] let callExpression = pipeExpression.body[0]
if (isCallExpression(callExpression)) { if (isCallExpression(callExpression)) {
const arrayExpression = callExpression.arguments[0] const arrayExpression = callExpression.arguments[0]
if (isArrayExpression(arrayExpression)) { if (isArrayExpression(arrayExpression)) {
@ -134,7 +132,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)) {
@ -148,7 +146,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)) {

View File

@ -278,18 +278,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, true],
},
}, },
}, },
} }
@ -568,8 +569,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]) =>
@ -578,7 +578,6 @@ export function getSelectionTypeDisplayText(
.replace('solid2D', 'face') .replace('solid2D', 'face')
.replace('segment', 'face')}${count > 1 ? 's' : ''}` .replace('segment', 'face')}${count > 1 ? 's' : ''}`
) )
.toArray()
.join(', ') .join(', ')
} }
@ -588,7 +587,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 &&
@ -611,7 +610,7 @@ 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)) return null if (!('codeRef' in artifact && artifact.codeRef)) return null
return isOverlap(artifact.codeRef.range, selection.range) return isOverlap(artifact.codeRef.range, selection.range)
? { ? {
artifact, artifact,
@ -862,7 +861,6 @@ export function updateSelections(
JSON.stringify(pathToNode) JSON.stringify(pathToNode)
) { ) {
artifact = a artifact = a
console.log('found artifact', a)
break break
} }
} }

View File

@ -1,20 +1,16 @@
import { import {
Program, Program,
ProgramMemory, ProgramMemory,
_executor, executor,
SourceRange, SourceRange,
ExecState, ExecState,
} from '../lang/wasm' } from '../lang/wasm'
import { import { EngineCommandManager } from 'lang/std/engineConnection'
EngineCommandManager,
EngineCommandManagerEvents,
} from 'lang/std/engineConnection'
import { EngineCommand } from 'lang/std/artifactGraph' import { EngineCommand } from 'lang/std/artifactGraph'
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes' import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
import { err, reportRejection } from 'lib/trap' import { err } from 'lib/trap'
import { toSync } from './utils'
import { Node } from 'wasm-lib/kcl/bindings/Node' import { Node } from 'wasm-lib/kcl/bindings/Node'
type WebSocketResponse = Models['WebSocketResponse_type'] type WebSocketResponse = Models['WebSocketResponse_type']
@ -94,36 +90,7 @@ export async function enginelessExecutor(
}) as any as EngineCommandManager }) as any as EngineCommandManager
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
mockEngineCommandManager.startNewSession() mockEngineCommandManager.startNewSession()
const execState = await _executor(ast, mockEngineCommandManager, pmo) const execState = await executor(ast, mockEngineCommandManager, pmo)
await mockEngineCommandManager.waitForAllCommands() await mockEngineCommandManager.waitForAllCommands()
return execState return execState
} }
export async function executor(
ast: Node<Program>,
pmo: ProgramMemory = ProgramMemory.empty()
): Promise<ExecState> {
const engineCommandManager = new EngineCommandManager()
engineCommandManager.start({
setIsStreamReady: () => {},
setMediaStream: () => {},
width: 0,
height: 0,
makeDefaultPlanes: () => {
return new Promise((resolve) => resolve(defaultPlanes))
},
})
return new Promise((resolve) => {
engineCommandManager.addEventListener(
EngineCommandManagerEvents.SceneReady,
toSync(async () => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
engineCommandManager.startNewSession()
const execState = await _executor(ast, engineCommandManager, pmo)
await engineCommandManager.waitForAllCommands()
resolve(execState)
}, reportRejection)
)
})
}

View File

@ -2,8 +2,6 @@ import { CustomIconName } from 'components/CustomIcon'
import { DEV } from 'env' import { DEV } from 'env'
import { commandBarMachine } from 'machines/commandBarMachine' import { commandBarMachine } from 'machines/commandBarMachine'
import { import {
canRectangleOrCircleTool,
isClosedSketch,
isEditingExistingSketch, isEditingExistingSketch,
modelingMachine, modelingMachine,
pipeHasCircle, pipeHasCircle,
@ -72,7 +70,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
icon: 'sketch', icon: 'sketch',
status: 'available', status: 'available',
title: ({ sketchPathId }) => title: ({ sketchPathId }) =>
`${sketchPathId ? 'Edit' : 'Start'} Sketch`, sketchPathId ? 'Edit Sketch' : 'Start Sketch',
showTitle: true, showTitle: true,
hotkey: 'S', hotkey: 'S',
description: 'Start drawing a 2D sketch', description: 'Start drawing a 2D sketch',
@ -332,13 +330,6 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
{ {
id: 'line', id: 'line',
onClick: ({ modelingState, modelingSend }) => { onClick: ({ modelingState, modelingSend }) => {
if (modelingState.matches({ Sketch: { 'Line tool': 'No Points' } })) {
// Exit the sketch state if there are no points and they press ESC
modelingSend({
type: 'Cancel',
})
} else {
// Exit the tool if there are points and they press ESC
modelingSend({ modelingSend({
type: 'change tool', type: 'change tool',
data: { data: {
@ -347,7 +338,6 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
: 'none', : 'none',
}, },
}) })
}
}, },
icon: 'line', icon: 'line',
status: 'available', status: 'available',
@ -358,8 +348,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',
@ -439,10 +428,7 @@ 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' })),
isActive: (state) => state.matches({ Sketch: 'Circle tool' }), isActive: (state) => state.matches({ Sketch: 'Circle tool' }),
hotkey: (state) => hotkey: (state) =>
state.matches({ Sketch: 'Circle tool' }) ? ['Esc', 'C'] : 'C', state.matches({ Sketch: 'Circle tool' }) ? ['Esc', 'C'] : 'C',
@ -490,10 +476,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',
@ -516,10 +499,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' })

View File

@ -97,3 +97,7 @@ export function trap<T>(
}) })
return true return true
} }
export function reject(errOrString: Error | string): Promise<never> {
return Promise.reject(errOrString)
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,47 @@
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::SourceRange;
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Hash, ts_rs::TS, JsonSchema)]
#[ts(export)]
pub struct ArtifactId(Uuid);
impl ArtifactId {
pub fn new(uuid: Uuid) -> Self {
Self(uuid)
}
}
impl From<Uuid> for ArtifactId {
fn from(uuid: Uuid) -> Self {
Self::new(uuid)
}
}
impl From<&Uuid> for ArtifactId {
fn from(uuid: &Uuid) -> Self {
Self::new(*uuid)
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct Artifact {
pub id: ArtifactId,
#[serde(flatten)]
pub inner: ArtifactInner,
pub source_range: SourceRange,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type")]
pub enum ArtifactInner {
#[serde(rename_all = "camelCase")]
StartSketchOnFace { face_id: Uuid },
#[serde(rename_all = "camelCase")]
StartSketchOnPlane { plane_id: Uuid },
}

View File

@ -25,6 +25,7 @@ pub use kcl_value::{KclObjectFields, KclValue};
use uuid::Uuid; use uuid::Uuid;
mod annotations; mod annotations;
mod artifact;
pub(crate) mod cache; pub(crate) mod cache;
mod cad_op; mod cad_op;
mod exec_ast; mod exec_ast;
@ -48,6 +49,7 @@ use crate::{
}; };
// Re-exports. // Re-exports.
pub use artifact::{Artifact, ArtifactId, ArtifactInner};
pub use cad_op::Operation; pub use cad_op::Operation;
/// State for executing a program. /// State for executing a program.
@ -67,6 +69,8 @@ pub struct GlobalState {
pub path_to_source_id: IndexMap<std::path::PathBuf, ModuleId>, pub path_to_source_id: IndexMap<std::path::PathBuf, ModuleId>,
/// Map from module ID to module info. /// Map from module ID to module info.
pub module_infos: IndexMap<ModuleId, ModuleInfo>, pub module_infos: IndexMap<ModuleId, ModuleInfo>,
/// Output map of UUIDs to artifacts.
pub artifacts: IndexMap<ArtifactId, Artifact>,
} }
#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq)] #[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq)]
@ -101,6 +105,8 @@ pub struct ExecOutcome {
/// Operations that have been performed in execution order, for display in /// Operations that have been performed in execution order, for display in
/// the Feature Tree. /// the Feature Tree.
pub operations: Vec<Operation>, pub operations: Vec<Operation>,
/// Output map of UUIDs to artifacts.
pub artifacts: IndexMap<ArtifactId, Artifact>,
} }
impl Default for ExecState { impl Default for ExecState {
@ -141,6 +147,7 @@ impl ExecState {
ExecOutcome { ExecOutcome {
memory: self.mod_local.memory, memory: self.mod_local.memory,
operations: self.mod_local.operations, operations: self.mod_local.operations,
artifacts: self.global.artifacts,
} }
} }
@ -156,6 +163,11 @@ impl ExecState {
self.global.id_generator.next_uuid() self.global.id_generator.next_uuid()
} }
pub fn add_artifact(&mut self, artifact: Artifact) {
let id = artifact.id;
self.global.artifacts.insert(id, artifact);
}
async fn add_module( async fn add_module(
&mut self, &mut self,
path: std::path::PathBuf, path: std::path::PathBuf,
@ -193,6 +205,7 @@ impl GlobalState {
id_generator: Default::default(), id_generator: Default::default(),
path_to_source_id: Default::default(), path_to_source_id: Default::default(),
module_infos: Default::default(), module_infos: Default::default(),
artifacts: Default::default(),
}; };
// TODO(#4434): Use the top-level file's path. // TODO(#4434): Use the top-level file's path.

View File

@ -11,6 +11,7 @@ use parse_display::{Display, FromStr};
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::execution::{Artifact, ArtifactId, ArtifactInner};
use crate::{ use crate::{
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
execution::{ execution::{
@ -1075,7 +1076,17 @@ async fn inner_start_sketch_on(
let plane = make_sketch_plane_from_orientation(plane_data, exec_state, args).await?; let plane = make_sketch_plane_from_orientation(plane_data, exec_state, args).await?;
Ok(SketchSurface::Plane(plane)) Ok(SketchSurface::Plane(plane))
} }
SketchData::Plane(plane) => Ok(SketchSurface::Plane(plane)), SketchData::Plane(plane) => {
// Create artifact used only by the UI, not the engine.
let id = exec_state.next_uuid();
exec_state.add_artifact(Artifact {
id: ArtifactId::from(id),
inner: ArtifactInner::StartSketchOnPlane { plane_id: plane.id },
source_range: args.source_range,
});
Ok(SketchSurface::Plane(plane))
}
SketchData::Solid(solid) => { SketchData::Solid(solid) => {
let Some(tag) = tag else { let Some(tag) = tag else {
return Err(KclError::Type(KclErrorDetails { return Err(KclError::Type(KclErrorDetails {
@ -1084,6 +1095,15 @@ async fn inner_start_sketch_on(
})); }));
}; };
let face = start_sketch_on_face(solid, tag, exec_state, args).await?; let face = start_sketch_on_face(solid, tag, exec_state, args).await?;
// Create artifact used only by the UI, not the engine.
let id = exec_state.next_uuid();
exec_state.add_artifact(Artifact {
id: ArtifactId::from(id),
inner: ArtifactInner::StartSketchOnFace { face_id: face.id },
source_range: args.source_range,
});
Ok(SketchSurface::Face(face)) Ok(SketchSurface::Face(face))
} }
} }