Compare commits

...

52 Commits

Author SHA1 Message Date
d70ebca165 Merge remote-tracking branch 'origin' into kurt-multi-profile-again 2025-02-06 17:40:24 +11:00
d8a9abba69 overlay fix 2025-02-06 17:16:51 +11:00
0fd18c14ef more rust fixes 2025-02-05 14:29:39 +11:00
36d4830c34 Merge remote-tracking branch 'origin' into kurt-multi-profile-again 2025-02-05 13:56:55 +11:00
4ce6054e64 Merge remote-tracking branch 'origin' into kurt-multi-profile-again 2025-02-05 13:56:44 +11:00
ced49f8ddc fix rust 2025-02-05 12:50:58 +11:00
e063622139 more 2025-02-05 12:42:23 +11:00
42178fa649 more 2025-02-05 12:34:19 +11:00
4bb23bc917 bad attempts at fixing rust 2025-02-05 12:27:58 +11:00
72272d5d98 Merge remote-tracking branch 'origin' into kurt-multi-profile-again 2025-02-05 11:49:09 +11:00
5ef0a1e75f Merge remote-tracking branch 'origin' into kurt-multi-profile-again 2025-02-05 11:48:43 +11:00
d8dc49b08a some things needed for multi-profile tests 2025-02-04 15:55:57 +11:00
87eabef450 fix sketch on face after updates to rust side artifact graph 2025-02-04 15:23:35 +11:00
40e4f2236f Merge remote-tracking branch 'origin' into kurt-multi-profile-again 2025-02-04 15:18:30 +11:00
663076f790 add face codef ref for walls and caps 2025-02-04 15:17:50 +11:00
f2c76b0509 Merge remote-tracking branch 'origin' into kurt-multi-profile-again 2025-02-04 06:46:35 +11:00
481bef859a cargo fmt 2025-02-03 22:12:05 +11:00
1a67d344ee Merge remote-tracking branch 'origin' into kurt-multi-profile-again 2025-02-03 22:08:13 +11:00
774e3efcb7 fix types 2025-02-03 22:06:24 +11:00
4ec44690bf fmt 2025-02-03 18:37:16 +11:00
d2f0865f95 Merge remote-tracking branch 'origin' into kurt-multi-profile-again 2025-02-03 18:36:38 +11:00
84d17454e9 get overlays working for circle three point 2025-02-03 17:53:08 +11:00
5a5138a703 most of the fix for 3 point circle 2025-01-31 21:26:19 +11:00
33468c4c96 rust changes to make three point confrom to same as others since we're not ready with name params yet 2025-01-31 13:31:04 +11:00
b3467bbe5a wip 2025-01-27 17:36:29 -05:00
90086488b5 WIP 2025-01-24 15:58:54 -05:00
32e8975799 wip 2025-01-17 15:37:28 -05:00
648616c667 Merge branch 'main' into lf94/kurt-bring-back-multi-profile 2025-01-16 11:13:59 -05:00
482487cf57 Merge branch 'main' into lf94/kurt-bring-back-multi-profile 2025-01-15 14:07:47 -05:00
5fe3023be9 Fix partial execution 2025-01-10 15:39:25 -05:00
30397ba7ab Fix up all the tests 2025-01-09 15:59:11 -05:00
3344208c63 Trigger CI 2025-01-08 21:28:36 -05:00
fcf3272ad2 A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores) 2025-01-09 02:17:14 +00:00
d3e4b123d0 Merge branch 'main' into kurt-bring-back-multi-profile 2025-01-08 21:08:47 -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
66 changed files with 5297 additions and 1879 deletions

View File

@ -54,23 +54,26 @@ async function doBasicSketch(
const startXPx = 600
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
if (openPanes.includes('code')) {
await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ')
|> startProfileAt(${commonPoints.startAt}, %)`)
await expect(u.codeLocator).toContainText(
`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${commonPoints.startAt}, sketch001)`
)
}
await page.waitForTimeout(500)
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await page.waitForTimeout(500)
if (openPanes.includes('code')) {
await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ')
|> startProfileAt(${commonPoints.startAt}, %)
await expect(u.codeLocator)
.toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${commonPoints.startAt}, sketch001)
|> xLine(${commonPoints.num1}, %)`)
}
await page.waitForTimeout(500)
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
if (openPanes.includes('code')) {
await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ')
|> startProfileAt(${commonPoints.startAt}, %)
await expect(u.codeLocator)
.toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${
commonPoints.startAt
}, sketch001)
|> xLine(${commonPoints.num1}, %)
|> yLine(${commonPoints.num1 + 0.01}, %)`)
} else {
@ -79,8 +82,10 @@ async function doBasicSketch(
await page.waitForTimeout(200)
await page.mouse.click(startXPx, 500 - PUR * 20)
if (openPanes.includes('code')) {
await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ')
|> startProfileAt(${commonPoints.startAt}, %)
await expect(u.codeLocator)
.toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${
commonPoints.startAt
}, sketch001)
|> xLine(${commonPoints.num1}, %)
|> yLine(${commonPoints.num1 + 0.01}, %)
|> xLine(${commonPoints.num2 * -1}, %)`)
@ -137,8 +142,10 @@ async function doBasicSketch(
// Open the code pane.
await u.openKclCodePanel()
await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ')
|> startProfileAt(${commonPoints.startAt}, %)
await expect(u.codeLocator)
.toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${
commonPoints.startAt
}, sketch001)
|> xLine(${commonPoints.num1}, %, $seg01)
|> yLine(${commonPoints.num1 + 0.01}, %)
|> xLine(-segLen(seg01), %)`)

View File

@ -41,8 +41,7 @@ test.describe(
},
}
const code = `sketch001 = startSketchOn('${plane}')
|> startProfileAt([0.9, -1.22], %)`
const code = `sketch001 = startSketchOn('${plane}')profile001 = startProfileAt([0.9, -1.22], sketch001)`
await u.openDebugPanel()

View File

@ -301,7 +301,7 @@ test(
}
)
test(
test.skip(
'external change of file contents are reflected in editor',
{ tag: '@electron' },
async ({ context, page }, testInfo) => {

View File

@ -9,13 +9,15 @@ import {
sendCustomCmd,
} from '../test-utils'
type mouseParams = {
type MouseParams = {
pixelDiff?: number
shouldDbClick?: boolean
delay?: number
}
type mouseDragToParams = mouseParams & {
type MouseDragToParams = MouseParams & {
fromPoint: { x: number; y: number }
}
type mouseDragFromParams = mouseParams & {
type MouseDragFromParams = MouseParams & {
toPoint: { x: number; y: number }
}
@ -26,12 +28,12 @@ type SceneSerialised = {
}
}
type ClickHandler = (clickParams?: mouseParams) => Promise<void | boolean>
type MoveHandler = (moveParams?: mouseParams) => Promise<void | boolean>
type DblClickHandler = (clickParams?: mouseParams) => Promise<void | boolean>
type DragToHandler = (dragParams: mouseDragToParams) => Promise<void | boolean>
type ClickHandler = (clickParams?: MouseParams) => Promise<void | boolean>
type MoveHandler = (moveParams?: MouseParams) => Promise<void | boolean>
type DblClickHandler = (clickParams?: MouseParams) => Promise<void | boolean>
type DragToHandler = (dragParams: MouseDragToParams) => Promise<void | boolean>
type DragFromHandler = (
dragParams: mouseDragFromParams
dragParams: MouseDragFromParams
) => Promise<void | boolean>
export class SceneFixture {
@ -77,17 +79,26 @@ export class SceneFixture {
{ steps }: { steps: number } = { steps: 20 }
): [ClickHandler, MoveHandler, DblClickHandler] =>
[
(clickParams?: mouseParams) => {
(clickParams?: MouseParams) => {
if (clickParams?.pixelDiff) {
return doAndWaitForImageDiff(
this.page,
() => this.page.mouse.click(x, y),
() =>
clickParams?.shouldDbClick
? this.page.mouse.dblclick(x, y, {
delay: clickParams?.delay || 0,
})
: this.page.mouse.click(x, y, {
delay: clickParams?.delay || 0,
}),
clickParams.pixelDiff
)
}
return this.page.mouse.click(x, y)
return clickParams?.shouldDbClick
? this.page.mouse.dblclick(x, y, { delay: clickParams?.delay || 0 })
: this.page.mouse.click(x, y, { delay: clickParams?.delay || 0 })
},
(moveParams?: mouseParams) => {
(moveParams?: MouseParams) => {
if (moveParams?.pixelDiff) {
return doAndWaitForImageDiff(
this.page,
@ -97,7 +108,7 @@ export class SceneFixture {
}
return this.page.mouse.move(x, y, { steps })
},
(clickParams?: mouseParams) => {
(clickParams?: MouseParams) => {
if (clickParams?.pixelDiff) {
return doAndWaitForImageDiff(
this.page,
@ -114,7 +125,7 @@ export class SceneFixture {
{ steps }: { steps: number } = { steps: 20 }
): [DragToHandler, DragFromHandler] =>
[
(dragToParams: mouseDragToParams) => {
(dragToParams: MouseDragToParams) => {
if (dragToParams?.pixelDiff) {
return doAndWaitForImageDiff(
this.page,
@ -131,7 +142,7 @@ export class SceneFixture {
targetPosition: { x, y },
})
},
(dragFromParams: mouseDragFromParams) => {
(dragFromParams: MouseDragFromParams) => {
if (dragFromParams?.pixelDiff) {
return doAndWaitForImageDiff(
this.page,
@ -219,7 +230,7 @@ export class SceneFixture {
}
expectPixelColor = async (
colour: [number, number, number],
colour: [number, number, number] | [number, number, number][],
coords: { x: number; y: number },
diff: number
) => {
@ -241,22 +252,36 @@ export class SceneFixture {
}
}
function isColourArray(
colour: [number, number, number] | [number, number, number][]
): colour is [number, number, number][] {
return Array.isArray(colour[0])
}
export async function expectPixelColor(
page: Page,
colour: [number, number, number],
colour: [number, number, number] | [number, number, number][],
coords: { x: number; y: number },
diff: number
) {
let finalValue = colour
await expect
.poll(async () => {
.poll(
async () => {
const pixel = (await getPixelRGBs(page)(coords, 1))[0]
if (!pixel) return null
finalValue = pixel
if (!isColourArray(colour)) {
return pixel.every(
(channel, index) => Math.abs(channel - colour[index]) < diff
)
})
}
return colour.some((c) =>
c.every((channel, index) => Math.abs(pixel[index] - channel) < diff)
)
},
{ timeout: 10_000 }
)
.toBeTruthy()
.catch((cause) => {
throw new Error(

View File

@ -22,7 +22,10 @@ export class ToolbarFixture {
offsetPlaneButton!: Locator
startSketchBtn!: Locator
lineBtn!: Locator
tangentialArcBtn!: Locator
circleBtn!: Locator
rectangleBtn!: Locator
lengthConstraintBtn!: Locator
exitSketchBtn!: Locator
editSketchBtn!: Locator
fileTreeBtn!: Locator
@ -51,7 +54,10 @@ export class ToolbarFixture {
this.offsetPlaneButton = page.getByTestId('plane-offset')
this.startSketchBtn = page.getByTestId('sketch')
this.lineBtn = page.getByTestId('line')
this.tangentialArcBtn = page.getByTestId('tangential-arc')
this.circleBtn = page.getByTestId('circle-center')
this.rectangleBtn = page.getByTestId('corner-rectangle')
this.lengthConstraintBtn = page.getByTestId('constraint-length')
this.exitSketchBtn = page.getByTestId('sketch-exit')
this.editSketchBtn = page.getByText('Edit Sketch')
this.fileTreeBtn = page.locator('[id="files-button-holder"]')
@ -117,6 +123,15 @@ export class ToolbarFixture {
await expect(this.exeIndicator).toBeVisible({ timeout: 15_000 })
}
}
selectCenterRectangle = async () => {
await this.page
.getByRole('button', { name: 'caret down Corner rectangle:' })
.click()
await expect(
this.page.getByTestId('dropdown-center-rectangle')
).toBeVisible()
await this.page.getByTestId('dropdown-center-rectangle').click()
}
async closePane(paneId: SidebarType) {
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',
{
appSettings: {
@ -514,10 +514,7 @@ test.fixme(
const modelColor: [number, number, number] = [76, 76, 76]
await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y)
await expectPixelColor(page, modelColor, XYPlanePoint, 8)
await tutorialDismissButton.click()
// Make sure model still there.
await expectPixelColor(page, modelColor, XYPlanePoint, 8)
})
await test.step('Clear code and restart onboarding from settings', async () => {

View File

@ -216,18 +216,13 @@ test.describe('Point-and-click tests', { tag: ['@skipWin'] }, () => {
afterChamferSelectSnippet:
'sketch002 = startSketchOn(extrude001, seg03)',
afterRectangle1stClickSnippet: 'startProfileAt([205.96, 254.59], %)',
afterRectangle1stClickSnippet:
'startProfileAt([205.96, 254.59], sketch002)',
afterRectangle2ndClickSnippet: `angledLine([0,11.39],%,$rectangleSegmentA002)
|> angledLine([
segAng(rectangleSegmentA002) - 90,
105.26
], %, $rectangleSegmentB001)
|> angledLine([
segAng(rectangleSegmentA002),
-segLen(rectangleSegmentA002)
], %, $rectangleSegmentC001)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()`,
|>angledLine([segAng(rectangleSegmentA002)-90,105.26],%)
|>angledLine([segAng(rectangleSegmentA002),-segLen(rectangleSegmentA002)],%)
|>line(endAbsolute=[profileStartX(%),profileStartY(%)],%)
|>close(%)`,
})
await sketchOnAChamfer({
@ -248,19 +243,15 @@ test.describe('Point-and-click tests', { tag: ['@skipWin'] }, () => {
afterChamferSelectSnippet:
'sketch003 = startSketchOn(extrude001, seg04)',
afterRectangle1stClickSnippet: 'startProfileAt([-209.64, 255.28], %)',
afterRectangle1stClickSnippet:
'startProfileAt([-209.64, 255.28], sketch003)',
afterRectangle2ndClickSnippet: `angledLine([0,11.56],%,$rectangleSegmentA003)
|> angledLine([
segAng(rectangleSegmentA003) - 90,
106.84
], %, $rectangleSegmentB002)
|> angledLine([
segAng(rectangleSegmentA003),
-segLen(rectangleSegmentA003)
], %, $rectangleSegmentC002)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()`,
|>angledLine([segAng(rectangleSegmentA003)-90,106.84],%)
|>angledLine([segAng(rectangleSegmentA003),-segLen(rectangleSegmentA003)],%)
|>line(endAbsolute=[profileStartX(%),profileStartY(%)],%)
|>close(%)`,
})
await sketchOnAChamfer({
clickCoords: { x: 677, y: 87 },
cameraPos: { x: -6200, y: 1500, z: 6200 },
@ -273,19 +264,14 @@ test.describe('Point-and-click tests', { tag: ['@skipWin'] }, () => {
]
}, %)`,
afterChamferSelectSnippet:
'sketch003 = startSketchOn(extrude001, seg04)',
afterRectangle1stClickSnippet: 'startProfileAt([75.8, 317.2], %)',
afterRectangle2ndClickSnippet: `angledLine([0, 11.56], %, $rectangleSegmentA003)
|> angledLine([
segAng(rectangleSegmentA003) - 90,
106.84
], %, $rectangleSegmentB002)
|> angledLine([
segAng(rectangleSegmentA003),
-segLen(rectangleSegmentA003)
], %, $rectangleSegmentC002)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()`,
'sketch004 = startSketchOn(extrude001, seg05)',
afterRectangle1stClickSnippet:
'startProfileAt([82.57, 322.96], sketch004)',
afterRectangle2ndClickSnippet: `angledLine([0,11.16],%,$rectangleSegmentA004)
|>angledLine([segAng(rectangleSegmentA004)-90,103.07],%)
|>angledLine([segAng(rectangleSegmentA004),-segLen(rectangleSegmentA004)],%)
|>line(endAbsolute=[profileStartX(%),profileStartY(%)],%)|
>close(%)`,
})
/// last one
await sketchOnAChamfer({
@ -298,25 +284,18 @@ test.describe('Point-and-click tests', { tag: ['@skipWin'] }, () => {
}, %)`,
afterChamferSelectSnippet:
'sketch005 = startSketchOn(extrude001, seg06)',
afterRectangle1stClickSnippet: 'startProfileAt([-23.43, 19.69], %)',
afterRectangle1stClickSnippet:
'startProfileAt([-23.43, 19.69], sketch005)',
afterRectangle2ndClickSnippet: `angledLine([0,9.1],%,$rectangleSegmentA005)
|> angledLine([
segAng(rectangleSegmentA005) - 90,
84.07
], %, $rectangleSegmentB004)
|> angledLine([
segAng(rectangleSegmentA005),
-segLen(rectangleSegmentA005)
], %, $rectangleSegmentC004)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()`,
|>angledLine([segAng(rectangleSegmentA005)-90,84.07],%)
|>angledLine([segAng(rectangleSegmentA005),-segLen(rectangleSegmentA005)],%)
|>line(endAbsolute=[profileStartX(%),profileStartY(%)],%)
|>close(%)`,
})
await test.step('verify at the end of the test that final code is what is expected', async () => {
await editor.expectEditor.toContain(
`sketch001 = startSketchOn('XZ')
|> startProfileAt([75.8, 317.2], %) // [$startCapTag, $EndCapTag]
|> angledLine([0, 268.43], %, $rectangleSegmentA001)
|> angledLine([
@ -327,9 +306,9 @@ test.describe('Point-and-click tests', { tag: ['@skipWin'] }, () => {
segAng(rectangleSegmentA001),
-segLen(rectangleSegmentA001)
], %, $yo)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $seg02)
|> close()
extrude001 = extrude(sketch001, length = 100)
|> line(endAbsolute=[profileStartX(%), profileStartY(%)], %, $seg02)
|> close(%)
extrude001 = extrude(100, sketch001)
|> chamfer({
length = 30,
tags = [getOppositeEdge(seg01)]
@ -344,57 +323,57 @@ test.describe('Point-and-click tests', { tag: ['@skipWin'] }, () => {
tags = [getNextAdjacentEdge(yo)]
}, %, $seg06)
sketch005 = startSketchOn(extrude001, seg06)
|> startProfileAt([-23.43,19.69], %)
profile004 = startProfileAt([-23.43, 19.69], sketch005)
|> angledLine([0, 9.1], %, $rectangleSegmentA005)
|> angledLine([
segAng(rectangleSegmentA005) - 90,
84.07
], %, $rectangleSegmentB004)
], %)
|> angledLine([
segAng(rectangleSegmentA005),
-segLen(rectangleSegmentA005)
], %, $rectangleSegmentC004)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
], %)
|> line(endAbsolute=[profileStartX(%), profileStartY(%)], %)
|> close(%)
sketch004 = startSketchOn(extrude001, seg05)
|> startProfileAt([82.57,322.96], %)
profile003 = startProfileAt([82.57, 322.96], sketch004)
|> angledLine([0, 11.16], %, $rectangleSegmentA004)
|> angledLine([
segAng(rectangleSegmentA004) - 90,
103.07
], %, $rectangleSegmentB003)
], %)
|> angledLine([
segAng(rectangleSegmentA004),
-segLen(rectangleSegmentA004)
], %, $rectangleSegmentC003)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
], %)
|> line(endAbsolute=[profileStartX(%), profileStartY(%)], %)
|> close(%)
sketch003 = startSketchOn(extrude001, seg04)
|> startProfileAt([-209.64,255.28], %)
profile002 = startProfileAt([-209.64, 255.28], sketch003)
|> angledLine([0, 11.56], %, $rectangleSegmentA003)
|> angledLine([
segAng(rectangleSegmentA003) - 90,
106.84
], %, $rectangleSegmentB002)
], %)
|> angledLine([
segAng(rectangleSegmentA003),
-segLen(rectangleSegmentA003)
], %, $rectangleSegmentC002)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
], %)
|> line(endAbsolute=[profileStartX(%), profileStartY(%)], %)
|> close(%)
sketch002 = startSketchOn(extrude001, seg03)
|> startProfileAt([205.96,254.59], %)
profile001 = startProfileAt([205.96, 254.59], sketch002)
|> angledLine([0, 11.39], %, $rectangleSegmentA002)
|> angledLine([
segAng(rectangleSegmentA002) - 90,
105.26
], %, $rectangleSegmentB001)
], %)
|> angledLine([
segAng(rectangleSegmentA002),
-segLen(rectangleSegmentA002)
], %, $rectangleSegmentC001)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
], %)
|> line(endAbsolute=[profileStartX(%), profileStartY(%)], %)
|> close(%)
`,
{ shouldNormalise: true }
)
@ -439,18 +418,13 @@ test.describe('Point-and-click tests', { tag: ['@skipWin'] }, () => {
beforeChamferSnippetEnd: '}, extrude001)',
afterChamferSelectSnippet:
'sketch002 = startSketchOn(extrude001, seg03)',
afterRectangle1stClickSnippet: 'startProfileAt([205.96, 254.59], %)',
afterRectangle1stClickSnippet:
'startProfileAt([205.96, 254.59], sketch002)',
afterRectangle2ndClickSnippet: `angledLine([0,11.39],%,$rectangleSegmentA002)
|> angledLine([
segAng(rectangleSegmentA002) - 90,
105.26
], %, $rectangleSegmentB001)
|> angledLine([
segAng(rectangleSegmentA002),
-segLen(rectangleSegmentA002)
], %, $rectangleSegmentC001)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()`,
|>angledLine([segAng(rectangleSegmentA002)-90,105.26],%)
|>angledLine([segAng(rectangleSegmentA002),-segLen(rectangleSegmentA002)],%)
|>line(endAbsolute=[profileStartX(%),profileStartY(%)],%)
|>close(%)`,
})
await editor.expectEditor.toContain(
`sketch001 = startSketchOn('XZ')
@ -480,24 +454,119 @@ chamf = chamfer({
]
}, %)
sketch002 = startSketchOn(extrude001, seg03)
|> startProfileAt([205.96, 254.59], %)
profile001 = startProfileAt([205.96, 254.59], sketch002)
|> angledLine([0, 11.39], %, $rectangleSegmentA002)
|> angledLine([
segAng(rectangleSegmentA002) - 90,
105.26
], %, $rectangleSegmentB001)
], %)
|> angledLine([
segAng(rectangleSegmentA002),
-segLen(rectangleSegmentA002)
], %, $rectangleSegmentC001)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
], %)
|> line(endAbsolute=[profileStartX(%), profileStartY(%)], %)
|> close(%)
`,
{ shouldNormalise: true }
)
})
})
test(`Verify axis, origin, and horizontal snapping`, async ({
page,
homePage,
editor,
toolbar,
scene,
}) => {
const viewPortSize = { width: 1200, height: 500 }
await page.setBodyDimensions(viewPortSize)
await homePage.goToModelingScene()
// Constants and locators
// These are mappings from screenspace to KCL coordinates,
// until we merge in our coordinate system helpers
const xzPlane = [
viewPortSize.width * 0.65,
viewPortSize.height * 0.3,
] as const
const originSloppy = {
screen: [
viewPortSize.width / 2 + 3, // 3px off the center of the screen
viewPortSize.height / 2,
],
kcl: [0, 0],
} as const
const xAxisSloppy = {
screen: [
viewPortSize.width * 0.75,
viewPortSize.height / 2 - 3, // 3px off the X-axis
],
kcl: [20.34, 0],
} as const
const offYAxis = {
screen: [
viewPortSize.width * 0.6, // Well off the Y-axis, out of snapping range
viewPortSize.height * 0.3,
],
kcl: [8.14, 6.78],
} as const
const yAxisSloppy = {
screen: [
viewPortSize.width / 2 + 5, // 5px off the Y-axis
viewPortSize.height * 0.3,
],
kcl: [0, 6.78],
} as const
const [clickOnXzPlane, moveToXzPlane] = scene.makeMouseHelpers(...xzPlane)
const [clickOriginSloppy] = scene.makeMouseHelpers(...originSloppy.screen)
const [clickXAxisSloppy, moveXAxisSloppy] = scene.makeMouseHelpers(
...xAxisSloppy.screen
)
const [dragToOffYAxis, dragFromOffAxis] = scene.makeDragHelpers(
...offYAxis.screen
)
const expectedCodeSnippets = {
sketchOnXzPlane: `sketch001 = startSketchOn('XZ')`,
pointAtOrigin: `startProfileAt([${originSloppy.kcl[0]}, ${originSloppy.kcl[1]}], sketch001)`,
segmentOnXAxis: `xLine(${xAxisSloppy.kcl[0]}, %)`,
afterSegmentDraggedOffYAxis: `startProfileAt([${offYAxis.kcl[0]}, ${offYAxis.kcl[1]}], sketch001)`,
afterSegmentDraggedOnYAxis: `startProfileAt([${yAxisSloppy.kcl[0]}, ${yAxisSloppy.kcl[1]}], sketch001)`,
}
await test.step(`Start a sketch on the XZ plane`, async () => {
await editor.closePane()
await toolbar.startSketchPlaneSelection()
await moveToXzPlane()
await clickOnXzPlane()
// timeout wait for engine animation is unavoidable
await page.waitForTimeout(600)
await editor.expectEditor.toContain(expectedCodeSnippets.sketchOnXzPlane)
})
await test.step(`Place a point a few pixels off the middle, verify it still snaps to 0,0`, async () => {
await clickOriginSloppy()
await editor.expectEditor.toContain(expectedCodeSnippets.pointAtOrigin)
})
await test.step(`Add a segment on x-axis after moving the mouse a bit, verify it snaps`, async () => {
await moveXAxisSloppy()
await clickXAxisSloppy()
await editor.expectEditor.toContain(expectedCodeSnippets.segmentOnXAxis)
})
await test.step(`Unequip line tool`, async () => {
await toolbar.lineBtn.click()
await expect(toolbar.lineBtn).not.toHaveAttribute('aria-pressed', 'true')
})
await test.step(`Drag the origin point up and to the right, verify it's past snapping`, async () => {
await dragToOffYAxis({
fromPoint: { x: originSloppy.screen[0], y: originSloppy.screen[1] },
})
})
})
// yo
test(`Verify axis, origin, and horizontal snapping`, async ({
page,
homePage,
@ -1001,6 +1070,21 @@ sketch002 = startSketchOn('XZ')
await cmdBar.progressCmdBar()
await page.waitForTimeout(500)
})
// // yo
// await clickOnSketch2()
// await page.waitForTimeout(500)
// await cmdBar.progressCmdBar()
// await toolbar.openPane('code')
// await page.waitForTimeout(500)
// })
// await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
// await scene.expectPixelColor([135, 64, 73], testPoint, 15)
// await editor.expectEditor.toContain(sweepDeclaration)
// await editor.expectState({
// diagnostics: [],
// activeLines: [sweepDeclaration],
// highlightedCode: '',
await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
await scene.expectPixelColor([135, 64, 73], testPoint, 15)

File diff suppressed because it is too large Load Diff

View File

@ -444,8 +444,7 @@ test(
const startXPx = 600
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
code += `
|> startProfileAt([7.19, -9.7], %)`
code += `profile001 = startProfileAt([7.19, -9.7], sketch001)`
await expect(page.locator('.cm-content')).toHaveText(code)
await page.waitForTimeout(100)
@ -467,6 +466,10 @@ test(
.getByRole('button', { name: 'arc Tangential Arc', exact: true })
.click()
// click to continue profile
await page.mouse.move(813, 392, { steps: 10 })
await page.waitForTimeout(100)
await page.mouse.move(startXPx + PUR * 30, 500 - PUR * 20, { steps: 10 })
await page.waitForTimeout(1000)
@ -589,8 +592,7 @@ test(
mask: [page.getByTestId('model-state-indicator')],
})
await expect(page.locator('.cm-content')).toHaveText(
`sketch001 = startSketchOn('XZ')
|> circle({ center = [14.44, -2.44], radius = 1 }, %)`
`sketch001 = startSketchOn('XZ')profile001 = circle({ center = [14.44, -2.44], radius = 1 }, sketch001)`
)
}
)
@ -634,8 +636,7 @@ test.describe(
const startXPx = 600
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
code += `
|> startProfileAt([7.19, -9.7], %)`
code += `profile001 = startProfileAt([7.19, -9.7], sketch001)`
await expect(u.codeLocator).toHaveText(code)
await page.waitForTimeout(100)
@ -653,6 +654,10 @@ test.describe(
.click()
await page.waitForTimeout(100)
// click to continue profile
await page.mouse.click(813, 392)
await page.waitForTimeout(100)
await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20)
code += `
@ -739,8 +744,7 @@ test.describe(
const startXPx = 600
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
code += `
|> startProfileAt([182.59, -246.32], %)`
code += `profile001 = startProfileAt([182.59, -246.32], sketch001)`
await expect(u.codeLocator).toHaveText(code)
await page.waitForTimeout(100)
@ -758,6 +762,10 @@ test.describe(
.click()
await page.waitForTimeout(100)
// click to continue profile
await page.mouse.click(813, 392)
await page.waitForTimeout(100)
await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20)
code += `

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 51 KiB

View File

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

View File

@ -673,7 +673,7 @@ test.describe('Testing constraints', { tag: ['@skipWin'] }, () => {
},
] as const
for (const { testName, addVariable, value, constraint } of cases) {
test(`${testName}`, async ({ context, homePage, page }) => {
test(`${testName}`, async ({ context, homePage, page, editor }) => {
// constants and locators
const cmdBarKclInput = page
.getByTestId('cmd-bar-arg-value')
@ -706,8 +706,11 @@ part002 = startSketchOn('XZ')
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await u.waitForPageLoad()
await page.getByText('line(end = [74.36, 130.4])').click()
await editor.scrollToText('line(end = [74.36, 130.4], %)', true)
await page.getByText('line(end = [74.36, 130.4], %)').click()
await page.screenshot({ path: 'ok.png' })
await page.getByRole('button', { name: 'Edit Sketch' }).click()
const line3 = await u.getSegmentBodyCoords(

View File

@ -63,33 +63,38 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => {
await page.mouse.click(700, 200)
await page.waitForTimeout(700) // wait for animation
// select a plane
await page.mouse.click(700, 200)
await page.waitForTimeout(700) // wait for animation
const startXPx = 600
await u.closeDebugPanel()
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
await expect(page.locator('.cm-content'))
.toHaveText(`sketch001 = startSketchOn('XZ')
|> startProfileAt(${commonPoints.startAt}, %)`)
await expect(page.locator('.cm-content')).toHaveText(
`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${commonPoints.startAt}, sketch001)`
)
await page.waitForTimeout(100)
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await expect(page.locator('.cm-content'))
.toHaveText(`sketch001 = startSketchOn('XZ')
|> startProfileAt(${commonPoints.startAt}, %)
.toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${commonPoints.startAt}, sketch001)
|> xLine(${commonPoints.num1}, %)`)
await page.waitForTimeout(100)
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
await expect(page.locator('.cm-content'))
.toHaveText(`sketch001 = startSketchOn('XZ')
|> startProfileAt(${commonPoints.startAt}, %)
.toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${
commonPoints.startAt
}, sketch001)
|> xLine(${commonPoints.num1}, %)
|> yLine(${commonPoints.num1 + 0.01}, %)`)
await page.waitForTimeout(100)
await page.mouse.click(startXPx, 500 - PUR * 20)
await expect(page.locator('.cm-content'))
.toHaveText(`sketch001 = startSketchOn('XZ')
|> startProfileAt(${commonPoints.startAt}, %)
.toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${
commonPoints.startAt
}, sketch001)
|> xLine(${commonPoints.num1}, %)
|> yLine(${commonPoints.num1 + 0.01}, %)
|> xLine(${commonPoints.num2 * -1}, %)`)
@ -256,64 +261,86 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => {
'persistCode',
`sketch001 = startSketchOn('XZ')
|> startProfileAt([-79.26, 95.04], %)
|> line(end = [112.54, 127.64], tag = $seg02)
|> line(end = [170.36, -121.61], tag = $seg01)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
extrude001 = extrude(sketch001, length = 50)
|> line(end=[112.54, 127.64], %, $seg02)
|> line(end=[170.36, -121.61], %, $seg01)
|> line(endAbsolute=[profileStartX(%), profileStartY(%)], %)
|> close(%)
extrude001 = extrude(50, sketch001)
sketch005 = startSketchOn(extrude001, 'END')
|> startProfileAt([23.24, 136.52], %)
|> line(end = [-8.44, 36.61])
|> line(end = [49.4, 2.05])
|> line(end = [29.69, -46.95])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
|> line(end=[-8.44, 36.61], %)
|> line(end=[49.4, 2.05], %)
|> line(end=[29.69, -46.95], %)
|> line(endAbsolute=[profileStartX(%), profileStartY(%)], %)
|> close(%)
sketch003 = startSketchOn(extrude001, seg01)
|> startProfileAt([21.23, 17.81], %)
|> line(end = [51.97, 21.32])
|> line(end = [4.07, -22.75])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
|> line(end=[51.97, 21.32], %)
|> line(end=[4.07, -22.75], %)
|> line(endAbsolute=[profileStartX(%), profileStartY(%)], %)
|> close(%)
sketch002 = startSketchOn(extrude001, seg02)
|> startProfileAt([-100.54, 16.99], %)
|> line(end = [0, 20.03])
|> line(end = [62.61, 0], tag = $seg03)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
extrude002 = extrude(sketch002, length = 50)
|> line(end=[0, 20.03], %)
|> line(end=[62.61, 0], %, $seg03)
|> line(endAbsolute=[profileStartX(%), profileStartY(%)], %)
|> close(%)
extrude002 = extrude(50, sketch002)
sketch004 = startSketchOn(extrude002, seg03)
|> startProfileAt([57.07, 134.77], %)
|> line(end = [-4.72, 22.84])
|> line(end = [28.8, 6.71])
|> line(end = [9.19, -25.33])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
extrude003 = extrude(sketch004, length = 20)
|> line(end=[-4.72, 22.84], %)
|> line(end=[28.8, 6.71], %)
|> line(end=[9.19, -25.33], %)
|> line(endAbsolute=[profileStartX(%), profileStartY(%)], %)
|> close(%)
extrude003 = extrude(20, sketch004)
pipeLength = 40
pipeSmallDia = 10
pipeLargeDia = 20
thickness = 0.5
part009 = startSketchOn('XY')
|> startProfileAt([pipeLargeDia - (thickness / 2), 38], %)
|> line(end = [thickness, 0])
|> line(end = [0, -1])
|> line(end=[thickness, 0], %)
|> line(end=[0, -1], %)
|> angledLineToX({
angle = 60,
to = pipeSmallDia + thickness
}, %)
|> line(end = [0, -pipeLength])
|> line(end=[0, -pipeLength], %)
|> angledLineToX({
angle = -60,
to = pipeLargeDia + thickness
}, %)
|> line(end = [0, -1])
|> line(end = [-thickness, 0])
|> line(end = [0, 1])
|> line(end=[0, -1], %)
|> line(end=[-thickness, 0], %)
|> line(end=[0, 1], %)
|> angledLineToX({ angle = 120, to = pipeSmallDia }, %)
|> line(end = [0, pipeLength])
|> line(end=[0, pipeLength], %)
|> angledLineToX({ angle = 60, to = pipeLargeDia }, %)
|> close()
rev = revolve({ axis: 'y' }, part009)
|> close(%)
rev = revolve({ axis = 'y' }, part009)
sketch006 = startSketchOn('XY')
profile001 = circle({
center = [42.91, -70.42],
radius = 17.96
}, sketch006)
profile002 = startProfileAt([86.92, -63.81], sketch006)
|> angledLine([0, 63.81], %, $rectangleSegmentA001)
|> angledLine([
segAng(rectangleSegmentA001) - 90,
17.05
], %)
|> angledLine([
segAng(rectangleSegmentA001),
-segLen(rectangleSegmentA001)
], %)
|> line(endAbsolute=[profileStartX(%), profileStartY(%)], %)
|> close(%)
profile003 = startProfileAt([40.16, -120.48], sketch006)
|> line(end=[26.95, 24.21], %)
|> line(end=[20.91, -28.61], %)
|> line(end=[32.46, 18.71], %)
`
)
}, KCL_DEFAULT_LENGTH)
@ -346,9 +373,10 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => {
})
await page.waitForTimeout(100)
const revolve = { x: 646, y: 248 }
const revolve = { x: 635, y: 253 }
const parentExtrude = { x: 915, y: 133 }
const solid2d = { x: 770, y: 167 }
const individualProfile = { x: 694, y: 432 }
// DELETE REVOLVE
await page.mouse.click(revolve.x, revolve.y)
@ -414,6 +442,20 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => {
await u.expectCmdLog('[data-message-type="execution-done"]', 10_000)
await page.waitForTimeout(200)
await expect(u.codeLocator).not.toContainText(`sketch005 = startSketchOn({`)
// Delete a single profile
await page.mouse.click(individualProfile.x, individualProfile.y)
await page.waitForTimeout(100)
const codeToBeDeletedSnippet =
'profile003 = startProfileAt([40.16, -120.48], sketch006)'
await expect(page.locator('.cm-activeLine')).toHaveText(
' |> line([20.91, -28.61], %)'
)
await u.clearCommandLogs()
await page.keyboard.press('Backspace')
await u.expectCmdLog('[data-message-type="execution-done"]', 10_000)
await page.waitForTimeout(200)
await expect(u.codeLocator).not.toContainText(codeToBeDeletedSnippet)
})
test("Deleting solid that the AST mod can't handle results in a toast message", async ({
page,
@ -1211,12 +1253,15 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => {
await page.waitForTimeout(600)
const firstClickCoords = { x: 650, y: 200 } as const
// Place a point because the line tool will exit if no points are pressed
await page.mouse.click(650, 200)
await page.mouse.click(firstClickCoords.x, firstClickCoords.y)
await page.waitForTimeout(600)
// Code before exiting the tool
let previousCodeContent = await page.locator('.cm-content').innerText()
let previousCodeContent = (
await page.locator('.cm-content').innerText()
).replace(/\s+/g, '')
// deselect the line tool by clicking it
await page.getByRole('button', { name: 'line Line', exact: true }).click()
@ -1228,14 +1273,23 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => {
await page.mouse.click(750, 200)
await page.waitForTimeout(100)
// expect no change
await expect(page.locator('.cm-content')).toHaveText(previousCodeContent)
await expect
.poll(async () => {
let str = await page.locator('.cm-content').innerText()
str = str.replace(/\s+/g, '')
return str
})
.toBe(previousCodeContent)
// select line tool again
await page.getByRole('button', { name: 'line Line', exact: true }).click()
await u.closeDebugPanel()
// Click to continue profile
await page.mouse.click(firstClickCoords.x, firstClickCoords.y)
await page.waitForTimeout(100)
// line tool should work as expected again
await page.mouse.click(700, 200)
await expect(page.locator('.cm-content')).not.toHaveText(

View File

@ -205,8 +205,13 @@ test('First escape in tool pops you out of tool, second exits sketch mode', asyn
// Draw a line
await page.mouse.move(700, 200, { steps: 5 })
await page.mouse.click(700, 200)
await page.mouse.move(800, 250, { steps: 5 })
await page.mouse.click(800, 250)
const secondMousePosition = { x: 800, y: 250 }
await page.mouse.move(secondMousePosition.x, secondMousePosition.y, {
steps: 5,
})
await page.mouse.click(secondMousePosition.x, secondMousePosition.y)
// Unequip line tool
await page.keyboard.press('Escape')
// Make sure we didn't pop out of sketch mode.
@ -215,9 +220,17 @@ test('First escape in tool pops you out of tool, second exits sketch mode', asyn
// Equip arc tool
await page.keyboard.press('a')
await expect(arcButton).toHaveAttribute('aria-pressed', 'true')
// click in the same position again to continue the profile
await page.mouse.move(secondMousePosition.x, secondMousePosition.y, {
steps: 5,
})
await page.mouse.click(secondMousePosition.x, secondMousePosition.y)
await page.mouse.move(1000, 100, { steps: 5 })
await page.mouse.click(1000, 100)
await page.keyboard.press('Escape')
await expect(arcButton).toHaveAttribute('aria-pressed', 'false')
await page.keyboard.press('l')
await expect(lineButton).toHaveAttribute('aria-pressed', 'true')
@ -519,11 +532,11 @@ extrude001 = extrude(sketch001, length = 5 + 7)`
await expect.poll(u.normalisedEditorCode).toContain(
u.normalisedCode(`sketch002 = startSketchOn(extrude001, seg01)
|> startProfileAt([-12.94, 6.6], %)
|> line(end = [2.45, -0.2])
|> line(end = [-2.6, -1.25])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
profile001 = startProfileAt([-12.88, 6.66], sketch002)
|> line(end = [2.71, -0.22], %)
|> line(end = [-2.87, -1.38], %)
|> lineTo(endAbsolute = [profileStartX(%), profileStartY(%)], %)
|> close(%)
`)
)
@ -537,9 +550,8 @@ extrude001 = extrude(sketch001, length = 5 + 7)`
await page.getByText('startProfileAt([-12').click()
await expect(page.getByRole('button', { name: 'Edit Sketch' })).toBeVisible()
await page.getByRole('button', { name: 'Edit Sketch' }).click()
await page.waitForTimeout(400)
await page.waitForTimeout(150)
await page.setBodyDimensions({ width: 1200, height: 1200 })
await page.waitForTimeout(500)
await page.setViewportSize({ width: 1200, height: 1200 })
await u.openAndClearDebugPanel()
await u.updateCamPosition([452, -152, 1166])
await u.closeDebugPanel()

View File

@ -5,7 +5,6 @@ import { useModelingContext } from 'hooks/useModelingContext'
import { useNetworkContext } from 'hooks/useNetworkContext'
import { NetworkHealthState } from 'hooks/useNetworkStatus'
import { ActionButton } from 'components/ActionButton'
import { isSingleCursorInPipe } from 'lang/queryAst'
import { useKclContext } from 'lang/KclProvider'
import { ActionButtonDropdown } from 'components/ActionButtonDropdown'
import { useHotkeys } from 'react-hotkeys-hook'
@ -21,6 +20,7 @@ import {
} from 'lib/toolbar'
import { isDesktop } from 'lib/isDesktop'
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
import { isCursorInFunctionDefinition } from 'lang/queryAst'
import { commandBarActor } from 'machines/commandBarMachine'
import { isArray } from 'lib/utils'
@ -37,7 +37,12 @@ export function Toolbar({
const buttonBorderClassName = '!border-transparent'
const sketchPathId = useMemo(() => {
if (!isSingleCursorInPipe(context.selectionRanges, kclManager.ast))
if (
isCursorInFunctionDefinition(
kclManager.ast,
context.selectionRanges.graphSelections[0]
)
)
return false
return isCursorInSketchCommandRange(
engineCommandManager.artifactGraph,

View File

@ -125,14 +125,7 @@ export const ClientSideScene = ({
'mouseup',
toSync(sceneInfra.onMouseUp, reportRejection)
)
sceneEntitiesManager
.tearDownSketch()
.then(() => {
// no op
})
.catch((e) => {
console.error(e)
})
sceneEntitiesManager.tearDownSketch({ removeAxis: true })
}
}, [])
@ -191,12 +184,15 @@ const Overlays = () => {
style={{ zIndex: '99999999' }}
>
{Object.entries(context.segmentOverlays)
.filter((a) => a[1].visible)
.map(([pathToNodeString, overlay], index) => {
.flatMap((a) =>
a[1].map((b) => ({ pathToNodeString: a[0], overlay: b }))
)
.filter((a) => a.overlay.visible)
.map(({ pathToNodeString, overlay }, index) => {
return (
<Overlay
overlay={overlay}
key={pathToNodeString}
key={pathToNodeString + String(index)}
pathToNodeString={pathToNodeString}
overlayIndex={index}
/>
@ -237,11 +233,17 @@ const Overlay = ({
const constraints =
callExpression.type === 'CallExpression'
? getConstraintInfo(callExpression, codeManager.code, overlay.pathToNode)
? getConstraintInfo(
callExpression,
codeManager.code,
overlay.pathToNode,
overlay.filterValue
)
: getConstraintInfoKw(
callExpression,
codeManager.code,
overlay.pathToNode
overlay.pathToNode,
overlay.filterValue
)
const offset = 20 // px
@ -261,7 +263,6 @@ const Overlay = ({
state.matches({ Sketch: 'Tangential arc to' }) ||
state.matches({ Sketch: 'Rectangle tool' })
)
return (
<div className={`absolute w-0 h-0`}>
<div
@ -319,7 +320,8 @@ const Overlay = ({
this will likely change soon when we implement multi-profile so we'll leave it for now
issue: https://github.com/KittyCAD/modeling-app/issues/3910
*/}
{callExpression?.callee?.name !== 'circle' && (
{callExpression?.callee?.name !== 'circle' &&
callExpression?.callee?.name !== 'circleThreePoint' && (
<SegmentMenu
verticalPosition={
overlay.windowCoords[1] > window.innerHeight / 2
@ -450,6 +452,8 @@ export async function deleteSegment({
if (!sketchDetails) return
await sceneEntitiesManager.updateAstAndRejigSketch(
pathToNode,
sketchDetails.sketchNodePaths,
sketchDetails.planeNodePath,
modifiedAst,
sketchDetails.zAxis,
sketchDetails.yAxis,

File diff suppressed because it is too large Load Diff

View File

@ -182,13 +182,15 @@ export class SceneInfra {
callbacks: (() => SegmentOverlayPayload | null)[] = []
_overlayCallbacks(callbacks: (() => SegmentOverlayPayload | null)[]) {
const segmentOverlayPayload: SegmentOverlayPayload = {
type: 'set-many',
type: 'add-many',
overlays: {},
}
callbacks.forEach((cb) => {
const overlay = cb()
if (overlay?.type === 'set-one') {
segmentOverlayPayload.overlays[overlay.pathToNodeString] = overlay.seg
} else if (overlay?.type === 'add-many') {
Object.assign(segmentOverlayPayload.overlays, overlay.overlays)
}
})
this.modelingSend({
@ -213,25 +215,27 @@ export class SceneInfra {
overlayThrottleMap: { [pathToNodeString: string]: number } = {}
updateOverlayDetails({
arrowGroup,
handle,
group,
isHandlesVisible,
from,
to,
angle,
hasThreeDotMenu,
}: {
arrowGroup: Group
handle: Group
group: Group
isHandlesVisible: boolean
from: Coords2d
to: Coords2d
hasThreeDotMenu: boolean
angle?: number
}): SegmentOverlayPayload | null {
if (!group.userData.draft && group.userData.pathToNode && arrowGroup) {
if (!group.userData.draft && group.userData.pathToNode && handle) {
const vector = new Vector3(0, 0, 0)
// Get the position of the object3D in world space
arrowGroup.getWorldPosition(vector)
handle.getWorldPosition(vector)
// Project that position to screen space
vector.project(this.camControls.camera)
@ -244,13 +248,16 @@ export class SceneInfra {
return {
type: 'set-one',
pathToNodeString,
seg: {
seg: [
{
windowCoords: [x, y],
angle: _angle,
group,
pathToNode: group.userData.pathToNode,
visible: isHandlesVisible,
hasThreeDotMenu,
},
],
}
}
return null

View File

@ -31,6 +31,12 @@ import {
CIRCLE_SEGMENT,
CIRCLE_SEGMENT_BODY,
CIRCLE_SEGMENT_DASH,
CIRCLE_THREE_POINT_HANDLE1,
CIRCLE_THREE_POINT_HANDLE2,
CIRCLE_THREE_POINT_HANDLE3,
CIRCLE_THREE_POINT_SEGMENT,
CIRCLE_THREE_POINT_SEGMENT_BODY,
CIRCLE_THREE_POINT_SEGMENT_DASH,
EXTRA_SEGMENT_HANDLE,
EXTRA_SEGMENT_OFFSET_PX,
HIDE_HOVER_SEGMENT_LENGTH,
@ -48,19 +54,26 @@ import {
import { getTangentPointFromPreviousArc } from 'lib/utils2d'
import {
ARROWHEAD,
CIRCLE_3_POINT_DRAFT_CIRCLE,
DRAFT_POINT,
SceneInfra,
SEGMENT_LENGTH_LABEL,
SEGMENT_LENGTH_LABEL_OFFSET_PX,
SEGMENT_LENGTH_LABEL_TEXT,
SKETCH_LAYER,
} from './sceneInfra'
import { Themes, getThemeColorForThreeJs } from 'lib/theme'
import { normaliseAngle, roundOff } from 'lib/utils'
import { SegmentOverlayPayload } from 'machines/modelingMachine'
import {
SegmentOverlay,
SegmentOverlayPayload,
SegmentOverlays,
} from 'machines/modelingMachine'
import { SegmentInputs } from 'lang/std/stdTypes'
import { err } from 'lib/trap'
import { editorManager, sceneInfra } from 'lib/singletons'
import { Selections } from 'lib/selections'
import { calculate_circle_from_3_points } from 'wasm-lib/pkg/wasm_lib'
import { commandBarActor } from 'machines/commandBarMachine'
interface CreateSegmentArgs {
@ -307,11 +320,12 @@ class StraightSegment implements SegmentUtils {
}
return () =>
sceneInfra.updateOverlayDetails({
arrowGroup,
handle: arrowGroup,
group,
isHandlesVisible,
from,
to,
hasThreeDotMenu: true,
})
}
}
@ -483,12 +497,13 @@ class TangentialArcToSegment implements SegmentUtils {
)
return () =>
sceneInfra.updateOverlayDetails({
arrowGroup,
handle: arrowGroup,
group,
isHandlesVisible,
from,
to,
angle,
hasThreeDotMenu: true,
})
}
}
@ -684,35 +699,255 @@ class CircleSegment implements SegmentUtils {
}
return () =>
sceneInfra.updateOverlayDetails({
arrowGroup,
handle: arrowGroup,
group,
isHandlesVisible,
from: from,
to: [center[0], center[1]],
angle: Math.PI / 4,
hasThreeDotMenu: true,
})
}
}
class CircleThreePointSegment implements SegmentUtils {
init: SegmentUtils['init'] = ({
input,
id,
pathToNode,
isDraftSegment,
scale = 1,
theme,
isSelected = false,
sceneInfra,
prevSegment,
}) => {
if (input.type !== 'circle-three-point-segment') {
return new Error('Invalid segment type')
}
const { p1, p2, p3 } = input
const { center_x, center_y, radius } = calculate_circle_from_3_points(
p1[0],
p1[1],
p2[0],
p2[1],
p3[0],
p3[1]
)
const center: [number, number] = [center_x, center_y]
const baseColor = getThemeColorForThreeJs(theme)
const color = isSelected ? 0x0000ff : baseColor
const group = new Group()
const geometry = createArcGeometry({
center,
radius,
startAngle: 0,
endAngle: Math.PI * 2,
ccw: true,
isDashed: isDraftSegment,
scale,
})
const mat = new MeshBasicMaterial({ color })
const arcMesh = new Mesh(geometry, mat)
const meshType = isDraftSegment
? CIRCLE_THREE_POINT_SEGMENT_DASH
: CIRCLE_THREE_POINT_SEGMENT_BODY
const handle1 = createCircleThreePointHandle(
scale,
theme,
CIRCLE_THREE_POINT_HANDLE1,
color
)
const handle2 = createCircleThreePointHandle(
scale,
theme,
CIRCLE_THREE_POINT_HANDLE2,
color
)
const handle3 = createCircleThreePointHandle(
scale,
theme,
CIRCLE_THREE_POINT_HANDLE3,
color
)
arcMesh.userData.type = meshType
arcMesh.name = meshType
group.userData = {
type: CIRCLE_THREE_POINT_SEGMENT,
draft: isDraftSegment,
id,
p1,
p2,
p3,
ccw: true,
prevSegment,
pathToNode,
isSelected,
baseColor,
}
group.name = CIRCLE_THREE_POINT_SEGMENT
group.add(arcMesh, handle1, handle2, handle3)
const updateOverlaysCallback = this.update({
prevSegment,
input,
group,
scale,
sceneInfra,
})
if (err(updateOverlaysCallback)) return updateOverlaysCallback
return {
group,
updateOverlaysCallback,
}
}
update: SegmentUtils['update'] = ({
input,
group,
scale = 1,
sceneInfra,
}) => {
if (input.type !== 'circle-three-point-segment') {
return new Error('Invalid segment type')
}
const { p1, p2, p3 } = input
group.userData.p1 = p1
group.userData.p2 = p2
group.userData.p3 = p3
const { center_x, center_y, radius } = calculate_circle_from_3_points(
p1[0],
p1[1],
p2[0],
p2[1],
p3[0],
p3[1]
)
const center: [number, number] = [center_x, center_y]
const points = [p1, p2, p3]
const handles = [
CIRCLE_THREE_POINT_HANDLE1,
CIRCLE_THREE_POINT_HANDLE2,
CIRCLE_THREE_POINT_HANDLE3,
].map((handle) => group.getObjectByName(handle) as Group)
handles.forEach((handle, i) => {
const point = points[i]
if (handle && point) {
handle.position.set(point[0], point[1], 0)
handle.scale.set(scale, scale, scale)
handle.visible = true
}
})
const pxLength = (2 * radius * Math.PI) / scale
const shouldHideIdle = pxLength < HIDE_SEGMENT_LENGTH
const shouldHideHover = pxLength < HIDE_HOVER_SEGMENT_LENGTH
const hoveredParent =
sceneInfra.hoveredObject &&
getParentGroup(sceneInfra.hoveredObject, [CIRCLE_SEGMENT])
let isHandlesVisible = !shouldHideIdle
if (hoveredParent && hoveredParent?.uuid === group?.uuid) {
isHandlesVisible = !shouldHideHover
}
const circleSegmentBody = group.children.find(
(child) => child.userData.type === CIRCLE_THREE_POINT_SEGMENT_BODY
) as Mesh
if (circleSegmentBody) {
const newGeo = createArcGeometry({
radius,
center,
startAngle: 0,
endAngle: Math.PI * 2,
ccw: true,
scale,
})
circleSegmentBody.geometry = newGeo
}
const circleSegmentBodyDashed = group.getObjectByName(
CIRCLE_THREE_POINT_SEGMENT_DASH
)
if (circleSegmentBodyDashed instanceof Mesh) {
// consider throttling the whole updateTangentialArcToSegment
// if there are more perf considerations going forward
circleSegmentBodyDashed.geometry = createArcGeometry({
center,
radius,
ccw: true,
// make the start end where the handle is
startAngle: Math.PI * 0.25,
endAngle: Math.PI * 2.25,
isDashed: true,
scale,
})
}
return () => {
const overlays: SegmentOverlays = {}
const points = [p1, p2, p3]
const overlayDetails = handles.map((handle, index) => {
const currentPoint = points[index]
const angle = Math.atan2(
currentPoint[1] - center[1],
currentPoint[0] - center[0]
)
return sceneInfra.updateOverlayDetails({
handle,
group,
isHandlesVisible,
from: [0, 0],
to: [center[0], center[1]],
angle: angle,
hasThreeDotMenu: index === 0,
})
})
const segmentOverlays: SegmentOverlay[] = []
overlayDetails.forEach((payload, index) => {
if (payload?.type === 'set-one') {
overlays[payload.pathToNodeString] = payload.seg
segmentOverlays.push({
...payload.seg[0],
filterValue: index === 0 ? 'p1' : index === 1 ? 'p2' : 'p3',
})
}
})
const segmentOverlayPayload: SegmentOverlayPayload = {
type: 'set-one',
pathToNodeString:
overlayDetails[0]?.type === 'set-one'
? overlayDetails[0].pathToNodeString
: '',
seg: segmentOverlays,
}
return segmentOverlayPayload
}
}
}
export function createProfileStartHandle({
from,
isDraft = false,
scale = 1,
theme,
isSelected,
size = 12,
...rest
}: {
from: Coords2d
scale?: number
theme: Themes
isSelected?: boolean
size?: number
} & (
| { isDraft: true }
| { isDraft: false; id: string; pathToNode: PathToNode }
)) {
const group = new Group()
const geometry = new BoxGeometry(12, 12, 12) // in pixels scaled later
const geometry = new BoxGeometry(size, size, size) // in pixels scaled later
const baseColor = getThemeColorForThreeJs(theme)
const color = isSelected ? 0x0000ff : baseColor
const body = new MeshBasicMaterial({ color })
@ -774,6 +1009,32 @@ function createCircleCenterHandle(
circleCenterGroup.scale.set(scale, scale, scale)
return circleCenterGroup
}
function createCircleThreePointHandle(
scale = 1,
theme: Themes,
name:
| 'circle-three-point-handle1'
| 'circle-three-point-handle2'
| 'circle-three-point-handle3',
color?: number
): Group {
const circleCenterGroup = new Group()
const geometry = new BoxGeometry(12, 12, 12) // in pixels scaled later
const baseColor = getThemeColorForThreeJs(theme)
const body = new MeshBasicMaterial({ color })
const mesh = new Mesh(geometry, body)
circleCenterGroup.add(mesh)
circleCenterGroup.userData = {
type: name,
baseColor,
}
circleCenterGroup.name = name
circleCenterGroup.scale.set(scale, scale, scale)
return circleCenterGroup
}
function createExtraSegmentHandle(
scale: number,
@ -1100,4 +1361,5 @@ export const segmentUtils = {
straight: new StraightSegment(),
tangentialArcTo: new TangentialArcToSegment(),
circle: new CircleSegment(),
circleThreePoint: new CircleThreePointSegment(),
} as const

View File

@ -329,7 +329,7 @@ export const FileMachineProvider = ({
onSubmit: async (data) => {
if (data.method === 'overwrite') {
codeManager.updateCodeStateEditor(data.code)
await kclManager.executeCode(true)
await kclManager.executeCode({ zoomToFit: true })
await codeManager.writeToFile()
} else if (data.method === 'newFile' && isDesktop()) {
send({

View File

@ -21,7 +21,6 @@ import { ContextMenu, ContextMenuItem } from './ContextMenu'
import usePlatform from 'hooks/usePlatform'
import { FileEntry } from 'lib/project'
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
import { normalizeLineEndings } from 'lib/codeEditor'
import { reportRejection } from 'lib/trap'
function getIndentationCSS(level: number) {
@ -190,24 +189,25 @@ const FileTreeItem = ({
// Because subtrees only render when they are opened, that means this
// only listens when they open. Because this acts like a useEffect, when
// the ReactNodes are destroyed, so is this listener :)
useFileSystemWatcher(
async (eventType, path) => {
// Prevents a cyclic read / write causing editor problems such as
// misplaced cursor positions.
if (codeManager.writeCausedByAppCheckedInFileTreeFileSystemWatcher) {
codeManager.writeCausedByAppCheckedInFileTreeFileSystemWatcher = false
return
}
/** Disabling this in favor of faster file writes until we fix file writing **/
/* useFileSystemWatcher(
* async (eventType, path) => {
* // Prevents a cyclic read / write causing editor problems such as
* // misplaced cursor positions.
* if (codeManager.writeCausedByAppCheckedInFileTreeFileSystemWatcher) {
* codeManager.writeCausedByAppCheckedInFileTreeFileSystemWatcher = false
* return
* }
if (isCurrentFile && eventType === 'change') {
let code = await window.electron.readFile(path, { encoding: 'utf-8' })
code = normalizeLineEndings(code)
codeManager.updateCodeStateEditor(code)
}
fileSend({ type: 'Refresh' })
},
[fileOrDir.path]
)
* if (isCurrentFile && eventType === 'change') {
* let code = await window.electron.readFile(path, { encoding: 'utf-8' })
* code = normalizeLineEndings(code)
* codeManager.updateCodeStateEditor(code)
* }
* fileSend({ type: 'Refresh' })
* },
* [fileOrDir.path]
* ) */
const showNewTreeEntry =
newTreeEntry !== undefined &&
@ -263,7 +263,7 @@ const FileTreeItem = ({
await codeManager.writeToFile()
// Prevent seeing the model built one piece at a time when changing files
await kclManager.executeCode(true)
await kclManager.executeCode({ zoomToFit: true })
} else {
// Let the lsp servers know we closed a file.
onFileClose(currentFile?.path || null, project?.path || null)

View File

@ -25,7 +25,7 @@ import { useSetupEngineManager } from 'hooks/useSetupEngineManager'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import {
isCursorInSketchCommandRange,
updatePathToNodeFromMap,
updateSketchDetailsNodePaths,
} from 'lang/util'
import {
kclManager,
@ -65,17 +65,32 @@ import {
replaceValueAtNodePath,
sketchOnExtrudedFace,
sketchOnOffsetPlane,
splitPipedProfile,
startSketchOnDefault,
} from 'lang/modifyAst'
import { PathToNode, Program, parse, recast, resultIsOk } from 'lang/wasm'
import { artifactIsPlaneWithPaths, isSingleCursorInPipe } from 'lang/queryAst'
import {
CodeRef,
PathToNode,
Program,
VariableDeclaration,
parse,
recast,
resultIsOk,
} from 'lang/wasm'
import {
artifactIsPlaneWithPaths,
doesSketchPipeNeedSplitting,
getNodeFromPath,
isCursorInFunctionDefinition,
traverse,
} from 'lang/queryAst'
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
import { exportFromEngine } from 'lib/exportFromEngine'
import { Models } from '@kittycad/lib/dist/types/src'
import toast from 'react-hot-toast'
import { useLoaderData, useNavigate, useSearchParams } from 'react-router-dom'
import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls'
import { err, reportRejection, trap } from 'lib/trap'
import { err, reportRejection, trap, reject } from 'lib/trap'
import {
ExportIntent,
EngineConnectionStateType,
@ -86,6 +101,10 @@ import { useFileContext } from 'hooks/useFileContext'
import { uuidv4 } from 'lib/utils'
import { IndexLoaderData } from 'lib/types'
import { Node } from 'wasm-lib/kcl/bindings/Node'
import {
getPathsFromArtifact,
getPlaneFromArtifact,
} from 'lang/std/artifactGraph'
import { promptToEditFlow } from 'lib/promptToEdit'
import { kclEditorActor } from 'machines/kclEditorMachine'
import { commandBarActor } from 'machines/commandBarMachine'
@ -163,10 +182,12 @@ export const ModelingMachineProvider = ({
'enable copilot': () => {
editorManager.setCopilotEnabled(true)
},
'sketch exit execute': ({ context: { store } }) => {
// TODO: Remove this async callback. For some reason eslint wouldn't
// let me disable @typescript-eslint/no-misused-promises for the line.
;(async () => {
// tsc reports this typing as perfectly fine, but eslint is complaining.
// It's actually nonsensical, so I'm quieting.
// eslint-disable-next-line @typescript-eslint/no-misused-promises
'sketch exit execute': async ({
context: { store },
}): Promise<void> => {
// When cancelling the sketch mode we should disable sketch mode within the engine.
await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
@ -184,6 +205,24 @@ export const ModelingMachineProvider = ({
store.videoElement?.pause()
return kclManager.executeCode().then(() => {
if (engineCommandManager.engineConnection?.idleMode) return
store.videoElement?.play().catch((e) => {
console.warn('Video playing was prevented', e)
})
})
sceneInfra.camControls.syncDirection = 'clientToEngine'
if (cameraProjection.current === 'perspective') {
await sceneInfra.camControls.snapToPerspectiveBeforeHandingBackControlToEngine()
}
sceneInfra.camControls.syncDirection = 'engineToClient'
store.videoElement?.pause()
return kclManager
.executeCode()
.then(() => {
@ -194,7 +233,6 @@ export const ModelingMachineProvider = ({
})
})
.catch(reportRejection)
})().catch(reportRejection)
},
'Set mouse state': assign(({ context, event }) => {
if (event.type !== 'Set mouse state') return {}
@ -254,7 +292,11 @@ export const ModelingMachineProvider = ({
'Set Segment Overlays': assign({
segmentOverlays: ({ context: { segmentOverlays }, event }) => {
if (event.type !== 'Set Segment Overlays') return {}
if (event.data.type === 'set-many') return event.data.overlays
if (event.data.type === 'add-many')
return {
...segmentOverlays,
...event.data.overlays,
}
if (event.data.type === 'set-one')
return {
...segmentOverlays,
@ -287,7 +329,7 @@ export const ModelingMachineProvider = ({
return {
sketchDetails: {
...sketchDetails,
sketchPathToNode: event.data,
sketchEntryNodePath: event.data,
},
}
}),
@ -411,9 +453,17 @@ export const ModelingMachineProvider = ({
selectionRanges: setSelections.selection,
sketchDetails: {
...sketchDetails,
sketchPathToNode:
setSelections.updatedPathToNode ||
sketchDetails?.sketchPathToNode ||
sketchEntryNodePath:
setSelections.updatedSketchEntryNodePath ||
sketchDetails?.sketchEntryNodePath ||
[],
sketchNodePaths:
setSelections.updatedSketchNodePaths ||
sketchDetails?.sketchNodePaths ||
[],
planeNodePath:
setSelections.updatedPlaneNodePath ||
sketchDetails?.planeNodePath ||
[],
},
}
@ -566,7 +616,12 @@ export const ModelingMachineProvider = ({
if (artifactIsPlaneWithPaths(selectionRanges)) {
return true
}
if (!isSingleCursorInPipe(selectionRanges, kclManager.ast))
if (
isCursorInFunctionDefinition(
kclManager.ast,
selectionRanges.graphSelections[0]
)
)
return false
return !!isCursorInSketchCommandRange(
engineCommandManager.artifactGraph,
@ -597,10 +652,32 @@ export const ModelingMachineProvider = ({
// this assumes no changes have been made to the sketch besides what we did when entering the sketch
// i.e. doesn't account for user's adding code themselves, maybe we need store a flag userEditedSinceSketchMode?
const newAst = structuredClone(kclManager.ast)
const varDecIndex = sketchDetails.sketchPathToNode[1][0]
const varDecIndex = sketchDetails.planeNodePath[1][0]
const varDec = getNodeFromPath<VariableDeclaration>(
newAst,
sketchDetails.planeNodePath,
'VariableDeclaration'
)
if (err(varDec)) return reject(new Error('No varDec'))
const variableName = varDec.node.declaration.id.name
let isIdentifierUsed = false
traverse(newAst, {
enter: (node) => {
if (
node.type === 'Identifier' &&
node.name === variableName
) {
isIdentifierUsed = true
}
},
})
if (isIdentifierUsed) return
// remove body item at varDecIndex
newAst.body = newAst.body.filter((_, i) => i !== varDecIndex)
await kclManager.executeAstMock(newAst)
await codeManager.updateEditorWithAstAndWriteToFile(newAst)
}
sceneInfra.setCallbacks({
onClick: () => {},
@ -610,7 +687,7 @@ export const ModelingMachineProvider = ({
}
),
'animate-to-face': fromPromise(async ({ input }) => {
if (!input) return undefined
if (!input) return null
if (input.type === 'extrudeFace' || input.type === 'offsetPlane') {
const sketched =
input.type === 'extrudeFace'
@ -637,7 +714,9 @@ export const ModelingMachineProvider = ({
await letEngineAnimateAndSyncCamAfter(engineCommandManager, id)
sceneInfra.camControls.syncDirection = 'clientToEngine'
return {
sketchPathToNode: pathToNewSketchNode,
sketchEntryNodePath: [],
planeNodePath: pathToNewSketchNode,
sketchNodePaths: [],
zAxis: input.zAxis,
yAxis: input.yAxis,
origin: input.position,
@ -658,7 +737,9 @@ export const ModelingMachineProvider = ({
)
return {
sketchPathToNode: pathToNode,
sketchEntryNodePath: [],
planeNodePath: pathToNode,
sketchNodePaths: [],
zAxis: input.zAxis,
yAxis: input.yAxis,
origin: [0, 0, 0],
@ -667,12 +748,14 @@ export const ModelingMachineProvider = ({
}),
'animate-to-sketch': fromPromise(
async ({ input: { selectionRanges } }) => {
const sourceRange =
selectionRanges.graphSelections[0]?.codeRef?.range
const sketchPathToNode = getNodePathFromSourceRange(
kclManager.ast,
sourceRange
const sketchPathToNode =
selectionRanges.graphSelections[0]?.codeRef?.pathToNode
const plane = getPlaneFromArtifact(
selectionRanges.graphSelections[0].artifact,
engineCommandManager.artifactGraph
)
if (err(plane)) return Promise.reject(plane)
const info = await getSketchOrientationDetails(
sketchPathToNode || []
)
@ -680,8 +763,22 @@ export const ModelingMachineProvider = ({
engineCommandManager,
info?.sketchDetails?.faceId || ''
)
return {
const sketchPaths = getPathsFromArtifact({
artifact: selectionRanges.graphSelections[0].artifact,
sketchPathToNode: sketchPathToNode || [],
})
if (err(sketchPaths)) return Promise.reject(sketchPaths)
let codeRef =
'faceCodeRef' in plane && plane.faceCodeRef
? plane.faceCodeRef
: 'codeRef' in plane && plane.codeRef
? plane.codeRef
: null
if (!codeRef) return Promise.reject(new Error('No plane codeRef'))
return {
sketchEntryNodePath: sketchPathToNode || [],
sketchNodePaths: sketchPaths,
planeNodePath: codeRef.pathToNode,
zAxis: info.sketchDetails.zAxis || null,
yAxis: info.sketchDetails.yAxis || null,
origin: info.sketchDetails.origin.map(
@ -694,7 +791,7 @@ export const ModelingMachineProvider = ({
'Get horizontal info': fromPromise(
async ({ input: { selectionRanges, sketchDetails } }) => {
const { modifiedAst, pathToNodeMap } =
const { modifiedAst, pathToNodeMap, exprInsertIndex } =
await applyConstraintHorzVertDistance({
constraint: 'setHorzDistance',
selectionRanges,
@ -706,13 +803,23 @@ export const ModelingMachineProvider = ({
if (!sketchDetails)
return Promise.reject(new Error('No sketch details'))
const updatedPathToNode = updatePathToNodeFromMap(
sketchDetails.sketchPathToNode,
pathToNodeMap
)
const {
updatedSketchEntryNodePath,
updatedSketchNodePaths,
updatedPlaneNodePath,
} = updateSketchDetailsNodePaths({
sketchEntryNodePath: sketchDetails.sketchEntryNodePath,
sketchNodePaths: sketchDetails.sketchNodePaths,
planeNodePath: sketchDetails.planeNodePath,
exprInsertIndex,
})
const updatedAst =
await sceneEntitiesManager.updateAstAndRejigSketch(
updatedPathToNode,
updatedSketchEntryNodePath,
updatedSketchNodePaths,
updatedPlaneNodePath,
_modifiedAst,
sketchDetails.zAxis,
sketchDetails.yAxis,
@ -733,13 +840,15 @@ export const ModelingMachineProvider = ({
return {
selectionType: 'completeSelection',
selection,
updatedPathToNode,
updatedSketchEntryNodePath,
updatedSketchNodePaths,
updatedPlaneNodePath,
}
}
),
'Get vertical info': fromPromise(
async ({ input: { selectionRanges, sketchDetails } }) => {
const { modifiedAst, pathToNodeMap } =
const { modifiedAst, pathToNodeMap, exprInsertIndex } =
await applyConstraintHorzVertDistance({
constraint: 'setVertDistance',
selectionRanges,
@ -750,13 +859,23 @@ export const ModelingMachineProvider = ({
const _modifiedAst = pResult.program
if (!sketchDetails)
return Promise.reject(new Error('No sketch details'))
const updatedPathToNode = updatePathToNodeFromMap(
sketchDetails.sketchPathToNode,
pathToNodeMap
)
const {
updatedSketchEntryNodePath,
updatedSketchNodePaths,
updatedPlaneNodePath,
} = updateSketchDetailsNodePaths({
sketchEntryNodePath: sketchDetails.sketchEntryNodePath,
sketchNodePaths: sketchDetails.sketchNodePaths,
planeNodePath: sketchDetails.planeNodePath,
exprInsertIndex,
})
const updatedAst =
await sceneEntitiesManager.updateAstAndRejigSketch(
updatedPathToNode,
updatedSketchEntryNodePath,
updatedSketchNodePaths,
updatedPlaneNodePath,
_modifiedAst,
sketchDetails.zAxis,
sketchDetails.yAxis,
@ -777,7 +896,9 @@ export const ModelingMachineProvider = ({
return {
selectionType: 'completeSelection',
selection,
updatedPathToNode,
updatedSketchEntryNodePath,
updatedSketchNodePaths,
updatedPlaneNodePath,
}
}
),
@ -787,7 +908,8 @@ export const ModelingMachineProvider = ({
selectionRanges,
})
if (err(info)) return Promise.reject(info)
const { modifiedAst, pathToNodeMap } = await (info.enabled
const { modifiedAst, pathToNodeMap, exprInsertIndex } =
await (info.enabled
? applyConstraintAngleBetween({
selectionRanges,
})
@ -803,13 +925,23 @@ export const ModelingMachineProvider = ({
if (!sketchDetails)
return Promise.reject(new Error('No sketch details'))
const updatedPathToNode = updatePathToNodeFromMap(
sketchDetails.sketchPathToNode,
pathToNodeMap
)
const {
updatedSketchEntryNodePath,
updatedSketchNodePaths,
updatedPlaneNodePath,
} = updateSketchDetailsNodePaths({
sketchEntryNodePath: sketchDetails.sketchEntryNodePath,
sketchNodePaths: sketchDetails.sketchNodePaths,
planeNodePath: sketchDetails.planeNodePath,
exprInsertIndex,
})
const updatedAst =
await sceneEntitiesManager.updateAstAndRejigSketch(
updatedPathToNode,
updatedSketchEntryNodePath,
updatedSketchNodePaths,
updatedPlaneNodePath,
_modifiedAst,
sketchDetails.zAxis,
sketchDetails.yAxis,
@ -830,7 +962,9 @@ export const ModelingMachineProvider = ({
return {
selectionType: 'completeSelection',
selection,
updatedPathToNode,
updatedSketchEntryNodePath,
updatedSketchNodePaths,
updatedPlaneNodePath,
}
}
),
@ -845,20 +979,30 @@ export const ModelingMachineProvider = ({
length: lengthValue,
})
if (err(constraintResult)) return Promise.reject(constraintResult)
const { modifiedAst, pathToNodeMap } = constraintResult
const { modifiedAst, pathToNodeMap, exprInsertIndex } =
constraintResult
const pResult = parse(recast(modifiedAst))
if (trap(pResult) || !resultIsOk(pResult))
return Promise.reject(new Error('Unexpected compilation error'))
const _modifiedAst = pResult.program
if (!sketchDetails)
return Promise.reject(new Error('No sketch details'))
const updatedPathToNode = updatePathToNodeFromMap(
sketchDetails.sketchPathToNode,
pathToNodeMap
)
const {
updatedSketchEntryNodePath,
updatedSketchNodePaths,
updatedPlaneNodePath,
} = updateSketchDetailsNodePaths({
sketchEntryNodePath: sketchDetails.sketchEntryNodePath,
sketchNodePaths: sketchDetails.sketchNodePaths,
planeNodePath: sketchDetails.planeNodePath,
exprInsertIndex,
})
const updatedAst =
await sceneEntitiesManager.updateAstAndRejigSketch(
updatedPathToNode,
updatedSketchEntryNodePath,
updatedSketchNodePaths,
updatedPlaneNodePath,
_modifiedAst,
sketchDetails.zAxis,
sketchDetails.yAxis,
@ -879,13 +1023,15 @@ export const ModelingMachineProvider = ({
return {
selectionType: 'completeSelection',
selection,
updatedPathToNode,
updatedSketchEntryNodePath,
updatedSketchNodePaths,
updatedPlaneNodePath,
}
}
),
'Get perpendicular distance info': fromPromise(
async ({ input: { selectionRanges, sketchDetails } }) => {
const { modifiedAst, pathToNodeMap } =
const { modifiedAst, pathToNodeMap, exprInsertIndex } =
await applyConstraintIntersect({
selectionRanges,
})
@ -895,13 +1041,22 @@ export const ModelingMachineProvider = ({
const _modifiedAst = pResult.program
if (!sketchDetails)
return Promise.reject(new Error('No sketch details'))
const updatedPathToNode = updatePathToNodeFromMap(
sketchDetails.sketchPathToNode,
pathToNodeMap
)
const {
updatedSketchEntryNodePath,
updatedSketchNodePaths,
updatedPlaneNodePath,
} = updateSketchDetailsNodePaths({
sketchEntryNodePath: sketchDetails.sketchEntryNodePath,
sketchNodePaths: sketchDetails.sketchNodePaths,
planeNodePath: sketchDetails.planeNodePath,
exprInsertIndex,
})
const updatedAst =
await sceneEntitiesManager.updateAstAndRejigSketch(
updatedPathToNode,
updatedSketchEntryNodePath,
updatedSketchNodePaths,
updatedPlaneNodePath,
_modifiedAst,
sketchDetails.zAxis,
sketchDetails.yAxis,
@ -922,13 +1077,15 @@ export const ModelingMachineProvider = ({
return {
selectionType: 'completeSelection',
selection,
updatedPathToNode,
updatedSketchEntryNodePath,
updatedSketchNodePaths,
updatedPlaneNodePath,
}
}
),
'Get ABS X info': fromPromise(
async ({ input: { selectionRanges, sketchDetails } }) => {
const { modifiedAst, pathToNodeMap } =
const { modifiedAst, pathToNodeMap, exprInsertIndex } =
await applyConstraintAbsDistance({
constraint: 'xAbs',
selectionRanges,
@ -939,13 +1096,22 @@ export const ModelingMachineProvider = ({
const _modifiedAst = pResult.program
if (!sketchDetails)
return Promise.reject(new Error('No sketch details'))
const updatedPathToNode = updatePathToNodeFromMap(
sketchDetails.sketchPathToNode,
pathToNodeMap
)
const {
updatedSketchEntryNodePath,
updatedSketchNodePaths,
updatedPlaneNodePath,
} = updateSketchDetailsNodePaths({
sketchEntryNodePath: sketchDetails.sketchEntryNodePath,
sketchNodePaths: sketchDetails.sketchNodePaths,
planeNodePath: sketchDetails.planeNodePath,
exprInsertIndex,
})
const updatedAst =
await sceneEntitiesManager.updateAstAndRejigSketch(
updatedPathToNode,
updatedSketchEntryNodePath,
updatedSketchNodePaths,
updatedPlaneNodePath,
_modifiedAst,
sketchDetails.zAxis,
sketchDetails.yAxis,
@ -966,13 +1132,15 @@ export const ModelingMachineProvider = ({
return {
selectionType: 'completeSelection',
selection,
updatedPathToNode,
updatedSketchEntryNodePath,
updatedSketchNodePaths,
updatedPlaneNodePath,
}
}
),
'Get ABS Y info': fromPromise(
async ({ input: { selectionRanges, sketchDetails } }) => {
const { modifiedAst, pathToNodeMap } =
const { modifiedAst, pathToNodeMap, exprInsertIndex } =
await applyConstraintAbsDistance({
constraint: 'yAbs',
selectionRanges,
@ -983,13 +1151,22 @@ export const ModelingMachineProvider = ({
const _modifiedAst = pResult.program
if (!sketchDetails)
return Promise.reject(new Error('No sketch details'))
const updatedPathToNode = updatePathToNodeFromMap(
sketchDetails.sketchPathToNode,
pathToNodeMap
)
const {
updatedSketchEntryNodePath,
updatedSketchNodePaths,
updatedPlaneNodePath,
} = updateSketchDetailsNodePaths({
sketchEntryNodePath: sketchDetails.sketchEntryNodePath,
sketchNodePaths: sketchDetails.sketchNodePaths,
planeNodePath: sketchDetails.planeNodePath,
exprInsertIndex,
})
const updatedAst =
await sceneEntitiesManager.updateAstAndRejigSketch(
updatedPathToNode,
updatedSketchEntryNodePath,
updatedSketchNodePaths,
updatedPlaneNodePath,
_modifiedAst,
sketchDetails.zAxis,
sketchDetails.yAxis,
@ -1010,7 +1187,9 @@ export const ModelingMachineProvider = ({
return {
selectionType: 'completeSelection',
selection,
updatedPathToNode,
updatedSketchEntryNodePath,
updatedSketchNodePaths,
updatedPlaneNodePath,
}
}
),
@ -1030,9 +1209,11 @@ export const ModelingMachineProvider = ({
let result: {
modifiedAst: Node<Program>
pathToReplaced: PathToNode | null
exprInsertIndex: number
} = {
modifiedAst: parsed,
pathToReplaced: null,
exprInsertIndex: -1,
}
// If the user provided a constant name,
// we need to insert the named constant
@ -1062,6 +1243,7 @@ export const ModelingMachineProvider = ({
result = {
modifiedAst: parseResultAfterInsertion.program,
pathToReplaced: astAfterReplacement.pathToReplaced,
exprInsertIndex: astAfterReplacement.exprInsertIndex,
}
} else if ('valueText' in data.namedValue) {
// If they didn't provide a constant name,
@ -1092,10 +1274,22 @@ export const ModelingMachineProvider = ({
parsed = parsed as Node<Program>
if (!result.pathToReplaced)
return Promise.reject(new Error('No path to replaced node'))
const {
updatedSketchEntryNodePath,
updatedSketchNodePaths,
updatedPlaneNodePath,
} = updateSketchDetailsNodePaths({
sketchEntryNodePath: sketchDetails.sketchEntryNodePath,
sketchNodePaths: sketchDetails.sketchNodePaths,
planeNodePath: sketchDetails.planeNodePath,
exprInsertIndex: result.exprInsertIndex,
})
const updatedAst =
await sceneEntitiesManager.updateAstAndRejigSketch(
result.pathToReplaced || [],
updatedSketchEntryNodePath,
updatedSketchNodePaths,
updatedPlaneNodePath,
parsed,
sketchDetails.zAxis,
sketchDetails.yAxis,
@ -1116,7 +1310,168 @@ export const ModelingMachineProvider = ({
return {
selectionType: 'completeSelection',
selection,
updatedPathToNode: result.pathToReplaced,
updatedSketchEntryNodePath,
updatedSketchNodePaths,
updatedPlaneNodePath,
}
}
),
'set-up-draft-circle': fromPromise(
async ({ input: { sketchDetails, data } }) => {
if (!sketchDetails || !data)
return reject('No sketch details or data')
sceneEntitiesManager.tearDownSketch({ removeAxis: false })
const result = await sceneEntitiesManager.setupDraftCircle(
sketchDetails.sketchEntryNodePath,
sketchDetails.sketchNodePaths,
sketchDetails.planeNodePath,
sketchDetails.zAxis,
sketchDetails.yAxis,
sketchDetails.origin,
data
)
if (err(result)) return reject(result)
await codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
return result
}
),
'set-up-draft-circle-three-point': fromPromise(
async ({ input: { sketchDetails, data } }) => {
if (!sketchDetails || !data)
return reject('No sketch details or data')
sceneEntitiesManager.tearDownSketch({ removeAxis: false })
const result =
await sceneEntitiesManager.setupDraftCircleThreePoint(
sketchDetails.sketchEntryNodePath,
sketchDetails.sketchNodePaths,
sketchDetails.planeNodePath,
sketchDetails.zAxis,
sketchDetails.yAxis,
sketchDetails.origin,
data.p1,
data.p2
)
if (err(result)) return reject(result)
await codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
return result
}
),
'set-up-draft-rectangle': fromPromise(
async ({ input: { sketchDetails, data } }) => {
if (!sketchDetails || !data)
return reject('No sketch details or data')
sceneEntitiesManager.tearDownSketch({ removeAxis: false })
const result = await sceneEntitiesManager.setupDraftRectangle(
sketchDetails.sketchEntryNodePath,
sketchDetails.sketchNodePaths,
sketchDetails.planeNodePath,
sketchDetails.zAxis,
sketchDetails.yAxis,
sketchDetails.origin,
data
)
if (err(result)) return reject(result)
await codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
return result
}
),
'set-up-draft-center-rectangle': fromPromise(
async ({ input: { sketchDetails, data } }) => {
if (!sketchDetails || !data)
return reject('No sketch details or data')
sceneEntitiesManager.tearDownSketch({ removeAxis: false })
const result = await sceneEntitiesManager.setupDraftCenterRectangle(
sketchDetails.sketchEntryNodePath,
sketchDetails.sketchNodePaths,
sketchDetails.planeNodePath,
sketchDetails.zAxis,
sketchDetails.yAxis,
sketchDetails.origin,
data
)
if (err(result)) return reject(result)
await codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
return result
}
),
'setup-client-side-sketch-segments': fromPromise(
async ({ input: { sketchDetails, selectionRanges } }) => {
if (!sketchDetails) return
if (!sketchDetails.sketchEntryNodePath.length) return
if (Object.keys(sceneEntitiesManager.activeSegments).length > 0) {
sceneEntitiesManager.tearDownSketch({ removeAxis: false })
}
sceneInfra.resetMouseListeners()
await sceneEntitiesManager.setupSketch({
sketchEntryNodePath: sketchDetails?.sketchEntryNodePath || [],
sketchNodePaths: sketchDetails.sketchNodePaths,
forward: sketchDetails.zAxis,
up: sketchDetails.yAxis,
position: sketchDetails.origin,
maybeModdedAst: kclManager.ast,
selectionRanges,
})
sceneInfra.resetMouseListeners()
sceneEntitiesManager.setupSketchIdleCallbacks({
sketchEntryNodePath: sketchDetails?.sketchEntryNodePath || [],
forward: sketchDetails.zAxis,
up: sketchDetails.yAxis,
position: sketchDetails.origin,
sketchNodePaths: sketchDetails.sketchNodePaths,
planeNodePath: sketchDetails.planeNodePath,
// We will want to pass sketchTools here
// to add their interactions
})
// We will want to update the context with sketchTools.
// They'll be used for their .destroy() in tearDownSketch
return undefined
}
),
'split-sketch-pipe-if-needed': fromPromise(
async ({ input: { sketchDetails } }) => {
if (!sketchDetails) return reject('No sketch details')
const existingSketchInfoNoOp = {
updatedEntryNodePath: sketchDetails.sketchEntryNodePath,
updatedSketchNodePaths: sketchDetails.sketchNodePaths,
updatedPlaneNodePath: sketchDetails.planeNodePath,
} 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

@ -187,7 +187,7 @@ export const SettingsAuthProviderBase = ({
) {
// Unit changes requires a re-exec of code
// eslint-disable-next-line @typescript-eslint/no-floating-promises
kclManager.executeCode(true)
kclManager.executeCode({ zoomToFit: true })
} else {
// For any future logging we'd like to do
// console.log(

View File

@ -2,7 +2,12 @@ import { SVGProps } from 'react'
export const Spinner = (props: SVGProps<SVGSVGElement>) => {
return (
<svg viewBox="0 0 10 10" className={'w-8 h-8'} {...props}>
<svg
data-testid="spinner"
viewBox="0 0 10 10"
className={'w-8 h-8'}
{...props}
>
<circle
cx="5"
cy="5"

View File

@ -60,7 +60,7 @@ export const Stream = () => {
*/
function executeCodeAndPlayStream() {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
kclManager.executeCode(true).then(async () => {
kclManager.executeCode({ zoomToFit: true }).then(async () => {
await videoRef.current?.play().catch((e) => {
console.warn('Video playing was prevented', e, videoRef.current)
})

View File

@ -136,6 +136,7 @@ export async function applyConstraintIntersect({
}): Promise<{
modifiedAst: Node<Program>
pathToNodeMap: PathToNodeMap
exprInsertIndex: number
}> {
const info = intersectInfo({
selectionRanges,
@ -174,6 +175,7 @@ export async function applyConstraintIntersect({
return {
modifiedAst,
pathToNodeMap,
exprInsertIndex: -1,
}
}
// transform again but forcing certain values
@ -192,6 +194,7 @@ export async function applyConstraintIntersect({
const { modifiedAst: _modifiedAst, pathToNodeMap: _pathToNodeMap } =
transform2
let exprInsertIndex = -1
if (variableName) {
const newBody = [..._modifiedAst.body]
newBody.splice(
@ -204,9 +207,11 @@ export async function applyConstraintIntersect({
const index = pathToNode.findIndex((a) => a[0] === 'body') + 1
pathToNode[index][0] = Number(pathToNode[index][0]) + 1
})
exprInsertIndex = newVariableInsertIndex
}
return {
modifiedAst: _modifiedAst,
pathToNodeMap: _pathToNodeMap,
exprInsertIndex,
}
}

View File

@ -28,7 +28,7 @@ export function removeConstrainingValuesInfo({
| Error {
const _nodes = selectionRanges.graphSelections.map(({ codeRef }) => {
const tmp = getNodeFromPath<Expr>(kclManager.ast, codeRef.pathToNode)
if (err(tmp)) return tmp
if (tmp instanceof Error) return tmp
return tmp.node
})
const _err1 = _nodes.find(err)

View File

@ -92,6 +92,7 @@ export async function applyConstraintAbsDistance({
}): Promise<{
modifiedAst: Program
pathToNodeMap: PathToNodeMap
exprInsertIndex: number
}> {
const info = absDistanceInfo({
selectionRanges,
@ -131,6 +132,7 @@ export async function applyConstraintAbsDistance({
if (err(transform2)) return Promise.reject(transform2)
const { modifiedAst: _modifiedAst, pathToNodeMap } = transform2
let exprInsertIndex = -1
if (variableName) {
const newBody = [..._modifiedAst.body]
newBody.splice(
@ -143,8 +145,9 @@ export async function applyConstraintAbsDistance({
const index = pathToNode.findIndex((a) => a[0] === 'body') + 1
pathToNode[index][0] = Number(pathToNode[index][0]) + 1
})
exprInsertIndex = newVariableInsertIndex
}
return { modifiedAst: _modifiedAst, pathToNodeMap }
return { modifiedAst: _modifiedAst, pathToNodeMap, exprInsertIndex }
}
export function applyConstraintAxisAlign({

View File

@ -86,6 +86,7 @@ export async function applyConstraintAngleBetween({
}): Promise<{
modifiedAst: Program
pathToNodeMap: PathToNodeMap
exprInsertIndex: number
}> {
const info = angleBetweenInfo({ selectionRanges })
if (err(info)) return Promise.reject(info)
@ -122,6 +123,7 @@ export async function applyConstraintAngleBetween({
return {
modifiedAst,
pathToNodeMap,
exprInsertIndex: -1,
}
}
@ -141,6 +143,7 @@ export async function applyConstraintAngleBetween({
const { modifiedAst: _modifiedAst, pathToNodeMap: _pathToNodeMap } =
transformed2
let exprInsertIndex = -1
if (variableName) {
const newBody = [..._modifiedAst.body]
newBody.splice(
@ -153,9 +156,11 @@ export async function applyConstraintAngleBetween({
const index = pathToNode.findIndex((a) => a[0] === 'body') + 1
pathToNode[index][0] = Number(pathToNode[index][0]) + 1
})
exprInsertIndex = newVariableInsertIndex
}
return {
modifiedAst: _modifiedAst,
pathToNodeMap: _pathToNodeMap,
exprInsertIndex,
}
}

View File

@ -87,15 +87,13 @@ export function horzVertDistanceInfo({
export async function applyConstraintHorzVertDistance({
selectionRanges,
constraint,
// TODO align will always be false (covered by synconous applyConstraintHorzVertAlign), remove it
isAlign = false,
}: {
selectionRanges: Selections
constraint: 'setHorzDistance' | 'setVertDistance'
isAlign?: false
}): Promise<{
modifiedAst: Program
pathToNodeMap: PathToNodeMap
exprInsertIndex: number
}> {
const info = horzVertDistanceInfo({
selectionRanges: selectionRanges,
@ -133,13 +131,12 @@ export async function applyConstraintHorzVertDistance({
return {
modifiedAst,
pathToNodeMap,
exprInsertIndex: -1,
}
} else {
if (!isExprBinaryPart(valueNode))
return Promise.reject('Invalid valueNode, is not a BinaryPart')
let finalValue = isAlign
? createLiteral(0)
: removeDoubleNegatives(valueNode, sign, variableName)
let finalValue = removeDoubleNegatives(valueNode, sign, variableName)
// transform again but forcing certain values
const transformed = transformSecondarySketchLinesTagFirst({
ast: kclManager.ast,
@ -152,6 +149,7 @@ export async function applyConstraintHorzVertDistance({
if (err(transformed)) return Promise.reject(transformed)
const { modifiedAst: _modifiedAst, pathToNodeMap } = transformed
let exprInsertIndex = -1
if (variableName) {
const newBody = [..._modifiedAst.body]
newBody.splice(
@ -164,10 +162,12 @@ export async function applyConstraintHorzVertDistance({
const index = pathToNode.findIndex((a) => a[0] === 'body') + 1
pathToNode[index][0] = Number(pathToNode[index][0]) + 1
})
exprInsertIndex = newVariableInsertIndex
}
return {
modifiedAst: _modifiedAst,
pathToNodeMap,
exprInsertIndex,
}
}
}

View File

@ -74,10 +74,14 @@ export async function applyConstraintLength({
}: {
length: KclCommandValue
selectionRanges: Selections
}) {
}): Promise<{
modifiedAst: Program
pathToNodeMap: PathToNodeMap
exprInsertIndex: number
}> {
const ast = kclManager.ast
const angleLength = angleLengthInfo({ selectionRanges })
if (err(angleLength)) return angleLength
if (err(angleLength)) return Promise.reject(angleLength)
const { transforms } = angleLength
let distanceExpression: Expr = length.valueAst
@ -98,7 +102,7 @@ export async function applyConstraintLength({
}
if (!isExprBinaryPart(distanceExpression)) {
return new Error('Invalid valueNode, is not a BinaryPart')
return Promise.reject('Invalid valueNode, is not a BinaryPart')
}
const retval = transformAstSketchLines({
@ -116,6 +120,12 @@ export async function applyConstraintLength({
return {
modifiedAst: _modifiedAst,
pathToNodeMap,
exprInsertIndex:
'variableName' in length &&
length.variableName &&
length.insertIndex !== undefined
? length.insertIndex
: -1,
}
}
@ -128,6 +138,7 @@ export async function applyConstraintAngleLength({
}): Promise<{
modifiedAst: Program
pathToNodeMap: PathToNodeMap
exprInsertIndex: number
}> {
const angleLength = angleLengthInfo({ selectionRanges, angleOrLength })
if (err(angleLength)) return Promise.reject(angleLength)
@ -212,5 +223,6 @@ export async function applyConstraintAngleLength({
return {
modifiedAst: _modifiedAst,
pathToNodeMap,
exprInsertIndex: variableName ? newVariableInsertIndex : -1,
}
}

View File

@ -39,6 +39,7 @@ import { Operation } from 'wasm-lib/kcl/bindings/Operation'
interface ExecuteArgs {
ast?: Node<Program>
zoomToFit?: boolean
isPartialExecution?: boolean
executionId?: number
zoomOnRangeAndType?: {
range: SourceRange
@ -379,12 +380,10 @@ export class KclManager {
}
this.ast = { ...ast }
// updateArtifactGraph relies on updated executeState/programMemory
this.engineCommandManager.updateArtifactGraph(execState.artifactGraph)
await this.engineCommandManager.updateArtifactGraph(execState.artifactGraph)
this._executeCallback()
if (!isInterrupted) {
if (!isInterrupted)
sceneInfra.modelingSend({ type: 'code edit during sketch' })
}
this.engineCommandManager.addCommandLog({
type: 'execution-done',
data: null,
@ -444,6 +443,7 @@ export class KclManager {
this._logs = logs
this.addDiagnostics(kclErrorsToDiagnostics(errors))
this._execState = execState
this._programMemory = execState.memory
if (!errors.length) {
@ -484,7 +484,10 @@ export class KclManager {
this._cancelTokens.set(key, true)
})
}
async executeCode(zoomToFit?: boolean): Promise<void> {
async executeCode(opts?: {
zoomToFit?: true
isPartialExecution?: true
}): Promise<void> {
const ast = await this.safeParse(codeManager.code)
if (!ast) {
@ -492,10 +495,10 @@ export class KclManager {
return
}
zoomToFit = this.tryToZoomToFitOnCodeUpdate(ast, zoomToFit)
// zoomToFit = this.tryToZoomToFitOnCodeUpdate(ast, opts?.zoomToFit)
this.ast = { ...ast }
return this.executeAst({ zoomToFit })
return this.executeAst(opts)
}
/**
* This will override the zoom to fit to zoom into the model if the previous AST was empty.

View File

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

View File

@ -27,6 +27,7 @@ export type ToolTip =
| 'angledLineThatIntersects'
| 'tangentialArcTo'
| 'circle'
| 'circleThreePoint'
export const toolTips: Array<ToolTip> = [
'line',
@ -42,6 +43,7 @@ export const toolTips: Array<ToolTip> = [
'yLineTo',
'angledLineThatIntersects',
'tangentialArcTo',
'circleThreePoint',
]
export async function executeAst({
@ -69,7 +71,6 @@ export async function executeAst({
: executor(ast, engineCommandManager, path))
await engineCommandManager.waitForAllCommands()
return {
logs: [],
errors: [],

View File

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

View File

@ -22,6 +22,7 @@ import {
SourceRange,
sketchFromKclValue,
isPathToNodeNumber,
parse,
formatNumber,
} from './wasm'
import {
@ -31,6 +32,8 @@ import {
getNodeFromPath,
isNodeSafeToReplace,
traverse,
getBodyIndex,
isCallExprWithName,
ARG_INDEX_FIELD,
LABELED_ARG_FIELD,
} from './queryAst'
@ -56,6 +59,8 @@ import { Models } from '@kittycad/lib'
import { ExtrudeFacePlane } from 'machines/modelingMachine'
import { Node } from 'wasm-lib/kcl/bindings/Node'
import { KclExpressionWithVariable } from 'lib/commandTypes'
import { Artifact, getPathsFromArtifact } from './std/artifactGraph'
import { BodyItem } from 'wasm-lib/kcl/bindings/BodyItem'
import { findKwArg } from './util'
import { deleteEdgeTreatment } from './modifyAst/addEdgeTreatment'
@ -90,41 +95,54 @@ export function startSketchOnDefault(
}
}
export function addStartProfileAt(
export function insertNewStartProfileAt(
node: Node<Program>,
pathToNode: PathToNode,
at: [number, number]
): { modifiedAst: Node<Program>; pathToNode: PathToNode } | Error {
const _node1 = getNodeFromPath<VariableDeclaration>(
node,
pathToNode,
'VariableDeclaration'
)
if (err(_node1)) return _node1
const variableDeclaration = _node1.node
if (variableDeclaration.type !== 'VariableDeclaration') {
return new Error('variableDeclaration.init.type !== PipeExpression')
sketchEntryNodePath: PathToNode,
sketchNodePaths: PathToNode[],
planeNodePath: PathToNode,
at: [number, number],
insertType: 'start' | 'end' = 'end'
):
| {
modifiedAst: Node<Program>
updatedSketchNodePaths: PathToNode[]
updatedEntryNodePath: PathToNode
}
const _node = { ...node }
const init = variableDeclaration.declaration.init
const startProfileAt = createCallExpressionStdLib('startProfileAt', [
| Error {
const varDec = getNodeFromPath<VariableDeclarator>(
node,
planeNodePath,
'VariableDeclarator'
)
if (err(varDec)) return varDec
if (varDec.node.type !== 'VariableDeclarator') return new Error('not a var')
const newExpression = createVariableDeclaration(
findUniqueName(node, 'profile'),
createCallExpressionStdLib('startProfileAt', [
createArrayExpression([
createLiteral(roundOff(at[0])),
createLiteral(roundOff(at[1])),
]),
createPipeSubstitution(),
createIdentifier(varDec.node.id.name),
])
if (init.type === 'PipeExpression') {
init.body.splice(1, 0, startProfileAt)
} else {
variableDeclaration.declaration.init = createPipeExpression([
init,
startProfileAt,
])
}
)
const insertIndex = getInsertIndex(sketchNodePaths, planeNodePath, insertType)
const _node = structuredClone(node)
// TODO the rest of this function will not be robust to work for sketches defined within a function declaration
_node.body.splice(insertIndex, 0, newExpression)
const { updatedEntryNodePath, updatedSketchNodePaths } =
updateSketchNodePathsWithInsertIndex({
insertIndex,
insertType,
sketchNodePaths,
})
return {
modifiedAst: _node,
pathToNode,
updatedSketchNodePaths,
updatedEntryNodePath,
}
}
@ -224,8 +242,21 @@ export function mutateKwArg(
for (let i = 0; i < node.arguments.length; i++) {
const arg = node.arguments[i]
if (arg.label.name === label) {
if (isLiteralArrayOrStatic(val) && isLiteralArrayOrStatic(arg.arg)) {
node.arguments[i].arg = val
return true
} else if (
arg.arg.type === 'ArrayExpression' &&
val.type === 'ArrayExpression'
) {
const arrExp = arg.arg
arrExp.elements.forEach((element, i) => {
if (isLiteralArrayOrStatic(element)) {
arrExp.elements[i] = val.elements[i]
}
})
return true
}
}
}
node.arguments.push(createLabeledArg(label, val))
@ -287,15 +318,16 @@ export function mutateObjExpProp(
export function extrudeSketch({
node,
pathToNode,
shouldPipe = false,
distance = createLiteral(4),
extrudeName,
artifact
}: {
node: Node<Program>
pathToNode: PathToNode
shouldPipe?: boolean
distance: Expr
extrudeName?: string
artifact?: Artifact,
}):
| {
modifiedAst: Node<Program>
@ -303,10 +335,14 @@ export function extrudeSketch({
pathToExtrudeArg: PathToNode
}
| Error {
const orderedSketchNodePaths = getPathsFromArtifact({
artifact: artifact,
sketchPathToNode: pathToNode,
})
if (err(orderedSketchNodePaths)) return orderedSketchNodePaths
const _node = structuredClone(node)
const _node1 = getNodeFromPath(_node, pathToNode)
if (err(_node1)) return _node1
const { node: sketchExpression } = _node1
// determine if sketchExpression is in a pipeExpression or not
const _node2 = getNodeFromPath<PipeExpression>(
@ -315,9 +351,6 @@ export function extrudeSketch({
'PipeExpression'
)
if (err(_node2)) return _node2
const { node: pipeExpression } = _node2
const isInPipeExpression = pipeExpression.type === 'PipeExpression'
const _node3 = getNodeFromPath<VariableDeclarator>(
_node,
@ -325,54 +358,27 @@ export function extrudeSketch({
'VariableDeclarator'
)
if (err(_node3)) return _node3
const { node: variableDeclarator, shallowPath: pathToDecleration } = _node3
const { node: variableDeclarator } = _node3
const sketchToExtrude = shouldPipe
? createPipeSubstitution()
: createIdentifier(variableDeclarator.id.name)
const extrudeCall = createCallExpressionStdLibKw('extrude', sketchToExtrude, [
createLabeledArg('length', distance),
])
const extrudeCall = createCallExpressionStdLibKw(
'extrude',
createIdentifier(variableDeclarator.id.name),
[createLabeledArg('length', distance)]
)
// index of the 'length' arg above. If you reorder the labeled args above,
// make sure to update this too.
const argIndex = 0
if (shouldPipe) {
const pipeChain = createPipeExpression(
isInPipeExpression
? [...pipeExpression.body, extrudeCall]
: [sketchExpression as any, extrudeCall]
)
variableDeclarator.init = pipeChain
const pathToExtrudeArg: PathToNode = [
...pathToDecleration,
['init', 'VariableDeclarator'],
['body', ''],
[pipeChain.body.length - 1, 'index'],
['arguments', 'CallExpressionKw'],
[argIndex, ARG_INDEX_FIELD],
['arg', LABELED_ARG_FIELD],
]
return {
modifiedAst: _node,
pathToNode,
pathToExtrudeArg,
}
}
// We're not creating a pipe expression,
// but rather a separate constant for the extrusion
const name =
extrudeName ?? findUniqueName(node, KCL_DEFAULT_CONSTANT_PREFIXES.EXTRUDE)
const VariableDeclaration = createVariableDeclaration(name, extrudeCall)
const sketchIndexInPathToNode =
pathToDecleration.findIndex((a) => a[0] === 'body') + 1
const sketchIndexInBody = pathToDecleration[
sketchIndexInPathToNode
][0] as number
const lastSketchNodePath =
orderedSketchNodePaths[orderedSketchNodePaths.length - 1]
const sketchIndexInBody = Number(lastSketchNodePath[1][0])
_node.body.splice(sketchIndexInBody + 1, 0, VariableDeclaration)
const pathToExtrudeArg: PathToNode = [
@ -1496,13 +1502,21 @@ export async function deleteFromSelection(
const pipeBody = varDec.node.init.body
if (
pipeBody[0].type === 'CallExpression' &&
pipeBody[0].callee.name === 'startSketchOn'
(pipeBody[0].callee.name === 'startSketchOn' ||
pipeBody[0].callee.name === 'startProfileAt')
) {
// remove varDec
const varDecIndex = varDec.shallowPath[1][0] as number
astClone.body.splice(varDecIndex, 1)
return astClone
}
} else if (
varDec.node.init.type === 'CallExpressionKw' &&
varDec.node.init.callee.name === 'circleThreePoint'
) {
const varDecIndex = varDec.shallowPath[1][0] as number
astClone.body.splice(varDecIndex, 1)
return astClone
}
return new Error('Selection not recognised, could not delete')
@ -1512,6 +1526,167 @@ const nonCodeMetaEmpty = () => {
return { nonCodeNodes: {}, startNodes: [], start: 0, end: 0 }
}
export const createLabeledArg = (name: string, arg: Expr): LabeledArg => {
return { label: createIdentifier(name), arg, type: 'LabeledArg' }
export function getInsertIndex(
sketchNodePaths: PathToNode[],
planeNodePath: PathToNode,
insertType: 'start' | 'end'
) {
let minIndex = 0
let maxIndex = 0
for (const path of sketchNodePaths) {
const index = Number(path[1][0])
if (index < minIndex) minIndex = index
if (index > maxIndex) maxIndex = index
}
const insertIndex = !sketchNodePaths.length
? Number(planeNodePath[1][0]) + 1
: insertType === 'start'
? minIndex
: maxIndex + 1
return insertIndex
}
export function updateSketchNodePathsWithInsertIndex({
insertIndex,
insertType,
sketchNodePaths,
}: {
insertIndex: number
insertType: 'start' | 'end'
sketchNodePaths: PathToNode[]
}): {
updatedEntryNodePath: PathToNode
updatedSketchNodePaths: PathToNode[]
} {
// TODO the rest of this function will not be robust to work for sketches defined within a function declaration
const newExpressionPathToNode: PathToNode = [
['body', ''],
[insertIndex, 'index'],
['declaration', 'VariableDeclaration'],
['init', 'VariableDeclarator'],
]
let updatedSketchNodePaths = structuredClone(sketchNodePaths)
if (insertType === 'start') {
updatedSketchNodePaths = updatedSketchNodePaths.map((path) => {
path[1][0] = Number(path[1][0]) + 1
return path
})
updatedSketchNodePaths.unshift(newExpressionPathToNode)
} else {
updatedSketchNodePaths.push(newExpressionPathToNode)
}
return {
updatedSketchNodePaths,
updatedEntryNodePath: newExpressionPathToNode,
}
}
/**
*
* Split the following pipe expression into
* ```ts
* part001 = startSketchOn('XZ')
|> startProfileAt([1, 2], %)
|> line([3, 4], %)
|> line([5, 6], %)
|> close(%)
extrude001 = extrude(5, part001)
```
into
```ts
sketch001 = startSketchOn('XZ')
part001 = startProfileAt([1, 2], sketch001)
|> line([3, 4], %)
|> line([5, 6], %)
|> close(%)
extrude001 = extrude(5, part001)
```
Notice that the `startSketchOn` is what gets the new variable name, this is so part001 still has the same data as before
making it safe for later code that uses part001 (the extrude in this example)
*
*/
export function splitPipedProfile(
ast: Program,
pathToPipe: PathToNode
):
| {
modifiedAst: Program
pathToProfile: PathToNode
pathToPlane: PathToNode
}
| Error {
const _ast = structuredClone(ast)
const varDec = getNodeFromPath<VariableDeclaration>(
_ast,
pathToPipe,
'VariableDeclaration'
)
if (err(varDec)) return varDec
if (
varDec.node.type !== 'VariableDeclaration' ||
varDec.node.declaration.init.type !== 'PipeExpression'
) {
return new Error('pathToNode does not point to pipe')
}
const init = varDec.node.declaration.init
const firstCall = init.body[0]
if (!isCallExprWithName(firstCall, 'startSketchOn'))
return new Error('First call is not startSketchOn')
const secondCall = init.body[1]
if (!isCallExprWithName(secondCall, 'startProfileAt'))
return new Error('Second call is not startProfileAt')
const varName = varDec.node.declaration.id.name
const newVarName = findUniqueName(_ast, 'sketch')
const secondCallArgs = structuredClone(secondCall.arguments)
secondCallArgs[1] = createIdentifier(newVarName)
const firstCallOfNewPipe = createCallExpression(
'startProfileAt',
secondCallArgs
)
const newSketch = createVariableDeclaration(
newVarName,
varDec.node.declaration.init.body[0]
)
const newProfile = createVariableDeclaration(
varName,
varDec.node.declaration.init.body.length <= 2
? firstCallOfNewPipe
: createPipeExpression([
firstCallOfNewPipe,
...varDec.node.declaration.init.body.slice(2),
])
)
const index = getBodyIndex(pathToPipe)
if (err(index)) return index
_ast.body.splice(index, 1, newSketch, newProfile)
const pathToPlane = structuredClone(pathToPipe)
const pathToProfile = structuredClone(pathToPipe)
pathToProfile[1][0] = index + 1
return {
modifiedAst: _ast,
pathToProfile,
pathToPlane,
}
}
export function createNodeFromExprSnippet(
strings: TemplateStringsArray,
...expressions: any[]
): Node<BodyItem> | Error {
const code = strings.reduce(
(acc, str, i) => acc + str + (expressions[i] || ''),
''
)
let program = parse(code)
if (err(program)) return program
const node = program.program?.body[0]
if (!node) return new Error('No node found')
return node
}
export const createLabeledArg = (label: string, arg: Expr): LabeledArg => {
return { label: createIdentifier(label), arg, type: 'LabeledArg' }
}

View File

@ -5,7 +5,6 @@ import {
PathToNode,
Expr,
CallExpression,
PipeExpression,
VariableDeclarator,
CallExpressionKw,
} from 'lang/wasm'
@ -16,7 +15,6 @@ import {
createCallExpressionStdLib,
createObjectExpression,
createIdentifier,
createPipeExpression,
findUniqueName,
createVariableDeclaration,
} from 'lang/modifyAst'
@ -26,14 +24,15 @@ import {
mutateAstWithTagForSketchSegment,
getEdgeTagCall,
} from 'lang/modifyAst/addEdgeTreatment'
import { Artifact, getPathsFromArtifact } from 'lang/std/artifactGraph'
export function revolveSketch(
ast: Node<Program>,
pathToSketchNode: PathToNode,
shouldPipe = false,
angle: Expr = createLiteral(4),
axisOrEdge: string,
axis: string,
edge: Selections
edge: Selections,
artifact?: Artifact
):
| {
modifiedAst: Node<Program>
@ -41,6 +40,11 @@ export function revolveSketch(
pathToRevolveArg: PathToNode
}
| Error {
const orderedSketchNodePaths = getPathsFromArtifact({
artifact: artifact,
sketchPathToNode: pathToSketchNode,
})
if (err(orderedSketchNodePaths)) return orderedSketchNodePaths
const clonedAst = structuredClone(ast)
const sketchNode = getNodeFromPath(clonedAst, pathToSketchNode)
if (err(sketchNode)) return sketchNode
@ -74,29 +78,13 @@ export function revolveSketch(
generatedAxis = createLiteral(axis)
}
/* Original Code */
const { node: sketchExpression } = sketchNode
// determine if sketchExpression is in a pipeExpression or not
const sketchPipeExpressionNode = getNodeFromPath<PipeExpression>(
clonedAst,
pathToSketchNode,
'PipeExpression'
)
if (err(sketchPipeExpressionNode)) return sketchPipeExpressionNode
const { node: sketchPipeExpression } = sketchPipeExpressionNode
const isInPipeExpression = sketchPipeExpression.type === 'PipeExpression'
const sketchVariableDeclaratorNode = getNodeFromPath<VariableDeclarator>(
clonedAst,
pathToSketchNode,
'VariableDeclarator'
)
if (err(sketchVariableDeclaratorNode)) return sketchVariableDeclaratorNode
const {
node: sketchVariableDeclarator,
shallowPath: sketchPathToDecleration,
} = sketchVariableDeclaratorNode
const { node: sketchVariableDeclarator } = sketchVariableDeclaratorNode
if (!generatedAxis) return new Error('Generated axis selection is missing.')
@ -108,37 +96,13 @@ export function revolveSketch(
createIdentifier(sketchVariableDeclarator.id.name),
])
if (shouldPipe) {
const pipeChain = createPipeExpression(
isInPipeExpression
? [...sketchPipeExpression.body, revolveCall]
: [sketchExpression as any, revolveCall]
)
sketchVariableDeclarator.init = pipeChain
const pathToRevolveArg: PathToNode = [
...sketchPathToDecleration,
['init', 'VariableDeclarator'],
['body', ''],
[pipeChain.body.length - 1, 'index'],
['arguments', 'CallExpression'],
[0, 'index'],
]
return {
modifiedAst: clonedAst,
pathToSketchNode,
pathToRevolveArg,
}
}
// We're not creating a pipe expression,
// but rather a separate constant for the extrusion
const name = findUniqueName(clonedAst, KCL_DEFAULT_CONSTANT_PREFIXES.REVOLVE)
const VariableDeclaration = createVariableDeclaration(name, revolveCall)
const sketchIndexInPathToNode =
sketchPathToDecleration.findIndex((a) => a[0] === 'body') + 1
const sketchIndexInBody = sketchPathToDecleration[sketchIndexInPathToNode][0]
const lastSketchNodePath =
orderedSketchNodePaths[orderedSketchNodePaths.length - 1]
const sketchIndexInBody = Number(lastSketchNodePath[1][0])
if (typeof sketchIndexInBody !== 'number')
return new Error('expected sketchIndexInBody to be a number')
clonedAst.body.splice(sketchIndexInBody + 1, 0, VariableDeclaration)

View File

@ -2,7 +2,6 @@ import { ToolTip } from 'lang/langHelpers'
import { Selection, Selections } from 'lib/selections'
import {
ArrayExpression,
ArtifactGraph,
BinaryExpression,
CallExpression,
CallExpressionKw,
@ -23,6 +22,7 @@ import {
VariableDeclaration,
VariableDeclarator,
recast,
ArtifactGraph,
} from './wasm'
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
import { createIdentifier, splitPathAtLastIndex } from './modifyAst'
@ -34,10 +34,10 @@ import {
getConstraintType,
} from './std/sketchcombos'
import { err, Reason } from 'lib/trap'
import { ImportStatement } from 'wasm-lib/kcl/bindings/ImportStatement'
import { Node } from 'wasm-lib/kcl/bindings/Node'
import { findKwArg } from './util'
import { codeRefFromRange } from './std/artifactGraph'
import { FunctionExpression } from 'wasm-lib/kcl/bindings/FunctionExpression'
export const LABELED_ARG_FIELD = 'LabeledArg -> Arg'
export const ARG_INDEX_FIELD = 'arg index'
@ -353,7 +353,13 @@ export function findAllPreviousVariables(
type ReplacerFn = (
_ast: Node<Program>,
varName: string
) => { modifiedAst: Node<Program>; pathToReplaced: PathToNode } | Error
) =>
| {
modifiedAst: Node<Program>
pathToReplaced: PathToNode
exprInsertIndex: number
}
| Error
export function isNodeSafeToReplacePath(
ast: Program,
@ -405,7 +411,7 @@ export function isNodeSafeToReplacePath(
if (err(_nodeToReplace)) return _nodeToReplace
const nodeToReplace = _nodeToReplace.node as any
nodeToReplace[last[0]] = identifier
return { modifiedAst: _ast, pathToReplaced }
return { modifiedAst: _ast, pathToReplaced, exprInsertIndex: index }
}
const hasPipeSub = isTypeInValue(finVal as Expr, 'PipeSubstitution')
@ -514,8 +520,15 @@ export function isLinesParallelAndConstrained(
if (err(_primarySegment)) return _primarySegment
const primarySegment = _primarySegment.segment
const _varDec2 = getNodeFromPath(ast, secondaryPath, 'VariableDeclaration')
if (err(_varDec2)) return _varDec2
const varDec2 = _varDec2.node
const varName2 = (varDec2 as VariableDeclaration)?.declaration.id?.name
const sg2 = sketchFromKclValue(programMemory?.get(varName2), varName2)
if (err(sg2)) return sg2
const _segment = getSketchSegmentFromSourceRange(
sg,
sg2,
secondaryLine?.codeRef?.range
)
if (err(_segment)) return _segment
@ -866,3 +879,57 @@ export function getObjExprProperty(
if (index === -1) return null
return { expr: node.properties[index].value, index }
}
export function isCursorInFunctionDefinition(
ast: Node<Program>,
selectionRanges: Selection
): boolean {
if (!selectionRanges?.codeRef?.pathToNode) return false
const node = getNodeFromPath<FunctionExpression>(
ast,
selectionRanges.codeRef.pathToNode,
'FunctionExpression'
)
if (err(node)) return false
if (node.node.type === 'FunctionExpression') return true
return false
}
export function getBodyIndex(pathToNode: PathToNode): number | Error {
const index = Number(pathToNode[1][0])
if (Number.isInteger(index)) return index
return new Error('Expected number index')
}
export function isCallExprWithName(
expr: Expr | CallExpression,
name: string
): expr is CallExpression {
if (expr.type === 'CallExpression' && expr.callee.type === 'Identifier') {
return expr.callee.name === name
}
return false
}
export function doesSketchPipeNeedSplitting(
ast: Node<Program>,
pathToPipe: PathToNode
): boolean | Error {
const varDec = getNodeFromPath<VariableDeclarator>(
ast,
pathToPipe,
'VariableDeclarator'
)
if (err(varDec)) return varDec
if (varDec.node.type !== 'VariableDeclarator') return new Error('Not a var')
const pipeExpression = varDec.node.init
if (pipeExpression.type !== 'PipeExpression') return false
const [firstPipe, secondPipe] = pipeExpression.body
if (!firstPipe || !secondPipe) return false
if (
isCallExprWithName(firstPipe, 'startSketchOn') &&
isCallExprWithName(secondPipe, 'startProfileAt')
)
return true
return false
}

View File

@ -82,6 +82,7 @@ function moreNodePathFromSourceRange(
return moreNodePathFromSourceRange(arg, sourceRange, path)
}
}
return path
}
return path
}

View File

@ -1,4 +1,5 @@
import {
Expr,
Artifact,
ArtifactGraph,
ArtifactId,
@ -18,7 +19,7 @@ import {
import { Models } from '@kittycad/lib'
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
import { err } from 'lib/trap'
import { codeManager } from 'lib/singletons'
import { engineCommandManager, kclManager } from 'lib/singletons'
export type { Artifact, ArtifactId, SegmentArtifact } from 'lang/wasm'
@ -40,7 +41,7 @@ export interface PlaneArtifactRich extends BaseArtifact {
export interface PathArtifactRich extends BaseArtifact {
type: 'path'
/** A path must always lie on a plane */
plane: PlaneArtifact | WallArtifact
plane: PlaneArtifact | WallArtifact | CapArtifact
/** A path must always contain 0 or more segments */
segments: Array<SegmentArtifact>
/** A path may not result in a sweep artifact */
@ -51,7 +52,7 @@ export interface PathArtifactRich extends BaseArtifact {
interface SegmentArtifactRich extends BaseArtifact {
type: 'segment'
path: PathArtifact
surf?: WallArtifact
surf: WallArtifact
edges: Array<SweepEdge>
edgeCut?: EdgeCut
codeRef: CodeRef
@ -239,6 +240,7 @@ export function expandSegment(
if (err(path)) return path
if (err(surf)) return surf
if (err(edgeCut)) return edgeCut
if (!surf) return new Error('Segment does not have a surface')
return {
type: 'segment',
@ -410,6 +412,205 @@ export function codeRefFromRange(range: SourceRange, ast: Program): CodeRef {
}
}
function getPlaneFromPath(
path: PathArtifact,
graph: ArtifactGraph
): PlaneArtifact | WallArtifact | CapArtifact | Error {
const plane = getArtifactOfTypes(
{ key: path.planeId, types: ['plane', 'wall', 'cap'] },
graph
)
if (err(plane)) return plane
return plane
}
function getPlaneFromSegment(
segment: SegmentArtifact,
graph: ArtifactGraph
): PlaneArtifact | WallArtifact | CapArtifact | Error {
const path = getArtifactOfTypes(
{ key: segment.pathId, types: ['path'] },
graph
)
if (err(path)) return path
return getPlaneFromPath(path, graph)
}
function getPlaneFromSolid2D(
solid2D: Solid2D,
graph: ArtifactGraph
): PlaneArtifact | WallArtifact | CapArtifact | Error {
const path = getArtifactOfTypes(
{ key: solid2D.pathId, types: ['path'] },
graph
)
if (err(path)) return path
return getPlaneFromPath(path, graph)
}
function getPlaneFromCap(
cap: CapArtifact,
graph: ArtifactGraph
): PlaneArtifact | WallArtifact | CapArtifact | Error {
const sweep = getArtifactOfTypes(
{ key: cap.sweepId, types: ['sweep'] },
graph
)
if (err(sweep)) return sweep
const path = getArtifactOfTypes({ key: sweep.pathId, types: ['path'] }, graph)
if (err(path)) return path
return getPlaneFromPath(path, graph)
}
function getPlaneFromWall(
wall: WallArtifact,
graph: ArtifactGraph
): PlaneArtifact | WallArtifact | CapArtifact | Error {
const sweep = getArtifactOfTypes(
{ key: wall.sweepId, types: ['sweep'] },
graph
)
if (err(sweep)) return sweep
const path = getArtifactOfTypes({ key: sweep.pathId, types: ['path'] }, graph)
if (err(path)) return path
return getPlaneFromPath(path, graph)
}
function getPlaneFromSweepEdge(edge: SweepEdge, graph: ArtifactGraph) {
const sweep = getArtifactOfTypes(
{ key: edge.sweepId, types: ['sweep'] },
graph
)
if (err(sweep)) return sweep
const path = getArtifactOfTypes({ key: sweep.pathId, types: ['path'] }, graph)
if (err(path)) return path
return getPlaneFromPath(path, graph)
}
export function getPlaneFromArtifact(
artifact: Artifact | undefined,
graph: ArtifactGraph
): PlaneArtifact | WallArtifact | CapArtifact | Error {
if (!artifact) return new Error(`Artifact is undefined`)
if (artifact.type === 'plane') return artifact
if (artifact.type === 'path') return getPlaneFromPath(artifact, graph)
if (artifact.type === 'segment') return getPlaneFromSegment(artifact, graph)
if (artifact.type === 'solid2d') return getPlaneFromSolid2D(artifact, graph)
if (artifact.type === 'cap') return getPlaneFromCap(artifact, graph)
if (artifact.type === 'wall') return getPlaneFromWall(artifact, graph)
if (artifact.type === 'sweepEdge')
return getPlaneFromSweepEdge(artifact, graph)
return new Error(`Artifact type ${artifact.type} does not have a plane`)
}
const isExprSafe = (index: number): boolean => {
const expr = kclManager.ast.body?.[index]
if (!expr) {
return false
}
if (expr.type === 'ImportStatement' || expr.type === 'ReturnStatement') {
return false
}
if (expr.type === 'VariableDeclaration') {
const init = expr.declaration?.init
if (!init) return false
if (init.type === 'CallExpression') {
return false
}
if (init.type === 'BinaryExpression' && isNodeSafe(init)) {
return true
}
if (init.type === 'Literal' || init.type === 'MemberExpression') {
return true
}
}
return false
}
const onlyConsecutivePaths = (
orderedNodePaths: PathToNode[],
originalPath: PathToNode
): PathToNode[] => {
const originalIndex = Number(
orderedNodePaths.find(
(path) => path[1][0] === originalPath[1][0]
)?.[1]?.[0] || 0
)
const minIndex = Number(orderedNodePaths[0][1][0])
const maxIndex = Number(orderedNodePaths[orderedNodePaths.length - 1][1][0])
const pathIndexMap: any = {}
orderedNodePaths.forEach((path) => {
const bodyIndex = Number(path[1][0])
pathIndexMap[bodyIndex] = path
})
const safePaths: PathToNode[] = []
// traverse expressions in either direction from the profile selected
// when the user entered sketch mode
for (let i = originalIndex; i <= maxIndex; i++) {
if (pathIndexMap[i]) {
safePaths.push(pathIndexMap[i])
} else if (!isExprSafe(i)) {
break
}
}
for (let i = originalIndex - 1; i >= minIndex; i--) {
if (pathIndexMap[i]) {
safePaths.unshift(pathIndexMap[i])
} else if (!isExprSafe(i)) {
break
}
}
return safePaths
}
export function getPathsFromPlaneArtifact(planeArtifact: PlaneArtifact) {
const nodePaths: PathToNode[] = []
for (const pathId of planeArtifact.pathIds) {
const path = engineCommandManager.artifactGraph.get(pathId)
if (!path) continue
if ('codeRef' in path && path.codeRef) {
// TODO should figure out why upstream the path is bad
const isNodePathBad = path.codeRef.pathToNode.length < 2
nodePaths.push(
isNodePathBad
? getNodePathFromSourceRange(kclManager.ast, path.codeRef.range)
: path.codeRef.pathToNode
)
}
}
return onlyConsecutivePaths(nodePaths, nodePaths[0])
}
export function getPathsFromArtifact({
sketchPathToNode,
artifact,
}: {
sketchPathToNode: PathToNode
artifact?: Artifact
}): PathToNode[] | Error {
const plane = getPlaneFromArtifact(
artifact,
engineCommandManager.artifactGraph
)
if (err(plane)) return plane
const paths = getArtifactsOfTypes(
{ keys: plane.pathIds, types: ['path'] },
engineCommandManager.artifactGraph
)
let nodePaths = [...paths.values()]
.map((path) => path.codeRef.pathToNode)
.sort((a, b) => Number(a[1][0]) - Number(b[1][0]))
return onlyConsecutivePaths(nodePaths, sketchPathToNode)
}
function isNodeSafe(node: Expr): boolean {
if (node.type === 'Literal' || node.type === 'MemberExpression') {
return true
}
if (node.type === 'BinaryExpression') {
return isNodeSafe(node.left) && isNodeSafe(node.right)
}
return false
}
/**
* Get an artifact from a code source range
*/
@ -418,7 +619,7 @@ export function getArtifactFromRange(
artifactGraph: ArtifactGraph
): Artifact | null {
for (const artifact of artifactGraph.values()) {
if ('codeRef' in artifact) {
if ('codeRef' in artifact && artifact.codeRef) {
const match =
artifact.codeRef?.range[0] === range[0] &&
artifact.codeRef.range[1] === range[1]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 569 KiB

After

Width:  |  Height:  |  Size: 560 KiB

View File

@ -66,7 +66,12 @@ import { perpendicularDistance } from 'sketch-helpers'
import { TagDeclarator } from 'wasm-lib/kcl/bindings/TagDeclarator'
import { EdgeCutInfo } from 'machines/modelingMachine'
import { Node } from 'wasm-lib/kcl/bindings/Node'
import { findKwArg, findKwArgAny, findKwArgAnyIndex } from 'lang/util'
import {
findKwArg,
findKwArgWithIndex,
findKwArgAny,
findKwArgAnyIndex,
} from 'lang/util'
export const ARG_TAG = 'tag'
export const ARG_END = 'end'
@ -76,6 +81,9 @@ const STRAIGHT_SEGMENT_ERR = new Error(
'Invalid input, expected "straight-segment"'
)
const ARC_SEGMENT_ERR = new Error('Invalid input, expected "arc-segment"')
const CIRCLE_THREE_POINT_SEGMENT_ERR = new Error(
'Invalid input, expected "circle-three-point-segment"'
)
export type Coords2d = [number, number]
@ -171,7 +179,8 @@ const commonConstraintInfoHelper = (
}
],
code: string,
pathToNode: PathToNode
pathToNode: PathToNode,
filterValue?: string
) => {
if (callExp.type !== 'CallExpression' && callExp.type !== 'CallExpressionKw')
return []
@ -295,7 +304,8 @@ const horzVertConstraintInfoHelper = (
stdLibFnName: ConstrainInfo['stdLibFnName'],
abbreviatedInput: AbbreviatedInput,
code: string,
pathToNode: PathToNode
pathToNode: PathToNode,
filterValue?: string
) => {
if (callExp.type !== 'CallExpression') return []
const firstArg = callExp.arguments?.[0]
@ -502,13 +512,14 @@ export const lineTo: SketchLineHelperKw = {
}) => {
if (segmentInput.type !== 'straight-segment') return STRAIGHT_SEGMENT_ERR
const to = segmentInput.to
const _node = { ...node }
const _node = structuredClone(node)
const nodeMeta = getNodeFromPath<PipeExpression | CallExpressionKw>(
_node,
pathToNode,
'PipeExpression'
)
if (err(nodeMeta)) return nodeMeta
const { node: pipe } = nodeMeta
const nodeMeta2 = getNodeFromPath<VariableDeclarator>(
_node,
@ -783,11 +794,11 @@ export const xLine: SketchLineHelper = {
add: ({ node, pathToNode, segmentInput, replaceExistingCallback }) => {
if (segmentInput.type !== 'straight-segment') return STRAIGHT_SEGMENT_ERR
const { from, to } = segmentInput
const _node = { ...node }
const _node = structuredClone(node)
const getNode = getNodeFromPathCurry(_node, pathToNode)
const _node1 = getNode<PipeExpression>('PipeExpression')
if (err(_node1)) return _node1
const { node: pipe } = _node1
const varDec = getNode<VariableDeclaration>('VariableDeclaration')
if (err(varDec)) return varDec
const dec = varDec.node.declaration
const newVal = createLiteral(roundOff(to[0] - from[0], 2))
@ -802,7 +813,11 @@ export const xLine: SketchLineHelper = {
])
if (err(result)) return result
const { callExp, valueUsedInTransform } = result
pipe.body[callIndex] = callExp
if (dec.init.type === 'PipeExpression') {
dec.init.body[callIndex] = callExp
} else {
dec.init = callExp
}
return {
modifiedAst: _node,
pathToNode,
@ -814,7 +829,11 @@ export const xLine: SketchLineHelper = {
newVal,
createPipeSubstitution(),
])
pipe.body = [...pipe.body, newLine]
if (dec.init.type === 'PipeExpression') {
dec.init.body = [...dec.init.body, newLine]
} else {
dec.init = createPipeExpression([dec.init, newLine])
}
return { modifiedAst: _node, pathToNode }
},
updateArgs: ({ node, pathToNode, input }) => {
@ -851,11 +870,11 @@ export const yLine: SketchLineHelper = {
add: ({ node, pathToNode, segmentInput, replaceExistingCallback }) => {
if (segmentInput.type !== 'straight-segment') return STRAIGHT_SEGMENT_ERR
const { from, to } = segmentInput
const _node = { ...node }
const _node = structuredClone(node)
const getNode = getNodeFromPathCurry(_node, pathToNode)
const _node1 = getNode<PipeExpression>('PipeExpression')
if (err(_node1)) return _node1
const { node: pipe } = _node1
const varDec = getNode<VariableDeclaration>('VariableDeclaration')
if (err(varDec)) return varDec
const dec = varDec.node.declaration
const newVal = createLiteral(roundOff(to[1] - from[1], 2))
if (replaceExistingCallback) {
const { index: callIndex } = splitPathAtPipeExpression(pathToNode)
@ -868,7 +887,11 @@ export const yLine: SketchLineHelper = {
])
if (err(result)) return result
const { callExp, valueUsedInTransform } = result
pipe.body[callIndex] = callExp
if (dec.init.type === 'PipeExpression') {
dec.init.body[callIndex] = callExp
} else {
dec.init = callExp
}
return {
modifiedAst: _node,
pathToNode,
@ -880,7 +903,11 @@ export const yLine: SketchLineHelper = {
newVal,
createPipeSubstitution(),
])
pipe.body = [...pipe.body, newLine]
if (dec.init.type === 'PipeExpression') {
dec.init.body = [...dec.init.body, newLine]
} else {
dec.init = createPipeExpression([dec.init, newLine])
}
return { modifiedAst: _node, pathToNode }
},
updateArgs: ({ node, pathToNode, input }) => {
@ -1220,6 +1247,295 @@ export const circle: SketchLineHelper = {
]
},
}
export const circleThreePoint: SketchLineHelperKw = {
add: ({ node, pathToNode, segmentInput, replaceExistingCallback }) => {
if (segmentInput.type !== 'circle-three-point-segment') {
return CIRCLE_THREE_POINT_SEGMENT_ERR
}
const { p1, p2, p3 } = segmentInput
const _node = structuredClone(node)
const nodeMeta = getNodeFromPath<VariableDeclaration>(
_node,
pathToNode,
'VariableDeclaration'
)
if (err(nodeMeta)) return nodeMeta
const { node: varDec } = nodeMeta
const createRoundedLiteral = (val: number) =>
createLiteral(roundOff(val, 2))
if (replaceExistingCallback) {
const result = replaceExistingCallback([
{
type: 'arrayInObject',
index: 0,
key: 'p1',
argType: 'xAbsolute',
expr: createRoundedLiteral(p1[0]),
},
{
type: 'arrayInObject',
index: 1,
key: 'p1',
argType: 'yAbsolute',
expr: createRoundedLiteral(p1[1]),
},
{
type: 'arrayInObject',
index: 0,
key: 'p2',
argType: 'xAbsolute',
expr: createRoundedLiteral(p2[0]),
},
{
type: 'arrayInObject',
index: 1,
key: 'p2',
argType: 'yAbsolute',
expr: createRoundedLiteral(p2[1]),
},
{
type: 'arrayInObject',
index: 0,
key: 'p3',
argType: 'xAbsolute',
expr: createRoundedLiteral(p3[0]),
},
{
type: 'arrayInObject',
index: 1,
key: 'p3',
argType: 'yAbsolute',
expr: createRoundedLiteral(p3[1]),
},
])
if (err(result)) return result
const { callExp, valueUsedInTransform } = result
varDec.declaration.init = callExp
return {
modifiedAst: _node,
pathToNode,
valueUsedInTransform,
}
}
return new Error('not implemented')
},
updateArgs: ({ node, pathToNode, input }) => {
if (input.type !== 'circle-three-point-segment') {
return CIRCLE_THREE_POINT_SEGMENT_ERR
}
const { p1, p2, p3 } = input
const _node = { ...node }
const nodeMeta = getNodeFromPath<CallExpressionKw>(_node, pathToNode)
if (err(nodeMeta)) return nodeMeta
const { node: callExpression, shallowPath } = nodeMeta
const createRounded2DPointArr = (point: [number, number]) =>
createArrayExpression([
createLiteral(roundOff(point[0], 2)),
createLiteral(roundOff(point[1], 2)),
])
const newP1 = createRounded2DPointArr(p1)
const newP2 = createRounded2DPointArr(p2)
const newP3 = createRounded2DPointArr(p3)
mutateKwArg('p1', callExpression, newP1)
mutateKwArg('p2', callExpression, newP2)
mutateKwArg('p3', callExpression, newP3)
return {
modifiedAst: _node,
pathToNode: shallowPath,
}
},
getTag: getTagKwArg(),
addTag: addTagKw(),
getConstraintInfo: (callExp, code, pathToNode, filterValue) => {
if (callExp.type !== 'CallExpressionKw') return []
const p1Details = findKwArgWithIndex('p1', callExp)
const p2Details = findKwArgWithIndex('p2', callExp)
const p3Details = findKwArgWithIndex('p3', callExp)
if (!p1Details || !p2Details || !p3Details) return []
if (
p1Details.expr.type !== 'ArrayExpression' ||
p2Details.expr.type !== 'ArrayExpression' ||
p3Details.expr.type !== 'ArrayExpression'
)
return []
const pathToP1ArrayExpression: PathToNode = [
...pathToNode,
['arguments', 'CallExpressionKw'],
[p1Details.argIndex, 'arg index'],
['arg', 'labeledArg -> Arg'],
['elements', 'ArrayExpression'],
]
const pathToP2ArrayExpression: PathToNode = [
...pathToNode,
['arguments', 'CallExpressionKw'],
[p2Details.argIndex, 'arg index'],
['arg', 'labeledArg -> Arg'],
['elements', 'ArrayExpression'],
]
const pathToP3ArrayExpression: PathToNode = [
...pathToNode,
['arguments', 'CallExpressionKw'],
[p3Details.argIndex, 'arg index'],
['arg', 'labeledArg -> Arg'],
['elements', 'ArrayExpression'],
]
const pathToP1XArg: PathToNode = [...pathToP1ArrayExpression, [0, 'index']]
const pathToP1YArg: PathToNode = [...pathToP1ArrayExpression, [1, 'index']]
const pathToP2XArg: PathToNode = [...pathToP2ArrayExpression, [0, 'index']]
const pathToP2YArg: PathToNode = [...pathToP2ArrayExpression, [1, 'index']]
const pathToP3XArg: PathToNode = [...pathToP3ArrayExpression, [0, 'index']]
const pathToP3YArg: PathToNode = [...pathToP3ArrayExpression, [1, 'index']]
const constraints: (ConstrainInfo & { filterValue: string })[] = [
{
stdLibFnName: 'circleThreePoint',
type: 'xAbsolute',
isConstrained: isNotLiteralArrayOrStatic(p1Details.expr.elements[0]),
sourceRange: [
p1Details.expr.elements[0].start,
p1Details.expr.elements[0].end,
0,
],
pathToNode: pathToP1XArg,
value: code.slice(
p1Details.expr.elements[0].start,
p1Details.expr.elements[0].end
),
argPosition: {
type: 'arrayInObject',
index: 0,
key: 'p1',
},
filterValue: 'p1',
},
{
stdLibFnName: 'circleThreePoint',
type: 'yAbsolute',
isConstrained: isNotLiteralArrayOrStatic(p1Details.expr.elements[1]),
sourceRange: [
p1Details.expr.elements[1].start,
p1Details.expr.elements[1].end,
0,
],
pathToNode: pathToP1YArg,
value: code.slice(
p1Details.expr.elements[1].start,
p1Details.expr.elements[1].end
),
argPosition: {
type: 'arrayInObject',
index: 1,
key: 'p1',
},
filterValue: 'p1',
},
{
stdLibFnName: 'circleThreePoint',
type: 'xAbsolute',
isConstrained: isNotLiteralArrayOrStatic(p2Details.expr.elements[0]),
sourceRange: [
p2Details.expr.elements[0].start,
p2Details.expr.elements[0].end,
0,
],
pathToNode: pathToP2XArg,
value: code.slice(
p2Details.expr.elements[0].start,
p2Details.expr.elements[0].end
),
argPosition: {
type: 'arrayInObject',
index: 0,
key: 'p2',
},
filterValue: 'p2',
},
{
stdLibFnName: 'circleThreePoint',
type: 'yAbsolute',
isConstrained: isNotLiteralArrayOrStatic(p2Details.expr.elements[1]),
sourceRange: [
p2Details.expr.elements[1].start,
p2Details.expr.elements[1].end,
0,
],
pathToNode: pathToP2YArg,
value: code.slice(
p2Details.expr.elements[1].start,
p2Details.expr.elements[1].end
),
argPosition: {
type: 'arrayInObject',
index: 1,
key: 'p2',
},
filterValue: 'p2',
},
{
stdLibFnName: 'circleThreePoint',
type: 'xAbsolute',
isConstrained: isNotLiteralArrayOrStatic(p3Details.expr.elements[0]),
sourceRange: [
p3Details.expr.elements[0].start,
p3Details.expr.elements[0].end,
0,
],
pathToNode: pathToP3XArg,
value: code.slice(
p3Details.expr.elements[0].start,
p3Details.expr.elements[0].end
),
argPosition: {
type: 'arrayInObject',
index: 0,
key: 'p3',
},
filterValue: 'p3',
},
{
stdLibFnName: 'circleThreePoint',
type: 'yAbsolute',
isConstrained: isNotLiteralArrayOrStatic(p3Details.expr.elements[1]),
sourceRange: [
p3Details.expr.elements[1].start,
p3Details.expr.elements[1].end,
0,
],
pathToNode: pathToP3YArg,
value: code.slice(
p3Details.expr.elements[1].start,
p3Details.expr.elements[1].end
),
argPosition: {
type: 'arrayInObject',
index: 1,
key: 'p3',
},
filterValue: 'p3',
},
]
const finalConstraints: ConstrainInfo[] = []
constraints.forEach((constraint) => {
if (!filterValue) {
finalConstraints.push(constraint)
}
if (filterValue && constraint.filterValue === filterValue) {
finalConstraints.push(constraint)
}
})
return finalConstraints
},
}
export const angledLine: SketchLineHelper = {
add: ({ node, pathToNode, segmentInput, replaceExistingCallback }) => {
if (segmentInput.type !== 'straight-segment') return STRAIGHT_SEGMENT_ERR
@ -1991,6 +2307,7 @@ export const sketchLineHelperMap: { [key: string]: SketchLineHelper } = {
export const sketchLineHelperMapKw: { [key: string]: SketchLineHelperKw } = {
line,
lineTo,
circleThreePoint,
} as const
export function changeSketchArguments(
@ -2058,30 +2375,36 @@ export function changeSketchArguments(
export function getConstraintInfo(
callExpression: Node<CallExpression>,
code: string,
pathToNode: PathToNode
pathToNode: PathToNode,
filterValue?: string
): ConstrainInfo[] {
const fnName = callExpression?.callee?.name || ''
if (!(fnName in sketchLineHelperMap)) return []
return sketchLineHelperMap[fnName].getConstraintInfo(
callExpression,
code,
pathToNode
pathToNode,
filterValue
)
}
export function getConstraintInfoKw(
callExpression: Node<CallExpressionKw>,
code: string,
pathToNode: PathToNode
pathToNode: PathToNode,
filterValue?: string
): ConstrainInfo[] {
const fnName = callExpression?.callee?.name || ''
const isAbsolute = findKwArg('endAbsolute', callExpression) !== undefined
const isAbsolute =
fnName === 'circleThreePoint' ||
findKwArg('endAbsolute', callExpression) !== undefined
if (!(fnName in sketchLineHelperMapKw)) return []
const correctFnName = fnName === 'line' && isAbsolute ? 'lineTo' : fnName
return sketchLineHelperMapKw[correctFnName].getConstraintInfo(
callExpression,
code,
pathToNode
pathToNode,
filterValue
)
}
@ -2305,8 +2628,6 @@ function addTagToChamfer(
if (err(variableDec)) return variableDec
const isPipeExpression = pipeExpr.node.type === 'PipeExpression'
console.log('pipeExpr', pipeExpr, variableDec)
// const callExpr = isPipeExpression ? pipeExpr.node.body[pipeIndex] : variableDec.node.init
const callExpr = isPipeExpression
? pipeExpr.node.body[pipeIndex]
: variableDec.node.init
@ -2387,7 +2708,6 @@ function addTagToChamfer(
if (isPipeExpression) {
pipeExpr.node.body.splice(pipeIndex, 0, newExpressionToInsert)
} else {
console.log('yo', createPipeExpression([newExpressionToInsert, callExpr]))
callExpr.arguments[1] = createPipeSubstitution()
variableDec.node.init = createPipeExpression([
newExpressionToInsert,
@ -2724,6 +3044,8 @@ export function isAbsoluteLine(lineCall: CallExpressionKw): boolean | Error {
return new Error(
`line call has neither ${ARG_END} nor ${ARG_END_ABSOLUTE} params`
)
case 'circleThreePoint':
return false
}
return new Error(`Unknown sketch function ${name}`)
}

View File

@ -22,7 +22,6 @@ import {
Literal,
SourceRange,
LiteralValue,
recast,
LabeledArg,
} from '../wasm'
import { getNodeFromPath, getNodeFromPathCurry } from '../queryAst'
@ -217,14 +216,19 @@ function createStdlibCallExpressionKw(
tool: ToolTip,
labeled: LabeledArg[],
tag?: Expr,
valueUsedInTransform?: number
valueUsedInTransform?: number,
unlabeled?: Expr
): CreatedSketchExprResult {
const args = labeled
if (tag) {
args.push(createLabeledArg(ARG_TAG, tag))
}
return {
callExp: createCallExpressionStdLibKw(tool, null, args),
callExp: createCallExpressionStdLibKw(
tool,
unlabeled ? unlabeled : null,
args
),
valueUsedInTransform,
}
}
@ -1306,6 +1310,12 @@ export function getRemoveConstraintsTransform(
},
}
if (
sketchFnExp.type === 'CallExpressionKw' &&
sketchFnExp.callee.name === 'circleThreePoint'
) {
return false
}
const isAbsolute =
// isAbsolute doesn't matter if the call is positional.
sketchFnExp.type === 'CallExpression' ? false : isAbsoluteLine(sketchFnExp)
@ -1320,7 +1330,6 @@ export function getRemoveConstraintsTransform(
? getFirstArg(sketchFnExp)
: getArgForEnd(sketchFnExp)
if (err(firstArg)) {
console.error(firstArg)
return false
}
@ -1351,7 +1360,7 @@ export function getRemoveConstraintsTransform(
export function removeSingleConstraint({
pathToCallExp,
inputDetails,
inputDetails: inputToReplace,
ast,
}: {
pathToCallExp: PathToNode
@ -1384,12 +1393,12 @@ export function removeSingleConstraint({
// So we should update the call expression to use the inputs, except for
// the inputDetails, input where we should use the rawValue(s)
if (inputDetails.type === 'arrayItem') {
if (inputToReplace.type === 'arrayItem') {
const values = inputs.map((arg) => {
if (
!(
(arg.type === 'arrayItem' || arg.type === 'arrayOrObjItem') &&
arg.index === inputDetails.index
arg.index === inputToReplace.index
)
)
return arg.expr
@ -1397,9 +1406,9 @@ export function removeSingleConstraint({
(rawValue) =>
(rawValue.type === 'arrayItem' ||
rawValue.type === 'arrayOrObjItem') &&
rawValue.index === inputDetails.index
rawValue.index === inputToReplace.index
)?.expr
return (arg.index === inputDetails.index && literal) || arg.expr
return (arg.index === inputToReplace.index && literal) || arg.expr
})
if (callExp.node.type === 'CallExpression') {
return createStdlibCallExpression(
@ -1428,66 +1437,110 @@ export function removeSingleConstraint({
}
}
if (
inputDetails.type === 'arrayInObject' ||
inputDetails.type === 'objectProperty'
inputToReplace.type === 'arrayInObject' ||
inputToReplace.type === 'objectProperty'
) {
const arrayDetailsNameBetterLater: {
const arrayInput: {
[key: string]: Parameters<typeof createArrayExpression>[0]
} = {}
const otherThing: Parameters<typeof createObjectExpression>[0] = {}
inputs.forEach((arg) => {
const objInput: Parameters<typeof createObjectExpression>[0] = {}
const kwArgInput: ReturnType<typeof createLabeledArg>[] = []
inputs.forEach((currentArg) => {
if (
arg.type !== 'objectProperty' &&
arg.type !== 'arrayOrObjItem' &&
arg.type !== 'arrayInObject'
// should be one of these, return early to make TS happy.
currentArg.type !== 'objectProperty' &&
currentArg.type !== 'arrayOrObjItem' &&
currentArg.type !== 'arrayInObject'
)
return
const rawLiteralArrayInObject = rawArgs.find(
(rawValue) =>
rawValue.type === 'arrayInObject' &&
rawValue.key === inputDetails.key &&
rawValue.index === (arg.type === 'arrayInObject' ? arg.index : -1)
rawValue.key === currentArg.key &&
rawValue.index ===
(currentArg.type === 'arrayInObject' ? currentArg.index : -1)
)
const rawLiteralObjProp = rawArgs.find(
(rawValue) =>
(rawValue.type === 'objectProperty' ||
rawValue.type === 'arrayOrObjItem' ||
rawValue.type === 'arrayInObject') &&
rawValue.key === inputDetails.key
rawValue.key === inputToReplace.key
)
if (
inputDetails.type === 'arrayInObject' &&
inputToReplace.type === 'arrayInObject' &&
rawLiteralArrayInObject?.type === 'arrayInObject' &&
rawLiteralArrayInObject?.index === inputDetails.index &&
rawLiteralArrayInObject?.key === inputDetails.key
rawLiteralArrayInObject?.index === inputToReplace.index &&
rawLiteralArrayInObject?.key === inputToReplace.key
) {
if (!arrayDetailsNameBetterLater[arg.key])
arrayDetailsNameBetterLater[arg.key] = []
arrayDetailsNameBetterLater[inputDetails.key][inputDetails.index] =
if (!arrayInput[currentArg.key]) {
arrayInput[currentArg.key] = []
}
arrayInput[inputToReplace.key][inputToReplace.index] =
rawLiteralArrayInObject.expr
let existingKwgForKey = kwArgInput.find(
(kwArg) => kwArg.label.name === currentArg.key
)
if (!existingKwgForKey) {
existingKwgForKey = createLabeledArg(
currentArg.key,
createArrayExpression([])
)
kwArgInput.push(existingKwgForKey)
}
if (existingKwgForKey.arg.type === 'ArrayExpression') {
existingKwgForKey.arg.elements[inputToReplace.index] =
rawLiteralArrayInObject.expr
}
} else if (
inputDetails.type === 'objectProperty' &&
inputToReplace.type === 'objectProperty' &&
(rawLiteralObjProp?.type === 'objectProperty' ||
rawLiteralObjProp?.type === 'arrayOrObjItem') &&
rawLiteralObjProp?.key === inputDetails.key &&
arg.key === inputDetails.key
rawLiteralObjProp?.key === inputToReplace.key &&
currentArg.key === inputToReplace.key
) {
otherThing[inputDetails.key] = rawLiteralObjProp.expr
} else if (arg.type === 'arrayInObject') {
if (!arrayDetailsNameBetterLater[arg.key])
arrayDetailsNameBetterLater[arg.key] = []
arrayDetailsNameBetterLater[arg.key][arg.index] = arg.expr
} else if (arg.type === 'objectProperty') {
otherThing[arg.key] = arg.expr
objInput[inputToReplace.key] = rawLiteralObjProp.expr
} else if (currentArg.type === 'arrayInObject') {
if (!arrayInput[currentArg.key]) arrayInput[currentArg.key] = []
arrayInput[currentArg.key][currentArg.index] = currentArg.expr
let existingKwgForKey = kwArgInput.find(
(kwArg) => kwArg.label.name === currentArg.key
)
if (!existingKwgForKey) {
existingKwgForKey = createLabeledArg(
currentArg.key,
createArrayExpression([])
)
kwArgInput.push(existingKwgForKey)
}
if (existingKwgForKey.arg.type === 'ArrayExpression') {
existingKwgForKey.arg.elements[currentArg.index] = currentArg.expr
}
} else if (currentArg.type === 'objectProperty') {
objInput[currentArg.key] = currentArg.expr
}
})
const createObjParam: Parameters<typeof createObjectExpression>[0] = {}
Object.entries(arrayDetailsNameBetterLater).forEach(([key, value]) => {
Object.entries(arrayInput).forEach(([key, value]) => {
createObjParam[key] = createArrayExpression(value)
})
if (
callExp.node.callee.name === 'circleThreePoint' &&
callExp.node.type === 'CallExpressionKw'
) {
// it's kwarg
const inputPlane = callExp.node.unlabeled as Expr
return createStdlibCallExpressionKw(
callExp.node.callee.name as any,
kwArgInput,
tag,
undefined,
inputPlane
)
}
const objExp = createObjectExpression({
...createObjParam,
...otherThing,
...objInput,
})
return createStdlibCallExpression(
callExp.node.callee.name as any,
@ -1571,6 +1624,16 @@ function getTransformMapPathKw(
}
| false {
const name = sketchFnExp.callee.name as ToolTip
if (name === 'circleThreePoint') {
const info = transformMap?.circleThreePoint?.free?.[constraintType]
if (info)
return {
toolTip: 'circleThreePoint',
lineInputType: 'free',
constraintType,
}
return false
}
const isAbsolute = findKwArg(ARG_END_ABSOLUTE, sketchFnExp) !== undefined
const nameAbsolute = name === 'line' ? 'lineTo' : name
if (!toolTips.includes(name)) {
@ -1989,6 +2052,13 @@ export function transformAstSketchLines({
radius: seg.radius,
from,
}
: seg.type === 'CircleThreePoint'
? {
type: 'circle-three-point-segment',
p1: seg.p1,
p2: seg.p2,
p3: seg.p3,
}
: {
type: 'straight-segment',
to,

View File

@ -45,6 +45,13 @@ interface ArcSegmentInput {
center: [number, number]
radius: number
}
/** Inputs for three point circle */
interface CircleThreePointSegmentInput {
type: 'circle-three-point-segment'
p1: [number, number]
p2: [number, number]
p3: [number, number]
}
/**
* SegmentInputs is a union type that can be either a StraightSegmentInput or an ArcSegmentInput.
@ -52,7 +59,10 @@ interface ArcSegmentInput {
* - StraightSegmentInput: Represents a straight segment with a starting point (from) and an ending point (to).
* - ArcSegmentInput: Represents an arc segment with a starting point (from), a center point, and a radius.
*/
export type SegmentInputs = StraightSegmentInput | ArcSegmentInput
export type SegmentInputs =
| StraightSegmentInput
| ArcSegmentInput
| CircleThreePointSegmentInput
/**
* Interface for adding or replacing a sketch stblib call expression to a sketch.
@ -85,6 +95,9 @@ export type InputArgKeys =
| 'intersectTag'
| 'radius'
| 'center'
| 'p1'
| 'p2'
| 'p3'
export interface SingleValueInput<T> {
type: 'singleValue'
argType: LineInputsType
@ -239,7 +252,8 @@ export interface SketchLineHelper {
getConstraintInfo: (
callExp: Node<CallExpression>,
code: string,
pathToNode: PathToNode
pathToNode: PathToNode,
filterValue?: string
) => ConstrainInfo[]
}
@ -267,6 +281,7 @@ export interface SketchLineHelperKw {
getConstraintInfo: (
callExp: Node<CallExpressionKw>,
code: string,
pathToNode: PathToNode
pathToNode: PathToNode,
filterValue?: string
) => ConstrainInfo[]
}

View File

@ -14,20 +14,47 @@ import {
import { filterArtifacts } from 'lang/std/artifactGraph'
import { isArray, isOverlap } from 'lib/utils'
export function updatePathToNodeFromMap(
oldPath: PathToNode,
pathToNodeMap: { [key: number]: PathToNode }
/**
* Updates pathToNode body indices to account for the insertion of an expression
* PathToNode expression is after the insertion index, that the body index is incremented
* Negative insertion index means no insertion
*/
export function updatePathToNodePostExprInjection(
pathToNode: PathToNode,
exprInsertIndex: number
): PathToNode {
const updatedPathToNode = structuredClone(oldPath)
let max = 0
Object.values(pathToNodeMap).forEach((path) => {
const index = Number(path[1][0])
if (index > max) {
max = index
if (exprInsertIndex < 0) return pathToNode
const bodyIndex = Number(pathToNode[1][0])
if (bodyIndex < exprInsertIndex) return pathToNode
const clone = structuredClone(pathToNode)
clone[1][0] = bodyIndex + 1
return clone
}
export function updateSketchDetailsNodePaths({
sketchEntryNodePath,
sketchNodePaths,
planeNodePath,
exprInsertIndex,
}: {
sketchEntryNodePath: PathToNode
sketchNodePaths: Array<PathToNode>
planeNodePath: PathToNode
exprInsertIndex: number
}) {
return {
updatedSketchEntryNodePath: updatePathToNodePostExprInjection(
sketchEntryNodePath,
exprInsertIndex
),
updatedSketchNodePaths: sketchNodePaths.map((path) =>
updatePathToNodePostExprInjection(path, exprInsertIndex)
),
updatedPlaneNodePath: updatePathToNodePostExprInjection(
planeNodePath,
exprInsertIndex
),
}
})
updatedPathToNode[1][0] = max
return updatedPathToNode
}
export function isCursorInSketchCommandRange(
@ -36,7 +63,7 @@ export function isCursorInSketchCommandRange(
): string | false {
const overlappingEntries = filterArtifacts(
{
types: ['segment', 'path'],
types: ['segment', 'path', 'plane'],
predicate: (artifact) => {
return selectionRanges.graphSelections.some(
(selection) =>
@ -81,11 +108,27 @@ export function findKwArg(
label: string,
call: CallExpressionKw
): Expr | undefined {
return call.arguments.find((arg) => {
return call?.arguments?.find((arg) => {
return arg.label.name === label
})?.arg
}
/**
Search the keyword arguments from a call for an argument with this label,
returns the index of the argument as well.
*/
export function findKwArgWithIndex(
label: string,
call: CallExpressionKw
): { expr: Expr; argIndex: number } | undefined {
const index = call.arguments.findIndex((arg) => {
return arg.label.name === label
})
return index >= 0
? { expr: call.arguments[index].arg, argIndex: index }
: undefined
}
/**
Search the keyword arguments from a call for an argument with one of these labels.
*/

View File

@ -599,10 +599,6 @@ export const executor = async (
if (programMemoryOverride !== null && err(programMemoryOverride))
return Promise.reject(programMemoryOverride)
// eslint-disable-next-line @typescript-eslint/no-floating-promises
if (programMemoryOverride !== null && err(programMemoryOverride))
return Promise.reject(programMemoryOverride)
try {
let jsAppSettings = default_app_settings()
if (!TEST) {

View File

@ -58,7 +58,7 @@ export type ModelingCommandSchema = {
Revolve: {
selection: Selections
angle: KclCommandValue
axisOrEdge: string
axisOrEdge: 'Axis' | 'Edge'
axis: string
edge: Selections
}

View File

@ -37,7 +37,7 @@ import {
*/
export const getRectangleCallExpressions = (
rectangleOrigin: [number, number],
tags: [string, string, string]
tag: string
) => [
createCallExpressionStdLib('angledLine', [
createArrayExpression([
@ -45,30 +45,28 @@ export const getRectangleCallExpressions = (
createLiteral(0), // This will be the width of the rectangle
]),
createPipeSubstitution(),
createTagDeclarator(tags[0]),
createTagDeclarator(tag),
]),
createCallExpressionStdLib('angledLine', [
createArrayExpression([
createBinaryExpression([
createCallExpressionStdLib('segAng', [createIdentifier(tags[0])]),
createCallExpressionStdLib('segAng', [createIdentifier(tag)]),
'+',
createLiteral(90),
]), // 90 offset from the previous line
createLiteral(0), // This will be the height of the rectangle
]),
createPipeSubstitution(),
createTagDeclarator(tags[1]),
]),
createCallExpressionStdLib('angledLine', [
createArrayExpression([
createCallExpressionStdLib('segAng', [createIdentifier(tags[0])]), // same angle as the first line
createCallExpressionStdLib('segAng', [createIdentifier(tag)]), // same angle as the first line
createUnaryExpression(
createCallExpressionStdLib('segLen', [createIdentifier(tags[0])]),
createCallExpressionStdLib('segLen', [createIdentifier(tag)]),
'-'
), // negative height
]),
createPipeSubstitution(),
createTagDeclarator(tags[2]),
]),
createCallExpressionStdLibKw('line', null, [
createLabeledArg(
@ -95,12 +93,12 @@ export function updateRectangleSketch(
y: number,
tag: string
) {
;((pipeExpression.body[2] as CallExpression)
;((pipeExpression.body[1] as CallExpression)
.arguments[0] as ArrayExpression) = createArrayExpression([
createLiteral(x >= 0 ? 0 : 180),
createLiteral(Math.abs(x)),
])
;((pipeExpression.body[3] as CallExpression)
;((pipeExpression.body[2] as CallExpression)
.arguments[0] as ArrayExpression) = createArrayExpression([
createBinaryExpression([
createCallExpressionStdLib('segAng', [createIdentifier(tag)]),
@ -130,7 +128,7 @@ export function updateCenterRectangleSketch(
let startY = originY - Math.abs(deltaY)
// pipeExpression.body[1] is startProfileAt
let callExpression = pipeExpression.body[1]
let callExpression = pipeExpression.body[0]
if (isCallExpression(callExpression)) {
const arrayExpression = callExpression.arguments[0]
if (isArrayExpression(arrayExpression)) {
@ -144,7 +142,7 @@ export function updateCenterRectangleSketch(
const twoX = deltaX * 2
const twoY = deltaY * 2
callExpression = pipeExpression.body[2]
callExpression = pipeExpression.body[1]
if (isCallExpression(callExpression)) {
const arrayExpression = callExpression.arguments[0]
if (isArrayExpression(arrayExpression)) {
@ -160,7 +158,7 @@ export function updateCenterRectangleSketch(
}
}
callExpression = pipeExpression.body[3]
callExpression = pipeExpression.body[2]
if (isCallExpression(callExpression)) {
const arrayExpression = callExpression.arguments[0]
if (isArrayExpression(arrayExpression)) {

View File

@ -276,18 +276,19 @@ export function getEventForSegmentSelection(
}
if (!id || !group) return null
const artifact = engineCommandManager.artifactGraph.get(id)
const codeRefs = getCodeRefsByArtifactId(
id,
engineCommandManager.artifactGraph
)
if (!artifact || !codeRefs) return null
if (!artifact) return null
const node = getNodeFromPath<Expr>(kclManager.ast, group.userData.pathToNode)
if (err(node)) return null
return {
type: 'Set selection',
data: {
selectionType: 'singleCodeCursor',
selection: {
artifact,
codeRef: codeRefs[0],
codeRef: {
pathToNode: group?.userData?.pathToNode,
range: [node.node.start, node.node.end, 0],
},
},
},
}
@ -572,8 +573,7 @@ export function getSelectionTypeDisplayText(
const selectionsByType = getSelectionCountByType(selection)
if (selectionsByType === 'none') return null
return selectionsByType
.entries()
return [...selectionsByType.entries()]
.map(
// Hack for showing "face" instead of "extrude-wall" in command bar text
([type, count]) =>
@ -581,7 +581,6 @@ export function getSelectionTypeDisplayText(
count > 1 ? 's' : ''
}`
)
.toArray()
.join(', ')
}
@ -591,7 +590,7 @@ export function canSubmitSelectionArg(
) {
return (
selectionsByType !== 'none' &&
selectionsByType.entries().every(([type, count]) => {
[...selectionsByType.entries()].every(([type, count]) => {
const foundIndex = argument.selectionTypes.findIndex((s) => s === type)
return (
foundIndex !== -1 &&
@ -867,7 +866,6 @@ export function updateSelections(
JSON.stringify(pathToNode)
) {
artifact = a
console.log('found artifact', a)
break
}
}

View File

@ -2,8 +2,6 @@ import { CustomIconName } from 'components/CustomIcon'
import { DEV } from 'env'
import { commandBarActor, commandBarMachine } from 'machines/commandBarMachine'
import {
canRectangleOrCircleTool,
isClosedSketch,
isEditingExistingSketch,
modelingMachine,
pipeHasCircle,
@ -72,7 +70,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
icon: 'sketch',
status: 'available',
title: ({ sketchPathId }) =>
`${sketchPathId ? 'Edit' : 'Start'} Sketch`,
sketchPathId ? 'Edit Sketch' : 'Start Sketch',
showTitle: true,
hotkey: 'S',
description: 'Start drawing a 2D sketch',
@ -360,13 +358,6 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
{
id: 'line',
onClick: ({ modelingState, modelingSend }) => {
if (modelingState.matches({ Sketch: { 'Line tool': 'No Points' } })) {
// Exit the sketch state if there are no points and they press ESC
modelingSend({
type: 'Cancel',
})
} else {
// Exit the tool if there are points and they press ESC
modelingSend({
type: 'change tool',
data: {
@ -375,7 +366,6 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
: 'none',
},
})
}
},
icon: 'line',
status: 'available',
@ -386,8 +376,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
}) ||
state.matches({
Sketch: { 'Circle tool': 'Awaiting Radius' },
}) ||
isClosedSketch(state.context),
}),
title: 'Line',
hotkey: (state) =>
state.matches({ Sketch: 'Line tool' }) ? ['Esc', 'L'] : 'L',
@ -467,14 +456,10 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
icon: 'circle',
status: 'available',
title: 'Center circle',
disabled: (state) =>
state.matches('Sketch no face') ||
(!canRectangleOrCircleTool(state.context) &&
!state.matches({ Sketch: 'Circle tool' }) &&
!state.matches({ Sketch: 'circle3PointToolSelect' })),
disabled: (state) => state.matches('Sketch no face'),
isActive: (state) =>
state.matches({ Sketch: 'Circle tool' }) ||
state.matches({ Sketch: 'circle3PointToolSelect' }),
state.matches({ Sketch: 'Circle three point tool' }),
hotkey: (state) =>
state.matches({ Sketch: 'Circle tool' }) ? ['Esc', 'C'] : 'C',
showTitle: false,
@ -488,9 +473,9 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
type: 'change tool',
data: {
tool: !modelingState.matches({
Sketch: 'circle3PointToolSelect',
Sketch: 'Circle three point tool',
})
? 'circle3Points'
? 'circleThreePointNeo'
: 'none',
},
}),
@ -516,10 +501,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
}),
icon: 'rectangle',
status: 'available',
disabled: (state) =>
state.matches('Sketch no face') ||
(!canRectangleOrCircleTool(state.context) &&
!state.matches({ Sketch: 'Rectangle tool' })),
disabled: (state) => state.matches('Sketch no face'),
title: 'Corner rectangle',
hotkey: (state) =>
state.matches({ Sketch: 'Rectangle tool' }) ? ['Esc', 'R'] : 'R',
@ -542,10 +524,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
}),
icon: 'arc',
status: 'available',
disabled: (state) =>
state.matches('Sketch no face') ||
(!canRectangleOrCircleTool(state.context) &&
!state.matches({ Sketch: 'Center Rectangle tool' })),
disabled: (state) => state.matches('Sketch no face'),
title: 'Center rectangle',
hotkey: (state) =>
state.matches({ Sketch: 'Center Rectangle tool' })

View File

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

File diff suppressed because one or more lines are too long

View File

@ -109,7 +109,7 @@ function OnboardingWarningWeb(props: OnboardingResetWarningProps) {
codeManager.updateCodeStateEditor(bracket)
await codeManager.writeToFile()
await kclManager.executeCode(true)
await kclManager.executeCode({ zoomToFit: true })
props.setShouldShowWarning(false)
}, reportRejection)}
nextText="Overwrite code and continue"

View File

@ -11,7 +11,7 @@ export default function Sketching() {
async function clearEditor() {
// We do want to update both the state and editor here.
codeManager.updateCodeStateEditor('')
await kclManager.executeCode(true)
await kclManager.executeCode({ zoomToFit: true })
}
// eslint-disable-next-line @typescript-eslint/no-floating-promises

View File

@ -100,7 +100,7 @@ export function useDemoCode() {
setTimeout(
toSync(async () => {
codeManager.updateCodeStateEditor(bracket)
await kclManager.executeCode(true)
await kclManager.executeCode({ zoomToFit: true })
await codeManager.writeToFile()
}, reportRejection)
)

View File

@ -188,6 +188,9 @@ pub struct Wall {
pub sweep_id: ArtifactId,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub path_ids: Vec<ArtifactId>,
/// This is for the sketch-on-face plane, not for the wall itself. Traverse
/// to the extrude and/or segment to get the wall's code_ref.
pub face_code_ref: CodeRef,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
@ -201,6 +204,9 @@ pub struct Cap {
pub sweep_id: ArtifactId,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub path_ids: Vec<ArtifactId>,
/// This is for the sketch-on-face plane, not for the cap itself. Traverse
/// to the extrude and/or segment to get the cap's code_ref.
pub face_code_ref: CodeRef,
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, ts_rs::TS)]
@ -619,6 +625,17 @@ fn artifacts_to_update(
edge_cut_edge_ids: wall.edge_cut_edge_ids.clone(),
sweep_id: wall.sweep_id,
path_ids: wall.path_ids.clone(),
face_code_ref: wall.face_code_ref.clone(),
})]);
}
Some(Artifact::Cap(cap)) => {
return Ok(vec![Artifact::Cap(Cap {
id: current_plane_id.into(),
sub_type: cap.sub_type,
edge_cut_edge_ids: cap.edge_cut_edge_ids.clone(),
sweep_id: cap.sweep_id,
path_ids: cap.path_ids.clone(),
face_code_ref: cap.face_code_ref.clone(),
})]);
}
Some(_) | None => {
@ -668,6 +685,7 @@ fn artifacts_to_update(
edge_cut_edge_ids: wall.edge_cut_edge_ids.clone(),
sweep_id: wall.sweep_id,
path_ids: vec![id],
face_code_ref: wall.face_code_ref.clone(),
}));
}
return Ok(return_arr);
@ -794,13 +812,48 @@ fn artifacts_to_update(
source_ranges: vec![range],
})
})?;
return_arr.push(Artifact::Wall(Wall {
let extra_artifact = _exec_artifacts.values().find_map(|a| {
if let Artifact::StartSketchOnFace { face_id: id, .. } = a {
if *id == face_id.0 {
return Some(a.clone());
}
}
None
});
let sketch_on_face_source_range = extra_artifact.and_then(|a| match a {
Artifact::StartSketchOnFace { source_range, .. } => Some(source_range),
_ => None,
});
let mut wall_artifact = Wall {
id: face_id,
seg_id: curve_id,
edge_cut_edge_ids: Vec::new(),
sweep_id: path.sweep_id.expect("Expected sweep_id to be Some"),
path_ids: Vec::new(),
face_code_ref: CodeRef {
range,
path_to_node: path_to_node.clone(),
},
};
if let Some(sketch_on_face_source_range) = sketch_on_face_source_range {
wall_artifact.face_code_ref = CodeRef {
range: sketch_on_face_source_range,
path_to_node: path_to_node.clone(),
};
}
let wall = Wall {
id: face_id,
seg_id: curve_id,
edge_cut_edge_ids: Vec::new(),
sweep_id: path_sweep_id,
path_ids: vec![],
}));
face_code_ref: CodeRef {
range,
path_to_node: path_to_node.clone(),
},
};
return_arr.push(Artifact::Wall(wall));
let mut new_seg = seg.clone();
new_seg.surface_id = Some(face_id);
return_arr.push(Artifact::Segment(new_seg));
@ -820,6 +873,7 @@ fn artifacts_to_update(
let Some(face_id) = face.face_id.map(ArtifactId::new) else {
continue;
};
let cap = face.clone().cap;
let path_sweep_id = path.sweep_id.ok_or_else(|| {
KclError::Internal(KclErrorDetails {
message:format!(
@ -828,13 +882,39 @@ fn artifacts_to_update(
source_ranges: vec![range],
})
})?;
return_arr.push(Artifact::Cap(Cap {
let extra_artifact = _exec_artifacts.values().find(|a| {
if let Artifact::StartSketchOnFace { face_id: id, .. } = a {
*id == face_id.0
} else {
false
}
});
let sketch_on_face_source_range = extra_artifact.and_then(|a| match a {
Artifact::StartSketchOnFace { source_range, .. } => Some(source_range),
_ => None,
});
let mut cap_artifact = Cap {
id: face_id,
sub_type,
sub_type: match cap {
ExtrusionFaceCapType::Bottom => CapSubType::Start,
_ => CapSubType::End,
},
edge_cut_edge_ids: Vec::new(),
sweep_id: path_sweep_id,
sweep_id: path.sweep_id.expect("Expected sweep_id to be Some"),
path_ids: Vec::new(),
}));
face_code_ref: CodeRef {
range,
path_to_node: path_to_node.clone(),
},
};
if let Some(sketch_on_face_source_range) = sketch_on_face_source_range {
let range = sketch_on_face_source_range;
cap_artifact.face_code_ref = CodeRef {
range: *range,
path_to_node: path_to_node.clone(),
};
}
return_arr.push(Artifact::Cap(cap_artifact));
let Some(Artifact::Sweep(sweep)) = artifacts.get(&path_sweep_id) else {
continue;
};

View File

@ -11,6 +11,7 @@ use crate::{
errors::KclError,
execution::{ExecState, Metadata, TagEngineInfo, TagIdentifier, UnitLen},
parsing::ast::types::{Node, NodeRef, TagDeclarator, TagNode},
std::shapes::circle_three_point,
};
type Point2D = kcmc::shared::Point2d<f64>;
@ -243,9 +244,9 @@ pub struct Plane {
pub value: PlaneType,
/// Origin of the plane.
pub origin: Point3d,
/// What should the planes X axis be?
/// What should the plane's X axis be?
pub x_axis: Point3d,
/// What should the planes Y axis be?
/// What should the plane's Y axis be?
pub y_axis: Point3d,
/// The z-axis (normal).
pub z_axis: Point3d,
@ -366,9 +367,9 @@ pub struct Face {
pub artifact_id: ArtifactId,
/// The tag of the face.
pub value: String,
/// What should the faces X axis be?
/// What should the face's X axis be?
pub x_axis: Point3d,
/// What should the faces Y axis be?
/// What should the face's Y axis be?
pub y_axis: Point3d,
/// The z-axis (normal).
pub z_axis: Point3d,
@ -773,6 +774,19 @@ pub enum Path {
/// This is used to compute the tangential angle.
ccw: bool,
},
CircleThreePoint {
#[serde(flatten)]
base: BasePath,
/// Point 1 of the circle
#[ts(type = "[number, number]")]
p1: [f64; 2],
/// Point 2 of the circle
#[ts(type = "[number, number]")]
p2: [f64; 2],
/// Point 3 of the circle
#[ts(type = "[number, number]")]
p3: [f64; 2],
},
/// A path that is horizontal.
Horizontal {
#[serde(flatten)]
@ -815,6 +829,7 @@ enum PathType {
TangentialArc,
TangentialArcTo,
Circle,
CircleThreePoint,
Horizontal,
AngledLineTo,
Arc,
@ -827,6 +842,7 @@ impl From<&Path> for PathType {
Path::TangentialArcTo { .. } => Self::TangentialArcTo,
Path::TangentialArc { .. } => Self::TangentialArc,
Path::Circle { .. } => Self::Circle,
Path::CircleThreePoint { .. } => Self::CircleThreePoint,
Path::Horizontal { .. } => Self::Horizontal,
Path::AngledLineTo { .. } => Self::AngledLineTo,
Path::Base { .. } => Self::Base,
@ -845,6 +861,7 @@ impl Path {
Path::TangentialArcTo { base, .. } => base.geo_meta.id,
Path::TangentialArc { base, .. } => base.geo_meta.id,
Path::Circle { base, .. } => base.geo_meta.id,
Path::CircleThreePoint { base, .. } => base.geo_meta.id,
Path::Arc { base, .. } => base.geo_meta.id,
}
}
@ -858,6 +875,7 @@ impl Path {
Path::TangentialArcTo { base, .. } => base.tag.clone(),
Path::TangentialArc { base, .. } => base.tag.clone(),
Path::Circle { base, .. } => base.tag.clone(),
Path::CircleThreePoint { base, .. } => base.tag.clone(),
Path::Arc { base, .. } => base.tag.clone(),
}
}
@ -871,6 +889,7 @@ impl Path {
Path::TangentialArcTo { base, .. } => base,
Path::TangentialArc { base, .. } => base,
Path::Circle { base, .. } => base,
Path::CircleThreePoint { base, .. } => base,
Path::Arc { base, .. } => base,
}
}
@ -908,6 +927,15 @@ impl Path {
linear_distance(self.get_from(), self.get_to())
}
Self::Circle { radius, .. } => 2.0 * std::f64::consts::PI * radius,
Self::CircleThreePoint { .. } => {
let circle_center = crate::std::utils::calculate_circle_from_3_points([
self.get_base().from.into(),
self.get_base().to.into(),
self.get_base().to.into(),
]);
let radius = linear_distance(&[circle_center.center.x, circle_center.center.y], &self.get_base().from);
2.0 * std::f64::consts::PI * radius
}
Self::Arc { .. } => {
// TODO: Call engine utils to figure this out.
linear_distance(self.get_from(), self.get_to())
@ -924,6 +952,7 @@ impl Path {
Path::TangentialArcTo { base, .. } => Some(base),
Path::TangentialArc { base, .. } => Some(base),
Path::Circle { base, .. } => Some(base),
Path::CircleThreePoint { base, .. } => Some(base),
Path::Arc { base, .. } => Some(base),
}
}
@ -943,6 +972,17 @@ impl Path {
ccw: *ccw,
radius: *radius,
},
Path::CircleThreePoint { p1, p2, p3, .. } => {
let circle_center =
crate::std::utils::calculate_circle_from_3_points([(*p1).into(), (*p2).into(), (*p3).into()]);
let radius = linear_distance(&[circle_center.center.x, circle_center.center.y], &p1);
let center_point = [circle_center.center.x, circle_center.center.y];
GetTangentialInfoFromPathsResult::Circle {
center: center_point,
ccw: true,
radius,
}
}
Path::ToPoint { .. } | Path::Horizontal { .. } | Path::AngledLineTo { .. } | Path::Base { .. } => {
let base = self.get_base();
GetTangentialInfoFromPathsResult::PreviousPoint(base.from)

View File

@ -243,7 +243,8 @@ pub(crate) async fn do_post_extrude(
Path::Arc { .. }
| Path::TangentialArc { .. }
| Path::TangentialArcTo { .. }
| Path::Circle { .. } => {
| Path::Circle { .. }
| Path::CircleThreePoint { .. } => {
let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc {
face_id: *actual_face_id,
tag: path.get_base().tag.clone(),

View File

@ -181,6 +181,9 @@ pub async fn circle_three_point(exec_state: &mut ExecState, args: Args) -> Resul
tag = {docs = "Identifier for the circle to reference elsewhere."},
}
}]
/// Similar to inner_circle, but needs to retain 3-point information in the
/// path so it can be used for other features, otherwise it's lost.
async fn inner_circle_three_point(
p1: [f64; 2],
p2: [f64; 2],
@ -191,18 +194,69 @@ async fn inner_circle_three_point(
args: Args,
) -> Result<Sketch, KclError> {
let center = calculate_circle_center(p1, p2, p3);
inner_circle(
CircleData {
center,
// It can be the distance to any of the 3 points - they all lay on the circumference.
radius: distance(center.into(), p2.into()),
},
sketch_surface_or_group,
tag,
let radius = distance(center.into(), p2.into());
let sketch_surface = match sketch_surface_or_group {
SketchOrSurface::SketchSurface(surface) => surface,
SketchOrSurface::Sketch(group) => group.on,
};
let sketch = crate::std::sketch::inner_start_profile_at(
[center[0] + radius, center[1]],
sketch_surface,
None,
exec_state,
args,
args.clone(),
)
.await
.await?;
let from = [center[0] + radius, center[1]];
let angle_start = Angle::zero();
let angle_end = Angle::turn();
let id = exec_state.next_uuid();
args.batch_modeling_cmd(
id,
ModelingCmd::from(mcmd::ExtendPath {
path: sketch.id.into(),
segment: PathSegment::Arc {
start: angle_start,
end: angle_end,
center: KPoint2d::from(center).map(LengthUnit),
radius: radius.into(),
relative: false,
},
}),
)
.await?;
let current_path = Path::CircleThreePoint {
base: BasePath {
from,
to: from,
tag: tag.clone(),
geo_meta: GeoMeta {
id,
metadata: args.source_range.into(),
},
},
p1,
p2,
p3,
};
let mut new_sketch = sketch.clone();
if let Some(tag) = &tag {
new_sketch.add_tag(tag, &current_path);
}
new_sketch.paths.push(current_path);
args.batch_modeling_cmd(id, ModelingCmd::from(mcmd::ClosePath { path_id: new_sketch.id }))
.await?;
Ok(new_sketch)
}
/// Type of the polygon