SketchOnFace UI (#1664)

* always enter edit mode

* initial blocking of extra code-mirror updates

* dry out code

* rejig selections

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* clean up

* stream clean up

* update export

* sketch mode can be entered and exited for extrude faces

But has bugs

* startSketchOn working in some cases, editsketch animation working but not orientation of instersection plane etc

* Revert "A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)"

This reverts commit 406fca4c55.

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* remove comment

* add sketch on face e2e test

* tweenCamToNegYAxis should respect reduced motion

* initial sketch on face working with test

* remove temporary toolbar button and xState flow

* un-used vars

* snapshot test tweak

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* type tidy up

* Revert "A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)"

This reverts commit c39b8ebf95.

* Revert "A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)"

This reverts commit fecf6f490a.

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* rename

* sketch on sketch on sketch

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* typo

* startSketchOn Endcaps

end works, start is weird still

* clear selections for entity_ids that are not recognised

* fix sketch on end cap of second order extrustion

* tiny clean up

* fix sketch on close segment/face

* clean up 'lastCodeMirrorSelectionUpdatedFromScene'

* add code mode test for sketchOnExtrudedFace

* make end cap selection more robust

* update js artifacts for extrudes

* update kcl docs

* clean up

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Kurt Hutten
2024-03-22 10:23:04 +11:00
committed by GitHub
parent e773e932b0
commit 0e916cfd5b
63 changed files with 1860 additions and 720 deletions

View File

@ -69,6 +69,8 @@ const part001 = startSketchOn('XY')
}, },
} | } |
{ {
// the face id the sketch is on
faceId: uuid,
// The id of the face. // The id of the face.
id: uuid, id: uuid,
// The original sketch group id of the object we are sketching on. // The original sketch group id of the object we are sketching on.

View File

@ -69,6 +69,8 @@ const part001 = startSketchOn('XY')
}, },
} | } |
{ {
// the face id the sketch is on
faceId: uuid,
// The id of the face. // The id of the face.
id: uuid, id: uuid,
// The original sketch group id of the object we are sketching on. // The original sketch group id of the object we are sketching on.

View File

@ -76,6 +76,8 @@ startSketchOn('XY')
}, },
} | } |
{ {
// the face id the sketch is on
faceId: uuid,
// The id of the face. // The id of the face.
id: uuid, id: uuid,
// The original sketch group id of the object we are sketching on. // The original sketch group id of the object we are sketching on.
@ -244,6 +246,8 @@ startSketchOn('XY')
}, },
} | } |
{ {
// the face id the sketch is on
faceId: uuid,
// The id of the face. // The id of the face.
id: uuid, id: uuid,
// The original sketch group id of the object we are sketching on. // The original sketch group id of the object we are sketching on.

View File

@ -76,6 +76,8 @@ startSketchOn('XZ')
}, },
} | } |
{ {
// the face id the sketch is on
faceId: uuid,
// The id of the face. // The id of the face.
id: uuid, id: uuid,
// The original sketch group id of the object we are sketching on. // The original sketch group id of the object we are sketching on.
@ -244,6 +246,8 @@ startSketchOn('XZ')
}, },
} | } |
{ {
// the face id the sketch is on
faceId: uuid,
// The id of the face. // The id of the face.
id: uuid, id: uuid,
// The original sketch group id of the object we are sketching on. // The original sketch group id of the object we are sketching on.

View File

@ -77,6 +77,8 @@ startSketchOn('YZ')
}, },
} | } |
{ {
// the face id the sketch is on
faceId: uuid,
// The id of the face. // The id of the face.
id: uuid, id: uuid,
// The original sketch group id of the object we are sketching on. // The original sketch group id of the object we are sketching on.
@ -245,6 +247,8 @@ startSketchOn('YZ')
}, },
} | } |
{ {
// the face id the sketch is on
faceId: uuid,
// The id of the face. // The id of the face.
id: uuid, id: uuid,
// The original sketch group id of the object we are sketching on. // The original sketch group id of the object we are sketching on.

View File

@ -82,6 +82,8 @@ const part001 = startSketchOn('XY')
}, },
} | } |
{ {
// the face id the sketch is on
faceId: uuid,
// The id of the face. // The id of the face.
id: uuid, id: uuid,
// The original sketch group id of the object we are sketching on. // The original sketch group id of the object we are sketching on.
@ -250,6 +252,8 @@ const part001 = startSketchOn('XY')
}, },
} | } |
{ {
// the face id the sketch is on
faceId: uuid,
// The id of the face. // The id of the face.
id: uuid, id: uuid,
// The original sketch group id of the object we are sketching on. // The original sketch group id of the object we are sketching on.

View File

@ -76,6 +76,8 @@ startSketchOn('XY')
}, },
} | } |
{ {
// the face id the sketch is on
faceId: uuid,
// The id of the face. // The id of the face.
id: uuid, id: uuid,
// The original sketch group id of the object we are sketching on. // The original sketch group id of the object we are sketching on.
@ -244,6 +246,8 @@ startSketchOn('XY')
}, },
} | } |
{ {
// the face id the sketch is on
faceId: uuid,
// The id of the face. // The id of the face.
id: uuid, id: uuid,
// The original sketch group id of the object we are sketching on. // The original sketch group id of the object we are sketching on.

View File

@ -75,6 +75,8 @@ startSketchOn('XY')
}, },
} | } |
{ {
// the face id the sketch is on
faceId: uuid,
// The id of the face. // The id of the face.
id: uuid, id: uuid,
// The original sketch group id of the object we are sketching on. // The original sketch group id of the object we are sketching on.
@ -243,6 +245,8 @@ startSketchOn('XY')
}, },
} | } |
{ {
// the face id the sketch is on
faceId: uuid,
// The id of the face. // The id of the face.
id: uuid, id: uuid,
// The original sketch group id of the object we are sketching on. // The original sketch group id of the object we are sketching on.

View File

@ -86,6 +86,8 @@ startSketchOn('-YZ')
}, },
} | } |
{ {
// the face id the sketch is on
faceId: uuid,
// The id of the face. // The id of the face.
id: uuid, id: uuid,
// The original sketch group id of the object we are sketching on. // The original sketch group id of the object we are sketching on.
@ -254,6 +256,8 @@ startSketchOn('-YZ')
}, },
} | } |
{ {
// the face id the sketch is on
faceId: uuid,
// The id of the face. // The id of the face.
id: uuid, id: uuid,
// The original sketch group id of the object we are sketching on. // The original sketch group id of the object we are sketching on.

View File

@ -79,6 +79,8 @@ startSketchOn('XY')
}, },
} | } |
{ {
// the face id the sketch is on
faceId: uuid,
// The id of the face. // The id of the face.
id: uuid, id: uuid,
// The original sketch group id of the object we are sketching on. // The original sketch group id of the object we are sketching on.
@ -247,6 +249,8 @@ startSketchOn('XY')
}, },
} | } |
{ {
// the face id the sketch is on
faceId: uuid,
// The id of the face. // The id of the face.
id: uuid, id: uuid,
// The original sketch group id of the object we are sketching on. // The original sketch group id of the object we are sketching on.

View File

@ -71,6 +71,8 @@ const rectangle = startSketchOn('XY')
}, },
} | } |
{ {
// the face id the sketch is on
faceId: uuid,
// The id of the face. // The id of the face.
id: uuid, id: uuid,
// The original sketch group id of the object we are sketching on. // The original sketch group id of the object we are sketching on.
@ -135,6 +137,8 @@ const rectangle = startSketchOn('XY')
}, },
} | } |
{ {
// the face id the sketch is on
faceId: uuid,
// The id of the face. // The id of the face.
id: uuid, id: uuid,
// The original sketch group id of the object we are sketching on. // The original sketch group id of the object we are sketching on.
@ -302,6 +306,8 @@ const rectangle = startSketchOn('XY')
}, },
} | } |
{ {
// the face id the sketch is on
faceId: uuid,
// The id of the face. // The id of the face.
id: uuid, id: uuid,
// The original sketch group id of the object we are sketching on. // The original sketch group id of the object we are sketching on.

View File

@ -72,6 +72,8 @@ startSketchOn('YZ')
}, },
} | } |
{ {
// the face id the sketch is on
faceId: uuid,
// The id of the face. // The id of the face.
id: uuid, id: uuid,
// The original sketch group id of the object we are sketching on. // The original sketch group id of the object we are sketching on.
@ -240,6 +242,8 @@ startSketchOn('YZ')
}, },
} | } |
{ {
// the face id the sketch is on
faceId: uuid,
// The id of the face. // The id of the face.
id: uuid, id: uuid,
// The original sketch group id of the object we are sketching on. // The original sketch group id of the object we are sketching on.

View File

@ -67,6 +67,8 @@ startSketchOn('XY')
}, },
} | } |
{ {
// the face id the sketch is on
faceId: uuid,
// The id of the face. // The id of the face.
id: uuid, id: uuid,
// The original sketch group id of the object we are sketching on. // The original sketch group id of the object we are sketching on.

View File

@ -68,6 +68,8 @@ const square = startSketchOn('XY')
}, },
} | } |
{ {
// the face id the sketch is on
faceId: uuid,
// The id of the face. // The id of the face.
id: uuid, id: uuid,
// The original sketch group id of the object we are sketching on. // The original sketch group id of the object we are sketching on.
@ -236,6 +238,8 @@ const square = startSketchOn('XY')
}, },
} | } |
{ {
// the face id the sketch is on
faceId: uuid,
// The id of the face. // The id of the face.
id: uuid, id: uuid,
// The original sketch group id of the object we are sketching on. // The original sketch group id of the object we are sketching on.
@ -403,6 +407,8 @@ const square = startSketchOn('XY')
}, },
} | } |
{ {
// the face id the sketch is on
faceId: uuid,
// The id of the face. // The id of the face.
id: uuid, id: uuid,
// The original sketch group id of the object we are sketching on. // The original sketch group id of the object we are sketching on.

View File

@ -66,6 +66,8 @@ startSketchOn("YZ")
}, },
} | } |
{ {
// the face id the sketch is on
faceId: uuid,
// The id of the face. // The id of the face.
id: uuid, id: uuid,
// The original sketch group id of the object we are sketching on. // The original sketch group id of the object we are sketching on.

View File

@ -66,6 +66,8 @@ startSketchOn("YZ")
}, },
} | } |
{ {
// the face id the sketch is on
faceId: uuid,
// The id of the face. // The id of the face.
id: uuid, id: uuid,
// The original sketch group id of the object we are sketching on. // The original sketch group id of the object we are sketching on.

View File

@ -66,6 +66,8 @@ startSketchOn('-XY')
}, },
} | } |
{ {
// the face id the sketch is on
faceId: uuid,
// The id of the face. // The id of the face.
id: uuid, id: uuid,
// The original sketch group id of the object we are sketching on. // The original sketch group id of the object we are sketching on.
@ -234,6 +236,8 @@ startSketchOn('-XY')
}, },
} | } |
{ {
// the face id the sketch is on
faceId: uuid,
// The id of the face. // The id of the face.
id: uuid, id: uuid,
// The original sketch group id of the object we are sketching on. // The original sketch group id of the object we are sketching on.

View File

@ -72,6 +72,8 @@ const part = rectShape([0, 0], 20, 20)
}, },
} | } |
{ {
// the face id the sketch is on
faceId: uuid,
// The id of the face. // The id of the face.
id: uuid, id: uuid,
// The original sketch group id of the object we are sketching on. // The original sketch group id of the object we are sketching on.
@ -240,6 +242,8 @@ const part = rectShape([0, 0], 20, 20)
}, },
} | } |
{ {
// the face id the sketch is on
faceId: uuid,
// The id of the face. // The id of the face.
id: uuid, id: uuid,
// The original sketch group id of the object we are sketching on. // The original sketch group id of the object we are sketching on.

View File

@ -80,6 +80,8 @@ const part = startSketchOn('XY')
}, },
} | } |
{ {
// the face id the sketch is on
faceId: uuid,
// The id of the face. // The id of the face.
id: uuid, id: uuid,
// The original sketch group id of the object we are sketching on. // The original sketch group id of the object we are sketching on.

View File

@ -77,6 +77,8 @@ const part = startSketchOn('XY')
}, },
} | } |
{ {
// the face id the sketch is on
faceId: uuid,
// The id of the face. // The id of the face.
id: uuid, id: uuid,
// The original sketch group id of the object we are sketching on. // The original sketch group id of the object we are sketching on.

View File

@ -69,6 +69,8 @@ const part001 = startSketchOn('XY')
}, },
} | } |
{ {
// the face id the sketch is on
faceId: uuid,
// The id of the face. // The id of the face.
id: uuid, id: uuid,
// The original sketch group id of the object we are sketching on. // The original sketch group id of the object we are sketching on.

View File

@ -67,6 +67,8 @@ startSketchOn("YZ")
}, },
} | } |
{ {
// the face id the sketch is on
faceId: uuid,
// The id of the face. // The id of the face.
id: uuid, id: uuid,
// The original sketch group id of the object we are sketching on. // The original sketch group id of the object we are sketching on.

View File

@ -67,6 +67,8 @@ startSketchOn("YZ")
}, },
} | } |
{ {
// the face id the sketch is on
faceId: uuid,
// The id of the face. // The id of the face.
id: uuid, id: uuid,
// The original sketch group id of the object we are sketching on. // The original sketch group id of the object we are sketching on.

View File

@ -67,6 +67,8 @@ startSketchOn("YZ")
}, },
} | } |
{ {
// the face id the sketch is on
faceId: uuid,
// The id of the face. // The id of the face.
id: uuid, id: uuid,
// The original sketch group id of the object we are sketching on. // The original sketch group id of the object we are sketching on.

View File

@ -57,6 +57,8 @@ startSketchOn('XY')
}, },
} | } |
{ {
// the face id the sketch is on
faceId: uuid,
// The id of the face. // The id of the face.
id: uuid, id: uuid,
// The original sketch group id of the object we are sketching on. // The original sketch group id of the object we are sketching on.
@ -128,6 +130,8 @@ startSketchOn('XY')
}, },
} | } |
{ {
// the face id the sketch is on
faceId: uuid,
// The id of the face. // The id of the face.
id: uuid, id: uuid,
// The original sketch group id of the object we are sketching on. // The original sketch group id of the object we are sketching on.

View File

@ -65,6 +65,8 @@ startSketchAt([0, 0])
}, },
} | } |
{ {
// the face id the sketch is on
faceId: uuid,
// The id of the face. // The id of the face.
id: uuid, id: uuid,
// The original sketch group id of the object we are sketching on. // The original sketch group id of the object we are sketching on.

View File

@ -254,6 +254,8 @@ string
}, },
} | } |
{ {
// the face id the sketch is on
faceId: uuid,
// The id of the face. // The id of the face.
id: uuid, id: uuid,
// The original sketch group id of the object we are sketching on. // The original sketch group id of the object we are sketching on.

File diff suppressed because it is too large Load Diff

View File

@ -75,6 +75,8 @@ startSketchOn('-YZ')
}, },
} | } |
{ {
// the face id the sketch is on
faceId: uuid,
// The id of the face. // The id of the face.
id: uuid, id: uuid,
// The original sketch group id of the object we are sketching on. // The original sketch group id of the object we are sketching on.
@ -243,6 +245,8 @@ startSketchOn('-YZ')
}, },
} | } |
{ {
// the face id the sketch is on
faceId: uuid,
// The id of the face. // The id of the face.
id: uuid, id: uuid,
// The original sketch group id of the object we are sketching on. // The original sketch group id of the object we are sketching on.

View File

@ -65,6 +65,8 @@ startSketchOn('-YZ')
}, },
} | } |
{ {
// the face id the sketch is on
faceId: uuid,
// The id of the face. // The id of the face.
id: uuid, id: uuid,
// The original sketch group id of the object we are sketching on. // The original sketch group id of the object we are sketching on.
@ -233,6 +235,8 @@ startSketchOn('-YZ')
}, },
} | } |
{ {
// the face id the sketch is on
faceId: uuid,
// The id of the face. // The id of the face.
id: uuid, id: uuid,
// The original sketch group id of the object we are sketching on. // The original sketch group id of the object we are sketching on.

View File

@ -66,6 +66,8 @@ startSketchOn('YZ')
}, },
} | } |
{ {
// the face id the sketch is on
faceId: uuid,
// The id of the face. // The id of the face.
id: uuid, id: uuid,
// The original sketch group id of the object we are sketching on. // The original sketch group id of the object we are sketching on.
@ -234,6 +236,8 @@ startSketchOn('YZ')
}, },
} | } |
{ {
// the face id the sketch is on
faceId: uuid,
// The id of the face. // The id of the face.
id: uuid, id: uuid,
// The original sketch group id of the object we are sketching on. // The original sketch group id of the object we are sketching on.

View File

@ -66,6 +66,8 @@ startSketchOn('XY')
}, },
} | } |
{ {
// the face id the sketch is on
faceId: uuid,
// The id of the face. // The id of the face.
id: uuid, id: uuid,
// The original sketch group id of the object we are sketching on. // The original sketch group id of the object we are sketching on.
@ -234,6 +236,8 @@ startSketchOn('XY')
}, },
} | } |
{ {
// the face id the sketch is on
faceId: uuid,
// The id of the face. // The id of the face.
id: uuid, id: uuid,
// The original sketch group id of the object we are sketching on. // The original sketch group id of the object we are sketching on.

View File

@ -66,6 +66,8 @@ startSketchOn('XY')
}, },
} | } |
{ {
// the face id the sketch is on
faceId: uuid,
// The id of the face. // The id of the face.
id: uuid, id: uuid,
// The original sketch group id of the object we are sketching on. // The original sketch group id of the object we are sketching on.
@ -234,6 +236,8 @@ startSketchOn('XY')
}, },
} | } |
{ {
// the face id the sketch is on
faceId: uuid,
// The id of the face. // The id of the face.
id: uuid, id: uuid,
// The original sketch group id of the object we are sketching on. // The original sketch group id of the object we are sketching on.

View File

@ -67,6 +67,8 @@ startSketchOn('XZ')
}, },
} | } |
{ {
// the face id the sketch is on
faceId: uuid,
// The id of the face. // The id of the face.
id: uuid, id: uuid,
// The original sketch group id of the object we are sketching on. // The original sketch group id of the object we are sketching on.
@ -235,6 +237,8 @@ startSketchOn('XZ')
}, },
} | } |
{ {
// the face id the sketch is on
faceId: uuid,
// The id of the face. // The id of the face.
id: uuid, id: uuid,
// The original sketch group id of the object we are sketching on. // The original sketch group id of the object we are sketching on.

View File

@ -20,6 +20,8 @@ const commonPoints = {
startAt: '[9.06, -12.22]', startAt: '[9.06, -12.22]',
num1: 9.14, num1: 9.14,
num2: 18.2, num2: 18.2,
// num1: 9.64,
// num2: 19.19,
} }
test.beforeEach(async ({ context, page }) => { test.beforeEach(async ({ context, page }) => {
@ -76,6 +78,7 @@ test('Basic sketch', async ({ page }) => {
await expect(page.locator('.cm-content')).toHaveText( await expect(page.locator('.cm-content')).toHaveText(
`const part001 = startSketchOn('-XZ')` `const part001 = startSketchOn('-XZ')`
) )
await u.closeDebugPanel()
await page.waitForTimeout(300) // TODO detect animation ending, or disable animation await page.waitForTimeout(300) // TODO detect animation ending, or disable animation
@ -86,7 +89,6 @@ test('Basic sketch', async ({ page }) => {
|> startProfileAt(${commonPoints.startAt}, %)`) |> startProfileAt(${commonPoints.startAt}, %)`)
await page.waitForTimeout(100) await page.waitForTimeout(100)
await u.closeDebugPanel()
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await page.waitForTimeout(100) await page.waitForTimeout(100)
@ -625,7 +627,7 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
const emptySpaceClick = () => const emptySpaceClick = () =>
page.mouse.click(728, 343).then(() => page.waitForTimeout(100)) page.mouse.click(728, 343).then(() => page.waitForTimeout(100))
const topHorzSegmentClick = () => const topHorzSegmentClick = () =>
page.mouse.click(709, 289).then(() => page.waitForTimeout(100)) page.mouse.click(709, 290).then(() => page.waitForTimeout(100))
const bottomHorzSegmentClick = () => const bottomHorzSegmentClick = () =>
page.mouse.click(767, 396).then(() => page.waitForTimeout(100)) page.mouse.click(767, 396).then(() => page.waitForTimeout(100))
@ -640,13 +642,12 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
await page.waitForTimeout(700) // wait for animation await page.waitForTimeout(700) // wait for animation
const startXPx = 600 const startXPx = 600
await u.closeDebugPanel()
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ') .toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${commonPoints.startAt}, %)`) |> startProfileAt(${commonPoints.startAt}, %)`)
await u.closeDebugPanel()
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
@ -727,13 +728,18 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
await emptySpaceClick() await emptySpaceClick()
// select segment in editor than another segment in scene and check there are two cursors // select segment in editor than another segment in scene and check there are two cursors
await page.getByText(` |> line([-${commonPoints.num2}, 0], %)`).click() // TODO change this back to shift click in the scene, not cmd click in the editor
await page.waitForTimeout(300)
await page.keyboard.down('Shift')
await expect(page.locator('.cm-cursor')).toHaveCount(1)
await bottomHorzSegmentClick() await bottomHorzSegmentClick()
await page.keyboard.up('Shift')
await expect(page.locator('.cm-cursor')).toHaveCount(1)
await page.keyboard.down(process.platform === 'linux' ? 'Control' : 'Meta')
await page.waitForTimeout(100)
await page.getByText(` |> line([-${commonPoints.num2}, 0], %)`).click()
await expect(page.locator('.cm-cursor')).toHaveCount(2) await expect(page.locator('.cm-cursor')).toHaveCount(2)
await page.waitForTimeout(500)
await page.keyboard.up(process.platform === 'linux' ? 'Control' : 'Meta')
// clear selection by clicking on nothing // clear selection by clicking on nothing
await emptySpaceClick() await emptySpaceClick()
@ -918,13 +924,13 @@ test('Can add multiple sketches', async ({ page }) => {
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
const startXPx = 600 const startXPx = 600
await u.closeDebugPanel()
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ') .toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${commonPoints.startAt}, %)`) |> startProfileAt(${commonPoints.startAt}, %)`)
await page.waitForTimeout(100) await page.waitForTimeout(100)
await u.closeDebugPanel()
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await page.waitForTimeout(100) await page.waitForTimeout(100)
@ -1372,10 +1378,129 @@ test('Snap to close works (at any scale)', async ({ page }) => {
) => `const part001 = startSketchOn('XZ') ) => `const part001 = startSketchOn('XZ')
|> startProfileAt([${roundOff(scale * 87.68)}, ${roundOff(scale * 43.84)}], %) |> startProfileAt([${roundOff(scale * 87.68)}, ${roundOff(scale * 43.84)}], %)
|> line([${roundOff(scale * 175.36)}, 0], %) |> line([${roundOff(scale * 175.36)}, 0], %)
|> line([0, -${roundOff(scale * 175.37) + fudge}], %) |> line([0, -${roundOff(scale * 175.36) + fudge}], %)
|> close(%)` |> close(%)`
await doSnapAtDifferentScales([0, 100, 100], codeTemplate(0.01, 0.01)) await doSnapAtDifferentScales([0, 100, 100], codeTemplate(0.01, 0.01))
await doSnapAtDifferentScales([0, 10000, 10000], codeTemplate()) await doSnapAtDifferentScales([0, 10000, 10000], codeTemplate())
}) })
test('Sketch on face', async ({ page, context }) => {
const u = getUtils(page)
await context.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`const part001 = startSketchOn('-XZ')
|> startProfileAt([3.29, 7.86], %)
|> line([2.48, 2.44], %)
|> line([2.66, 1.17], %)
|> line([3.75, 0.46], %)
|> line([4.99, -0.46], %)
|> line([3.3, -2.12], %)
|> line([2.16, -3.33], %)
|> line([0.85, -3.08], %)
|> line([-0.18, -3.36], %)
|> line([-3.86, -2.73], %)
|> line([-17.67, 0.85], %)
|> close(%)
|> extrude(5 + 7, %)`
)
})
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
await page.getByRole('button', { name: 'Start Sketch' }).click()
let previousCodeContent = await page.locator('.cm-content').innerText()
await page.mouse.click(793, 133)
const firstClickPosition = [612, 238]
const secondClickPosition = [661, 242]
const thirdClickPosition = [609, 267]
await page.waitForTimeout(300)
await page.mouse.click(firstClickPosition[0], firstClickPosition[1])
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
previousCodeContent = await page.locator('.cm-content').innerText()
await page.mouse.click(secondClickPosition[0], secondClickPosition[1])
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
previousCodeContent = await page.locator('.cm-content').innerText()
await page.mouse.click(thirdClickPosition[0], thirdClickPosition[1])
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
previousCodeContent = await page.locator('.cm-content').innerText()
await page.mouse.click(firstClickPosition[0], firstClickPosition[1])
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
previousCodeContent = await page.locator('.cm-content').innerText()
await expect(page.locator('.cm-content'))
.toContainText(`const part002 = startSketchOn(part001, 'seg01')
|> startProfileAt([1.03, 1.03], %)
|> line([4.18, -0.35], %)
|> line([-4.44, -2.13], %)
|> close(%)`)
await u.openAndClearDebugPanel()
await page.getByRole('button', { name: 'Exit Sketch' }).click()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.updateCamPosition([1049, 239, 686])
await u.closeDebugPanel()
await page.getByText('startProfileAt([1.03, 1.03], %)').click()
await expect(page.getByRole('button', { name: 'Edit Sketch' })).toBeVisible()
await page.getByRole('button', { name: 'Edit Sketch' }).click()
await page.waitForTimeout(200)
const pointToDragFirst = [691, 237]
await page.mouse.move(pointToDragFirst[0], pointToDragFirst[1])
await page.mouse.down()
await page.mouse.move(pointToDragFirst[0] - 20, pointToDragFirst[1], {
steps: 5,
})
await page.mouse.up()
await page.waitForTimeout(100)
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
previousCodeContent = await page.locator('.cm-content').innerText()
await expect(page.locator('.cm-content'))
.toContainText(`const part002 = startSketchOn(part001, 'seg01')
|> startProfileAt([1.03, 1.03], %)
|> line([2.81, -0.33], %)
|> line([-4.44, -2.13], %)
|> close(%)`)
// exit sketch
await u.openAndClearDebugPanel()
await page.getByRole('button', { name: 'Exit Sketch' }).click()
await u.expectCmdLog('[data-message-type="execution-done"]')
await page.getByText('startProfileAt([1.03, 1.03], %)').click()
await expect(page.getByRole('button', { name: 'Extrude' })).not.toBeDisabled()
await page.getByRole('button', { name: 'Extrude' }).click()
await expect(page.getByTestId('command-bar')).toBeVisible()
await page.keyboard.press('Enter')
await expect(page.getByText('Confirm Extrude')).toBeVisible()
await page.keyboard.press('Enter')
await expect(page.locator('.cm-content'))
.toContainText(`const part002 = startSketchOn(part001, 'seg01')
|> startProfileAt([1.03, 1.03], %)
|> line([2.81, -0.33], %)
|> line([-4.44, -2.13], %)
|> close(%)
|> extrude(5 + 7, %)`)
})

View File

@ -612,7 +612,7 @@ test('Client side scene scale should match engine scale mm', async ({
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ') .toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt([230.03, -310.33], %)`) |> startProfileAt([230.03, -310.32], %)`)
await page.waitForTimeout(100) await page.waitForTimeout(100)
await u.closeDebugPanel() await u.closeDebugPanel()
@ -622,7 +622,7 @@ test('Client side scene scale should match engine scale mm', async ({
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ') .toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt([230.03, -310.33], %) |> startProfileAt([230.03, -310.32], %)
|> line([232.2, 0], %)`) |> line([232.2, 0], %)`)
await page.getByRole('button', { name: 'Tangential Arc' }).click() await page.getByRole('button', { name: 'Tangential Arc' }).click()
@ -632,7 +632,7 @@ test('Client side scene scale should match engine scale mm', async ({
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ') .toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt([230.03, -310.33], %) |> startProfileAt([230.03, -310.32], %)
|> line([232.2, 0], %) |> line([232.2, 0], %)
|> tangentialArcTo([694.43, -78.12], %)`) |> tangentialArcTo([694.43, -78.12], %)`)
@ -658,3 +658,48 @@ test('Client side scene scale should match engine scale mm', async ({
maxDiffPixels: 100, maxDiffPixels: 100,
}) })
}) })
test('Sketch on face with none z-up', async ({ page, context }) => {
const u = getUtils(page)
await context.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`const part001 = startSketchOn('-XZ')
|> startProfileAt([1.4, 2.47], %)
|> line({ to: [9.31, 10.55], tag: 'seg01' }, %)
|> line([11.91, -10.42], %)
|> close(%)
|> extrude(5 + 7, %)
const part002 = startSketchOn(part001, 'seg01')
|> startProfileAt([-2.89, 1.82], %)
|> line([4.68, 3.05], %)
|> line({ to: [0, -7.79], tag: 'seg02' }, %)
|> close(%)
|> extrude(5 + 7, %)
`
)
})
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
await page.getByRole('button', { name: 'Start Sketch' }).click()
let previousCodeContent = await page.locator('.cm-content').innerText()
// click at 641, 135
await page.mouse.click(641, 135)
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
previousCodeContent = await page.locator('.cm-content').innerText()
await page.waitForTimeout(300)
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
})
await page.waitForTimeout(200)
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

View File

@ -425,6 +425,7 @@ export class CameraControls {
if (this.camera instanceof OrthographicCamera) return if (this.camera instanceof OrthographicCamera) return
const { x: px, y: py, z: pz } = this.camera.position const { x: px, y: py, z: pz } = this.camera.position
const { x: qx, y: qy, z: qz, w: qw } = this.camera.quaternion const { x: qx, y: qy, z: qz, w: qw } = this.camera.quaternion
const oldCamUp = this.camera.up.clone()
const aspect = window.innerWidth / window.innerHeight const aspect = window.innerWidth / window.innerHeight
this.lastPerspectiveFov = this.camera.fov this.lastPerspectiveFov = this.camera.fov
const { z_near, z_far } = calculateNearFarFromFOV(this.lastPerspectiveFov) const { z_near, z_far } = calculateNearFarFromFOV(this.lastPerspectiveFov)
@ -436,7 +437,8 @@ export class CameraControls {
z_near, z_near,
z_far z_far
) )
this.camera.up.set(0, 0, 1)
this.camera.up.copy(oldCamUp)
this.camera.layers.enable(SKETCH_LAYER) this.camera.layers.enable(SKETCH_LAYER)
if (DEBUG_SHOW_INTERSECTION_PLANE) if (DEBUG_SHOW_INTERSECTION_PLANE)
this.camera.layers.enable(INTERSECTION_PLANE_LAYER) this.camera.layers.enable(INTERSECTION_PLANE_LAYER)
@ -458,13 +460,14 @@ export class CameraControls {
} }
private createPerspectiveCamera = () => { private createPerspectiveCamera = () => {
const { z_near, z_far } = calculateNearFarFromFOV(this.lastPerspectiveFov) const { z_near, z_far } = calculateNearFarFromFOV(this.lastPerspectiveFov)
const previousCamUp = this.camera.up.clone()
this.camera = new PerspectiveCamera( this.camera = new PerspectiveCamera(
this.lastPerspectiveFov, this.lastPerspectiveFov,
window.innerWidth / window.innerHeight, window.innerWidth / window.innerHeight,
z_near, z_near,
z_far z_far
) )
this.camera.up.set(0, 0, 1) this.camera.up.copy(previousCamUp)
this.camera.layers.enable(SKETCH_LAYER) this.camera.layers.enable(SKETCH_LAYER)
if (DEBUG_SHOW_INTERSECTION_PLANE) if (DEBUG_SHOW_INTERSECTION_PLANE)
this.camera.layers.enable(INTERSECTION_PLANE_LAYER) this.camera.layers.enable(INTERSECTION_PLANE_LAYER)
@ -618,7 +621,7 @@ export class CameraControls {
didChange = true didChange = true
} }
this.safeLookAtTarget() this.safeLookAtTarget(this.camera.up)
// Update the camera's matrices // Update the camera's matrices
this.camera.updateMatrixWorld() this.camera.updateMatrixWorld()
@ -683,6 +686,7 @@ export class CameraControls {
targetAngle = -Math.PI / 2, targetAngle = -Math.PI / 2,
duration = 500 duration = 500
): Promise<void> { ): Promise<void> {
return new Promise((resolve) => {
// should tween the camera so that it has an xPosition of 0, and forcing it's yPosition to be negative // should tween the camera so that it has an xPosition of 0, and forcing it's yPosition to be negative
// zPosition should stay the same // zPosition should stay the same
const xyRadius = Math.sqrt( const xyRadius = Math.sqrt(
@ -693,22 +697,7 @@ export class CameraControls {
this.camera.position.y - this.target.y, this.camera.position.y - this.target.y,
this.camera.position.x - this.target.x this.camera.position.x - this.target.x
) )
this._isCamMovingCallback(true, true) const camAtTime = (obj: { angle: number }) => {
return new Promise((resolve) => {
new TWEEN.Tween({ angle: xyAngle })
.to({ angle: targetAngle }, duration)
.onUpdate((obj) => {
const x = xyRadius * Math.cos(obj.angle)
const y = xyRadius * Math.sin(obj.angle)
this.camera.position.set(
this.target.x + x,
this.target.y + y,
this.camera.position.z
)
this.update()
this.onCameraChange()
})
.onComplete((obj) => {
const x = xyRadius * Math.cos(obj.angle) const x = xyRadius * Math.cos(obj.angle)
const y = xyRadius * Math.sin(obj.angle) const y = xyRadius * Math.sin(obj.angle)
this.camera.position.set( this.camera.position.set(
@ -718,13 +707,27 @@ export class CameraControls {
) )
this.update() this.update()
this.onCameraChange() this.onCameraChange()
}
const onComplete = (obj: { angle: number }) => {
camAtTime(obj)
this._isCamMovingCallback(false, true) this._isCamMovingCallback(false, true)
// resolve after a couple of frames // resolve after a couple of frames
requestAnimationFrame(() => { requestAnimationFrame(() => {
requestAnimationFrame(() => resolve()) requestAnimationFrame(() => resolve())
}) })
}) }
this._isCamMovingCallback(true, true)
if (isReducedMotion()) {
onComplete({ angle: targetAngle })
return
}
new TWEEN.Tween({ angle: xyAngle })
.to({ angle: targetAngle }, duration)
.onUpdate(camAtTime)
.onComplete(onComplete)
.start() .start()
}) })
} }
@ -778,6 +781,8 @@ export class CameraControls {
targetQuaternion, targetQuaternion,
animationProgress animationProgress
) )
const up = new Vector3(0, 0, 1).applyQuaternion(currentQ)
this.camera.up.copy(up)
const currentTarget = tempVec.lerpVectors( const currentTarget = tempVec.lerpVectors(
initialTarget, initialTarget,
targetPosition, targetPosition,
@ -802,7 +807,7 @@ export class CameraControls {
const onComplete = async () => { const onComplete = async () => {
if (isReducedMotion() && toOrthographic) { if (isReducedMotion() && toOrthographic) {
cameraAtTime(0.99) cameraAtTime(0.9999)
this.useOrthographicCamera() this.useOrthographicCamera()
} else if (toOrthographic) { } else if (toOrthographic) {
await this.animateToOrthographic() await this.animateToOrthographic()
@ -863,37 +868,40 @@ export class CameraControls {
animateFovChange() // Start the animation animateFovChange() // Start the animation
}) })
animateToPerspective = () => animateToPerspective = (targetCamUp = new Vector3(0, 0, 1)) =>
new Promise((resolve) => { new Promise((resolve) => {
if (this.syncDirection === 'engineToClient') if (this.syncDirection === 'engineToClient') {
console.warn( console.warn(
'animate To Perspective not design to work with engineToClient syncDirection.' 'animate To Perspective not design to work with engineToClient syncDirection.'
) )
}
this.isFovAnimationInProgress = true this.isFovAnimationInProgress = true
// Immediately set the camera to perspective with a very low FOV
const targetFov = this.fovBeforeOrtho // Target FOV for perspective const targetFov = this.fovBeforeOrtho // Target FOV for perspective
this.lastPerspectiveFov = 4 this.lastPerspectiveFov = 4
let currentFov = 4 let currentFov = 4
this.camera.updateProjectionMatrix() const initialCameraUp = this.camera.up.clone()
const fovAnimationStep = (targetFov - currentFov) / FRAMES_TO_ANIMATE_IN
this.usePerspectiveCamera() this.usePerspectiveCamera()
const tempVec = new Vector3()
const animateFovChange = () => { const cameraAtTime = (t: number) => {
if (this.camera instanceof OrthographicCamera) return currentFov =
if (this.camera.fov < targetFov) { this.lastPerspectiveFov + (targetFov - this.lastPerspectiveFov) * t
// Increase the FOV const currentUp = tempVec.lerpVectors(initialCameraUp, targetCamUp, t)
currentFov = Math.min(currentFov + fovAnimationStep, targetFov) this.camera.up.copy(currentUp)
// this.camera.fov = currentFov
this.camera.updateProjectionMatrix()
this.dollyZoom(currentFov) this.dollyZoom(currentFov)
requestAnimationFrame(animateFovChange) // Continue the animation }
} else {
// Set the flag to false as the FOV animation is complete const onComplete = () => {
this.isFovAnimationInProgress = false this.isFovAnimationInProgress = false
resolve(true) resolve(true)
} }
}
animateFovChange() // Start the animation new TWEEN.Tween({ t: 0 })
.to({ t: 1 }, isReducedMotion() ? 50 : FRAMES_TO_ANIMATE_IN * 16) // Assuming 60fps, hence 16ms per frame
.easing(TWEEN.Easing.Quadratic.InOut)
.onUpdate(({ t }) => cameraAtTime(t))
.onComplete(onComplete)
.start()
}) })
reactCameraPropertiesCallback: (a: ReactCameraProperties) => void = () => {} reactCameraPropertiesCallback: (a: ReactCameraProperties) => void = () => {}

View File

@ -40,3 +40,12 @@ export function isQuaternionVertical(q: Quaternion) {
// no x or y components means it's vertical // no x or y components means it's vertical
return compareVec2Epsilon2([v.x, v.y], [0, 0]) return compareVec2Epsilon2([v.x, v.y], [0, 0])
} }
export function quaternionFromUpNForward(up: Vector3, forward: Vector3) {
const dummyCam = new PerspectiveCamera()
dummyCam.up.copy(up)
dummyCam.position.copy(forward)
dummyCam.lookAt(0, 0, 0)
dummyCam.updateMatrix()
return dummyCam.quaternion.clone()
}

View File

@ -5,7 +5,6 @@ import {
Group, Group,
Intersection, Intersection,
LineCurve3, LineCurve3,
Matrix4,
Mesh, Mesh,
MeshBasicMaterial, MeshBasicMaterial,
Object3D, Object3D,
@ -37,7 +36,7 @@ import {
Y_AXIS, Y_AXIS,
YZ_PLANE, YZ_PLANE,
} from './sceneInfra' } from './sceneInfra'
import { isQuaternionVertical } from './helpers' import { isQuaternionVertical, quaternionFromUpNForward } from './helpers'
import { import {
CallExpression, CallExpression,
getTangentialArcToInfo, getTangentialArcToInfo,
@ -55,7 +54,7 @@ import {
} from 'lang/wasm' } from 'lang/wasm'
import { kclManager } from 'lang/KclSingleton' import { kclManager } from 'lang/KclSingleton'
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst' import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
import { executeAst } from 'useStore' import { executeAst, useStore } from 'useStore'
import { engineCommandManager } from 'lang/std/engineConnection' import { engineCommandManager } from 'lang/std/engineConnection'
import { import {
createArcGeometry, createArcGeometry,
@ -70,16 +69,22 @@ import {
changeSketchArguments, changeSketchArguments,
updateStartProfileAtArgs, updateStartProfileAtArgs,
} from 'lang/std/sketch' } from 'lang/std/sketch'
import { isReducedMotion, throttle } from 'lib/utils' import { throttle } from 'lib/utils'
import { import {
createArrayExpression, createArrayExpression,
createCallExpressionStdLib, createCallExpressionStdLib,
createLiteral, createLiteral,
createPipeSubstitution, createPipeSubstitution,
} from 'lang/modifyAst' } from 'lang/modifyAst'
import { getEventForSegmentSelection } from 'lib/selections' import {
getEventForSegmentSelection,
sendSelectEventToEngine,
} from 'lib/selections'
import { getTangentPointFromPreviousArc } from 'lib/utils2d' import { getTangentPointFromPreviousArc } from 'lib/utils2d'
import { createGridHelper, orthoScale, perspScale } from './helpers' import { createGridHelper, orthoScale, perspScale } from './helpers'
import { Models } from '@kittycad/lib'
import { v4 as uuidv4 } from 'uuid'
import { SketchDetails } from 'machines/modelingMachine'
type DraftSegment = 'line' | 'tangentialArcTo' type DraftSegment = 'line' | 'tangentialArcTo'
@ -164,7 +169,7 @@ class SceneEntities {
console.warn('createIntersectionPlane called when it already exists') console.warn('createIntersectionPlane called when it already exists')
return return
} }
const hundredM = 1000000 const hundredM = 100_0000
const planeGeometry = new PlaneGeometry(hundredM, hundredM) const planeGeometry = new PlaneGeometry(hundredM, hundredM)
const planeMaterial = new MeshBasicMaterial({ const planeMaterial = new MeshBasicMaterial({
color: 0xff0000, color: 0xff0000,
@ -178,7 +183,12 @@ class SceneEntities {
this.intersectionPlane.layers.set(INTERSECTION_PLANE_LAYER) this.intersectionPlane.layers.set(INTERSECTION_PLANE_LAYER)
this.scene.add(this.intersectionPlane) this.scene.add(this.intersectionPlane)
} }
createSketchAxis(sketchPathToNode: PathToNode) { createSketchAxis(
sketchPathToNode: PathToNode,
forward: [number, number, number],
up: [number, number, number],
sketchPosition?: [number, number, number]
) {
const orthoFactor = orthoScale(sceneInfra.camControls.camera) const orthoFactor = orthoScale(sceneInfra.camControls.camera)
const baseXColor = 0x000055 const baseXColor = 0x000055
const baseYColor = 0x550000 const baseYColor = 0x550000
@ -238,14 +248,12 @@ class SceneEntities {
child.layers.set(SKETCH_LAYER) child.layers.set(SKETCH_LAYER)
}) })
const quat = quaternionFromSketchGroup( const quat = quaternionFromUpNForward(
sketchGroupFromPathToNode({ new Vector3(...up),
pathToNode: sketchPathToNode, new Vector3(...forward)
ast: kclManager.ast,
programMemory: kclManager.programMemory,
})
) )
this.axisGroup.setRotationFromQuaternion(quat) this.axisGroup.setRotationFromQuaternion(quat)
sketchPosition && this.axisGroup.position.set(...sketchPosition)
this.scene.add(this.axisGroup) this.scene.add(this.axisGroup)
} }
removeIntersectionPlane() { removeIntersectionPlane() {
@ -258,10 +266,16 @@ class SceneEntities {
ast, ast,
// is draft line assumes the last segment is a draft line, and mods it as the user moves the mouse // is draft line assumes the last segment is a draft line, and mods it as the user moves the mouse
draftSegment, draftSegment,
forward,
up,
position,
}: { }: {
sketchPathToNode: PathToNode sketchPathToNode: PathToNode
ast?: Program ast?: Program
draftSegment?: DraftSegment draftSegment?: DraftSegment
forward: [number, number, number]
up: [number, number, number]
position?: [number, number, number]
}) { }) {
sceneInfra.resetMouseListeners() sceneInfra.resetMouseListeners()
this.createIntersectionPlane() this.createIntersectionPlane()
@ -286,6 +300,7 @@ class SceneEntities {
if (!Array.isArray(sketchGroup?.value)) return if (!Array.isArray(sketchGroup?.value)) return
this.sceneProgramMemory = programMemory this.sceneProgramMemory = programMemory
const group = new Group() const group = new Group()
position && group.position.set(...position)
group.userData = { group.userData = {
type: SKETCH_GROUP_SEGMENTS, type: SKETCH_GROUP_SEGMENTS,
pathToNode: sketchPathToNode, pathToNode: sketchPathToNode,
@ -377,13 +392,18 @@ class SceneEntities {
this.activeSegments[JSON.stringify(segPathToNode)] = seg this.activeSegments[JSON.stringify(segPathToNode)] = seg
}) })
this.currentSketchQuaternion = quaternionFromSketchGroup(sketchGroup) this.currentSketchQuaternion = quaternionFromUpNForward(
new Vector3(...up),
new Vector3(...forward)
)
group.setRotationFromQuaternion(this.currentSketchQuaternion) group.setRotationFromQuaternion(this.currentSketchQuaternion)
this.intersectionPlane && this.intersectionPlane &&
this.intersectionPlane.setRotationFromQuaternion( this.intersectionPlane.setRotationFromQuaternion(
this.currentSketchQuaternion this.currentSketchQuaternion
) )
this.intersectionPlane &&
position &&
this.intersectionPlane.position.set(...position)
this.scene.add(group) this.scene.add(group)
if (!draftSegment) { if (!draftSegment) {
sceneInfra.setCallbacks({ sceneInfra.setCallbacks({
@ -453,7 +473,13 @@ class SceneEntities {
kclManager.executeAstMock(modifiedAst, { updates: 'code' }) kclManager.executeAstMock(modifiedAst, { updates: 'code' })
await this.tearDownSketch({ removeAxis: false }) await this.tearDownSketch({ removeAxis: false })
this.setupSketch({ sketchPathToNode, draftSegment }) this.setupSketch({
sketchPathToNode,
draftSegment,
forward,
up,
position,
})
}, },
onMove: (args) => { onMove: (args) => {
this.onDragSegment({ this.onDragSegment({
@ -476,21 +502,37 @@ class SceneEntities {
} }
updateAstAndRejigSketch = async ( updateAstAndRejigSketch = async (
sketchPathToNode: PathToNode, sketchPathToNode: PathToNode,
modifiedAst: Program modifiedAst: Program,
forward: [number, number, number],
up: [number, number, number],
origin: [number, number, number]
) => { ) => {
await kclManager.updateAst(modifiedAst, false) await kclManager.updateAst(modifiedAst, false)
await this.tearDownSketch({ removeAxis: false }) await this.tearDownSketch({ removeAxis: false })
this.setupSketch({ sketchPathToNode }) this.setupSketch({ sketchPathToNode, forward, up, position: origin })
} }
setUpDraftArc = async (sketchPathToNode: PathToNode) => { setUpDraftArc = async (
sketchPathToNode: PathToNode,
forward: [number, number, number],
up: [number, number, number]
) => {
await this.tearDownSketch({ removeAxis: false }) await this.tearDownSketch({ removeAxis: false })
await new Promise((resolve) => setTimeout(resolve, 100)) await new Promise((resolve) => setTimeout(resolve, 100))
this.setupSketch({ sketchPathToNode, draftSegment: 'tangentialArcTo' }) this.setupSketch({
sketchPathToNode,
draftSegment: 'tangentialArcTo',
forward,
up,
})
} }
setUpDraftLine = async (sketchPathToNode: PathToNode) => { setUpDraftLine = async (
sketchPathToNode: PathToNode,
forward: [number, number, number],
up: [number, number, number]
) => {
await this.tearDownSketch({ removeAxis: false }) await this.tearDownSketch({ removeAxis: false })
await new Promise((resolve) => setTimeout(resolve, 100)) await new Promise((resolve) => setTimeout(resolve, 100))
this.setupSketch({ sketchPathToNode, draftSegment: 'line' }) this.setupSketch({ sketchPathToNode, draftSegment: 'line', forward, up })
} }
onDraftLineMouseMove = () => {} onDraftLineMouseMove = () => {}
prepareTruncatedMemoryAndAst = ( prepareTruncatedMemoryAndAst = (
@ -785,10 +827,10 @@ class SceneEntities {
} }
} }
async animateAfterSketch() { async animateAfterSketch() {
if (isReducedMotion()) { // if (isReducedMotion()) {
sceneInfra.camControls.usePerspectiveCamera() // sceneInfra.camControls.usePerspectiveCamera()
return // return
} // }
await sceneInfra.camControls.animateToPerspective() await sceneInfra.camControls.animateToPerspective()
} }
removeSketchGrid() { removeSketchGrid() {
@ -853,26 +895,81 @@ class SceneEntities {
const type: DefaultPlane = selected.userData.type const type: DefaultPlane = selected.userData.type
selected.material.color = defaultPlaneColor(type) selected.material.color = defaultPlaneColor(type)
}, },
onClick: (args) => { onClick: async (args) => {
const checkExtrudeFaceClick = async (): Promise<boolean> => {
const { streamDimensions } = useStore.getState()
const { entity_id } = await sendSelectEventToEngine(
args?.mouseEvent,
document.getElementById('video-stream') as HTMLVideoElement,
streamDimensions
)
if (!entity_id) return false
const artifact = engineCommandManager.artifactMap[entity_id]
if (artifact?.commandType !== 'solid3d_get_extrusion_face_info')
return false
const faceInfo: Models['FaceIsPlanar_type'] = (
await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'face_is_planar',
object_id: entity_id,
},
})
)?.data?.data
if (!faceInfo?.origin || !faceInfo?.z_axis || !faceInfo?.y_axis)
return false
const { z_axis, origin, y_axis } = faceInfo
const pathToNode = getNodePathFromSourceRange(
kclManager.ast,
artifact.range
)
sceneInfra.modelingSend({
type: 'Select default plane',
data: {
type: 'extrudeFace',
zAxis: [z_axis.x, z_axis.y, z_axis.z],
yAxis: [y_axis.x, y_axis.y, y_axis.z],
position: [origin.x, origin.y, origin.z].map(
(num) => num / sceneInfra._baseUnitMultiplier
) as [number, number, number],
extrudeSegmentPathToNode: pathToNode,
cap:
artifact?.additionalData?.type === 'cap'
? artifact.additionalData.info
: 'none',
},
})
return true
}
if (await checkExtrudeFaceClick()) return
if (!args || !args.intersects?.[0]) return if (!args || !args.intersects?.[0]) return
if (args.mouseEvent.which !== 1) return if (args.mouseEvent.which !== 1) return
const { intersects } = args const { intersects } = args
const type = intersects?.[0].object.name || '' const type = intersects?.[0].object.name || ''
const posNorm = Number(intersects?.[0]?.normal?.z) > 0 const posNorm = Number(intersects?.[0]?.normal?.z) > 0
let planeString: DefaultPlaneStr = posNorm ? 'XY' : '-XY' let planeString: DefaultPlaneStr = posNorm ? 'XY' : '-XY'
let normal: [number, number, number] = posNorm ? [0, 0, 1] : [0, 0, -1] let zAxis: [number, number, number] = posNorm ? [0, 0, 1] : [0, 0, -1]
let yAxis: [number, number, number] = [0, 1, 0]
if (type === YZ_PLANE) { if (type === YZ_PLANE) {
planeString = posNorm ? 'YZ' : '-YZ' planeString = posNorm ? 'YZ' : '-YZ'
normal = posNorm ? [1, 0, 0] : [-1, 0, 0] zAxis = posNorm ? [1, 0, 0] : [-1, 0, 0]
yAxis = [0, 0, 1]
} else if (type === XZ_PLANE) { } else if (type === XZ_PLANE) {
planeString = posNorm ? 'XZ' : '-XZ' planeString = posNorm ? 'XZ' : '-XZ'
normal = posNorm ? [0, 1, 0] : [0, -1, 0] zAxis = posNorm ? [0, 1, 0] : [0, -1, 0]
yAxis = [0, 0, 1]
} }
sceneInfra.modelingSend({ sceneInfra.modelingSend({
type: 'Select default plane', type: 'Select default plane',
data: { data: {
type: 'defaultPlane',
plane: planeString, plane: planeString,
normal, zAxis,
yAxis,
}, },
}) })
}, },
@ -1002,33 +1099,10 @@ export function sketchGroupFromPathToNode({
pathToNode, pathToNode,
'VariableDeclarator' 'VariableDeclarator'
).node ).node
// console.trace('where from?')
return programMemory.root[varDec?.id?.name || ''] as SketchGroup return programMemory.root[varDec?.id?.name || ''] as SketchGroup
} }
export function quaternionFromSketchGroup(
sketchGroup: SketchGroup
): Quaternion {
// TODO figure what is happening in the executor that it's some times returning
// [x,y,z] and sometimes {x,y,z}
if (!sketchGroup?.zAxis) {
// sometimes sketchGroup is undefined,
// I don't quiet understand the circumstances yet
// and it's very intermittent so leaving this here for now
console.log('no zAxis', sketchGroup)
console.trace('no zAxis')
}
const zAxisVec = massageFormats(sketchGroup?.zAxis)
const yAxisVec = massageFormats(sketchGroup?.yAxis)
const xAxisVec = new Vector3().crossVectors(yAxisVec, zAxisVec).normalize()
let yAxisVecNormalized = yAxisVec.clone().normalize()
let zAxisVecNormalized = zAxisVec.clone().normalize()
let rotationMatrix = new Matrix4()
rotationMatrix.makeBasis(xAxisVec, yAxisVecNormalized, zAxisVecNormalized)
return new Quaternion().setFromRotationMatrix(rotationMatrix)
}
function colorSegment(object: any, color: number) { function colorSegment(object: any, color: number) {
const segmentHead = getParentGroup(object, [ARROWHEAD, PROFILE_START]) const segmentHead = getParentGroup(object, [ARROWHEAD, PROFILE_START])
if (segmentHead) { if (segmentHead) {
@ -1063,10 +1137,68 @@ export function getSketchQuaternion(
programMemory: kclManager.programMemory, programMemory: kclManager.programMemory,
}) })
const zAxis = sketchGroup?.zAxis || sketchNormalBackUp const zAxis = sketchGroup?.zAxis || sketchNormalBackUp
return getQuaternionFromZAxis(massageFormats(zAxis))
}
export async function getSketchOrientationDetails(
sketchPathToNode: PathToNode
): Promise<{
quat: Quaternion
sketchDetails: SketchDetails
}> {
const sketchGroup = sketchGroupFromPathToNode({
pathToNode: sketchPathToNode,
ast: kclManager.ast,
programMemory: kclManager.programMemory,
})
if (sketchGroup.on.type === 'plane') {
const zAxis = sketchGroup?.zAxis
return {
quat: getQuaternionFromZAxis(massageFormats(zAxis)),
sketchDetails: {
sketchPathToNode,
zAxis: [zAxis.x, zAxis.y, zAxis.z],
yAxis: [sketchGroup.yAxis.x, sketchGroup.yAxis.y, sketchGroup.yAxis.z],
origin: [0, 0, 0],
},
}
}
if (sketchGroup.on.type === 'face') {
const faceInfo: Models['FaceIsPlanar_type'] = (
await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'face_is_planar',
object_id: sketchGroup.on.faceId,
},
})
)?.data?.data
if (!faceInfo?.origin || !faceInfo?.z_axis || !faceInfo?.y_axis)
throw new Error('faceInfo')
const { z_axis, y_axis, origin } = faceInfo
const quaternion = quaternionFromUpNForward(
new Vector3(y_axis.x, y_axis.y, y_axis.z),
new Vector3(z_axis.x, z_axis.y, z_axis.z)
)
return {
quat: quaternion,
sketchDetails: {
sketchPathToNode,
zAxis: [z_axis.x, z_axis.y, z_axis.z],
yAxis: [y_axis.x, y_axis.y, y_axis.z],
origin: [origin.x, origin.y, origin.z],
},
}
}
throw new Error(
'sketchGroup.on.type not recognized, has a new type been added?'
)
}
export function getQuaternionFromZAxis(zAxis: Vector3): Quaternion {
const dummyCam = new PerspectiveCamera() const dummyCam = new PerspectiveCamera()
dummyCam.up.set(0, 0, 1) dummyCam.up.set(0, 0, 1)
const _zAxis = massageFormats(zAxis) dummyCam.position.copy(zAxis)
dummyCam.position.copy(_zAxis)
dummyCam.lookAt(0, 0, 0) dummyCam.lookAt(0, 0, 0)
dummyCam.updateMatrix() dummyCam.updateMatrix()
const quaternion = dummyCam.quaternion.clone() const quaternion = dummyCam.quaternion.clone()
@ -1075,7 +1207,7 @@ export function getSketchQuaternion(
// because vertical quaternions are a gimbal lock, for the orbit controls // because vertical quaternions are a gimbal lock, for the orbit controls
// it's best to set them explicitly to the vertical position with a known good camera up // it's best to set them explicitly to the vertical position with a known good camera up
if (isVert && _zAxis.z < 0) { if (isVert && zAxis.z < 0) {
quaternion.set(0, 1, 0, 0) quaternion.set(0, 1, 0, 0)
} else if (isVert) { } else if (isVert) {
quaternion.set(0, 0, 0, 1) quaternion.set(0, 0, 0, 1)

View File

@ -37,8 +37,10 @@ export const ZOOM_MAGIC_NUMBER = 63.5
export const INTERSECTION_PLANE_LAYER = 1 export const INTERSECTION_PLANE_LAYER = 1
export const SKETCH_LAYER = 2 export const SKETCH_LAYER = 2
export const DEBUG_SHOW_INTERSECTION_PLANE = false
export const DEBUG_SHOW_BOTH_SCENES = false // redundant types so that it can be changed temporarily but CI will catch the wrong type
export const DEBUG_SHOW_INTERSECTION_PLANE: false = false
export const DEBUG_SHOW_BOTH_SCENES: false = false
export const RAYCASTABLE_PLANE = 'raycastable-plane' export const RAYCASTABLE_PLANE = 'raycastable-plane'
export const DEFAULT_PLANES = 'default-planes' export const DEFAULT_PLANES = 'default-planes'
@ -97,13 +99,13 @@ class SceneInfra {
_baseUnitMultiplier = 1 _baseUnitMultiplier = 1
onDragCallback: (arg: OnDragCallbackArgs) => void = () => {} onDragCallback: (arg: OnDragCallbackArgs) => void = () => {}
onMoveCallback: (arg: OnMoveCallbackArgs) => void = () => {} onMoveCallback: (arg: OnMoveCallbackArgs) => void = () => {}
onClickCallback: (arg?: OnClickCallbackArgs) => void = () => {} onClickCallback: (arg: OnClickCallbackArgs) => void = () => {}
onMouseEnter: (arg: OnMouseEnterLeaveArgs) => void = () => {} onMouseEnter: (arg: OnMouseEnterLeaveArgs) => void = () => {}
onMouseLeave: (arg: OnMouseEnterLeaveArgs) => void = () => {} onMouseLeave: (arg: OnMouseEnterLeaveArgs) => void = () => {}
setCallbacks = (callbacks: { setCallbacks = (callbacks: {
onDrag?: (arg: OnDragCallbackArgs) => void onDrag?: (arg: OnDragCallbackArgs) => void
onMove?: (arg: OnMoveCallbackArgs) => void onMove?: (arg: OnMoveCallbackArgs) => void
onClick?: (arg?: OnClickCallbackArgs) => void onClick?: (arg: OnClickCallbackArgs) => void
onMouseEnter?: (arg: OnMouseEnterLeaveArgs) => void onMouseEnter?: (arg: OnMouseEnterLeaveArgs) => void
onMouseLeave?: (arg: OnMouseEnterLeaveArgs) => void onMouseLeave?: (arg: OnMouseEnterLeaveArgs) => void
}) => { }) => {
@ -272,16 +274,19 @@ class SceneInfra {
let transformedPoint = intersectPoint.clone() let transformedPoint = intersectPoint.clone()
if (transformedPoint) { if (transformedPoint) {
transformedPoint.applyQuaternion(inversePlaneQuaternion) transformedPoint.applyQuaternion(inversePlaneQuaternion)
transformedPoint?.sub(
new Vector3(...planePosition).applyQuaternion(inversePlaneQuaternion)
)
} }
const twoD = new Vector2(
return { // I think the intersection plane doesn't get scale when nearly everything else does, maybe that should change
twoD: new Vector2(
transformedPoint.x / this._baseUnitMultiplier, transformedPoint.x / this._baseUnitMultiplier,
transformedPoint.y / this._baseUnitMultiplier transformedPoint.y / this._baseUnitMultiplier
), // z should be 0 ) // z should be 0
const planePositionCorrected = new Vector3(
...planePosition
).applyQuaternion(inversePlaneQuaternion)
twoD.sub(new Vector2(...planePositionCorrected))
return {
twoD,
threeD: intersectPoint.divideScalar(this._baseUnitMultiplier), threeD: intersectPoint.divideScalar(this._baseUnitMultiplier),
intersection: planeIntersects[0], intersection: planeIntersects[0],
} }
@ -464,7 +469,7 @@ class SceneInfra {
intersects, intersects,
}) })
} else { } else {
this.onClickCallback() this.onClickCallback({ mouseEvent, intersects })
} }
// Clear the selected state whether it was dragged or not // Clear the selected state whether it was dragged or not
this.selected = null this.selected = null
@ -478,7 +483,7 @@ class SceneInfra {
intersects, intersects,
}) })
} else { } else {
this.onClickCallback() this.onClickCallback({ mouseEvent, intersects })
} }
} }
showDefaultPlanes() { showDefaultPlanes() {

View File

@ -38,7 +38,26 @@ describe('processMemory', () => {
myVar: 5, myVar: 5,
myFn: undefined, myFn: undefined,
otherVar: 3, otherVar: 3,
theExtrude: [], theExtrude: [
{
type: 'extrudePlane',
position: [0, 0, 0],
rotation: [0, 0, 0, 1],
faceId: expect.any(String),
name: '',
id: expect.any(String),
sourceRange: [170, 194],
},
{
type: 'extrudePlane',
position: [0, 0, 0],
rotation: [0, 0, 0, 1],
faceId: expect.any(String),
name: '',
id: expect.any(String),
sourceRange: [202, 230],
},
],
theSketch: [ theSketch: [
{ type: 'ToPoint', to: [-3.35, 0.17], from: [0, 0], name: '' }, { type: 'ToPoint', to: [-3.35, 0.17], from: [0, 0], name: '' },
{ type: 'ToPoint', to: [0.98, 5.16], from: [-3.35, 0.17], name: '' }, { type: 'ToPoint', to: [0.98, 5.16], from: [-3.35, 0.17], name: '' },

View File

@ -23,9 +23,9 @@ import { applyConstraintAngleLength } from './Toolbar/setAngleLength'
import { pathMapToSelections } from 'lang/util' import { pathMapToSelections } from 'lang/util'
import { useStore } from 'useStore' import { useStore } from 'useStore'
import { import {
Selections,
canExtrudeSelection, canExtrudeSelection,
handleSelectionBatch, handleSelectionBatch,
handleSelectionWithShift,
isSelectionLastLine, isSelectionLastLine,
isSketchPipe, isSketchPipe,
} from 'lib/selections' } from 'lib/selections'
@ -34,14 +34,20 @@ import { applyConstraintAbsDistance } from './Toolbar/SetAbsDistance'
import useStateMachineCommands from 'hooks/useStateMachineCommands' import useStateMachineCommands from 'hooks/useStateMachineCommands'
import { modelingMachineConfig } from 'lib/commandBarConfigs/modelingCommandConfig' import { modelingMachineConfig } from 'lib/commandBarConfigs/modelingCommandConfig'
import { sceneInfra } from 'clientSideScene/sceneInfra' import { sceneInfra } from 'clientSideScene/sceneInfra'
import { getSketchQuaternion } from 'clientSideScene/sceneEntities' import {
import { startSketchOnDefault } from 'lang/modifyAst' getSketchQuaternion,
import { Program } from 'lang/wasm' getSketchOrientationDetails,
import { isSingleCursorInPipe } from 'lang/queryAst' } from 'clientSideScene/sceneEntities'
import { sketchOnExtrudedFace, startSketchOnDefault } from 'lang/modifyAst'
import { Program, parse } from 'lang/wasm'
import { getNodePathFromSourceRange, isSingleCursorInPipe } from 'lang/queryAst'
import { TEST } from 'env' import { TEST } from 'env'
import { exportFromEngine } from 'lib/exportFromEngine' import { exportFromEngine } from 'lib/exportFromEngine'
import { Models } from '@kittycad/lib/dist/types/src' import { Models } from '@kittycad/lib/dist/types/src'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { EditorSelection } from '@uiw/react-codemirror'
import { Vector3 } from 'three'
import { quaternionFromUpNForward } from 'clientSideScene/helpers'
type MachineContext<T extends AnyStateMachine> = { type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T> state: StateFrom<T>
@ -69,9 +75,15 @@ export const ModelingMachineProvider = ({
const streamRef = useRef<HTMLDivElement>(null) const streamRef = useRef<HTMLDivElement>(null)
useSetupEngineManager(streamRef, token) useSetupEngineManager(streamRef, token)
const { isShiftDown, editorView } = useStore((s) => ({ const {
isShiftDown,
editorView,
setLastCodeMirrorSelectionUpdatedFromScene,
} = useStore((s) => ({
isShiftDown: s.isShiftDown, isShiftDown: s.isShiftDown,
editorView: s.editorView, editorView: s.editorView,
setLastCodeMirrorSelectionUpdatedFromScene:
s.setLastCodeMirrorSelectionUpdatedFromScene,
})) }))
// Settings machine setup // Settings machine setup
@ -92,92 +104,98 @@ export const ModelingMachineProvider = ({
{ {
actions: { actions: {
'sketch exit execute': () => { 'sketch exit execute': () => {
try {
kclManager.executeAst(parse(kclManager.code))
} catch (e) {
kclManager.executeAst() kclManager.executeAst()
}
}, },
'Set selection': assign(({ selectionRanges }, event) => { 'Set selection': assign(({ selectionRanges }, event) => {
if (event.type !== 'Set selection') return {} // this was needed for ts after adding 'Set selection' action to on done modal events if (event.type !== 'Set selection') return {} // this was needed for ts after adding 'Set selection' action to on done modal events
const setSelections = event.data const setSelections = event.data
if (!editorView) return {} if (!editorView) return {}
if (setSelections.selectionType === 'mirrorCodeMirrorSelections') const dispatchSelection = (selection?: EditorSelection) => {
return { selectionRanges: setSelections.selection } if (!selection) return // TODO less of hack for the below please
else if (setSelections.selectionType === 'otherSelection') { setLastCodeMirrorSelectionUpdatedFromScene(Date.now())
const { setTimeout(() => editorView.dispatch({ selection }))
codeMirrorSelection, }
selectionRangeTypeMap, let selections: Selections = {
otherSelections, codeBasedSelections: [],
} = handleSelectionWithShift({ otherSelections: [],
otherSelection: setSelections.selection, }
currentSelections: selectionRanges, if (setSelections.selectionType === 'singleCodeCursor') {
isShiftDown, if (!setSelections.selection && isShiftDown) {
}) } else if (!setSelections.selection && !isShiftDown) {
setTimeout(() => { selections = {
editorView.dispatch({ codeBasedSelections: [],
selection: codeMirrorSelection, otherSelections: [],
}) }
}) } else if (setSelections.selection && !isShiftDown) {
return { selections = {
selectionRangeTypeMap, codeBasedSelections: [setSelections.selection],
selectionRanges: { otherSelections: [],
codeBasedSelections: selectionRanges.codeBasedSelections, }
otherSelections, } else if (setSelections.selection && isShiftDown) {
}, selections = {
codeBasedSelections: [
...selectionRanges.codeBasedSelections,
setSelections.selection,
],
otherSelections: selectionRanges.otherSelections,
}
} }
} else if (setSelections.selectionType === 'singleCodeCursor') {
// This DOES NOT set the `selectionRanges` in xstate context
// instead it updates/dispatches to the editor, which in turn updates the xstate context
// I've found this the best way to deal with the editor without causing an infinite loop
// and really we want the editor to be in charge of cursor positions and for `selectionRanges` mirror it
// because we want to respect the user manually placing the cursor too.
// for more details on how selections see `src/lib/selections.ts`.
const { const {
engineEvents,
codeMirrorSelection, codeMirrorSelection,
selectionRangeTypeMap, updateSceneObjectColors,
otherSelections, } = handleSelectionBatch({
} = handleSelectionWithShift({ selections,
codeSelection: setSelections.selection,
currentSelections: selectionRanges,
isShiftDown,
}) })
if (codeMirrorSelection) { codeMirrorSelection && dispatchSelection(codeMirrorSelection)
setTimeout(() => { engineEvents &&
editorView.dispatch({ engineEvents.forEach((event) =>
selection: codeMirrorSelection, engineCommandManager.sendSceneCommand(event)
}) )
}) updateSceneObjectColors()
}
if (!setSelections.selection) {
return { return {
selectionRangeTypeMap, selectionRanges: selections,
selectionRanges: {
codeBasedSelections: selectionRanges.codeBasedSelections,
otherSelections,
},
} }
} }
if (setSelections.selectionType === 'mirrorCodeMirrorSelections') {
return { return {
selectionRangeTypeMap, selectionRanges: setSelections.selection,
selectionRanges: { }
}
if (setSelections.selectionType === 'otherSelection') {
if (isShiftDown) {
selections = {
codeBasedSelections: selectionRanges.codeBasedSelections, codeBasedSelections: selectionRanges.codeBasedSelections,
otherSelections, otherSelections: [setSelections.selection],
}, }
} else {
selections = {
codeBasedSelections: [],
otherSelections: [setSelections.selection],
} }
} }
// This DOES NOT set the `selectionRanges` in xstate context const { engineEvents, updateSceneObjectColors } =
// same as comment above
const { codeMirrorSelection, selectionRangeTypeMap } =
handleSelectionBatch({ handleSelectionBatch({
selections: setSelections.selection, selections,
})
if (codeMirrorSelection) {
setTimeout(() => {
editorView.dispatch({
selection: codeMirrorSelection,
})
}) })
engineEvents &&
engineEvents.forEach((event) =>
engineCommandManager.sendSceneCommand(event)
)
updateSceneObjectColors()
return {
selectionRanges: selections,
} }
return { selectionRangeTypeMap } }
return {}
}), }),
'Engine export': (_, event) => { 'Engine export': (_, event) => {
if (event.type !== 'Export' || TEST) return if (event.type !== 'Export' || TEST) return
@ -255,10 +273,10 @@ export const ModelingMachineProvider = ({
kclManager.kclErrors.length === 0 && kclManager.ast.body.length > 0, kclManager.kclErrors.length === 0 && kclManager.ast.body.length > 0,
}, },
services: { services: {
'AST-undo-startSketchOn': async ({ sketchPathToNode }) => { 'AST-undo-startSketchOn': async ({ sketchDetails }) => {
if (!sketchPathToNode) return if (!sketchDetails) return
const newAst: Program = JSON.parse(JSON.stringify(kclManager.ast)) const newAst: Program = JSON.parse(JSON.stringify(kclManager.ast))
const varDecIndex = sketchPathToNode[1][0] const varDecIndex = sketchDetails.sketchPathToNode[1][0]
// remove body item at varDecIndex // remove body item at varDecIndex
newAst.body = newAst.body.filter((_, i) => i !== varDecIndex) newAst.body = newAst.body.filter((_, i) => i !== varDecIndex)
await kclManager.executeAstMock(newAst, { updates: 'code' }) await kclManager.executeAstMock(newAst, { updates: 'code' })
@ -267,28 +285,69 @@ export const ModelingMachineProvider = ({
onDrag: () => {}, onDrag: () => {},
}) })
}, },
'animate-to-face': async (_, { data: { plane, normal } }) => { 'animate-to-face': async (_, { data }) => {
if (data.type === 'extrudeFace') {
const { modifiedAst, pathToNode: pathToNewSketchNode } =
sketchOnExtrudedFace(
kclManager.ast,
data.extrudeSegmentPathToNode,
kclManager.programMemory,
data.cap
)
await kclManager.executeAstMock(modifiedAst, { updates: 'code' })
const forward = new Vector3(...data.zAxis)
const up = new Vector3(...data.yAxis)
let target = new Vector3(...data.position).multiplyScalar(
sceneInfra._baseUnitMultiplier
)
const quaternion = quaternionFromUpNForward(up, forward)
await sceneInfra.camControls.tweenCameraToQuaternion(
quaternion,
target
)
return {
sketchPathToNode: pathToNewSketchNode,
zAxis: data.zAxis,
yAxis: data.yAxis,
origin: data.position,
}
}
const { modifiedAst, pathToNode } = startSketchOnDefault( const { modifiedAst, pathToNode } = startSketchOnDefault(
kclManager.ast, kclManager.ast,
plane data.plane
) )
await kclManager.updateAst(modifiedAst, false) await kclManager.updateAst(modifiedAst, false)
const quaternion = getSketchQuaternion(pathToNode, normal) const quat = await getSketchQuaternion(pathToNode, data.zAxis)
await sceneInfra.camControls.tweenCameraToQuaternion(quaternion) await sceneInfra.camControls.tweenCameraToQuaternion(quat)
return { return {
sketchPathToNode: pathToNode, sketchPathToNode: pathToNode,
sketchNormalBackUp: normal, zAxis: data.zAxis,
yAxis: data.yAxis,
origin: [0, 0, 0],
} }
}, },
'animate-to-sketch': async ({ 'animate-to-sketch': async ({ selectionRanges }) => {
sketchPathToNode, const sourceRange = selectionRanges.codeBasedSelections[0].range
sketchNormalBackUp, const sketchPathToNode = getNodePathFromSourceRange(
}) => { kclManager.ast,
const quaternion = getSketchQuaternion( sourceRange
sketchPathToNode || [],
sketchNormalBackUp
) )
await sceneInfra.camControls.tweenCameraToQuaternion(quaternion) const info = await getSketchOrientationDetails(sketchPathToNode || [])
await sceneInfra.camControls.tweenCameraToQuaternion(
info.quat,
new Vector3(...info.sketchDetails.origin)
)
return {
sketchPathToNode: sketchPathToNode || [],
zAxis: info.sketchDetails.zAxis || null,
yAxis: info.sketchDetails.yAxis || null,
origin: info.sketchDetails.origin.map(
(a) => a / sceneInfra._baseUnitMultiplier
) as [number, number, number],
}
}, },
'Get horizontal info': async ({ 'Get horizontal info': async ({
selectionRanges, selectionRanges,

View File

@ -1,15 +1,14 @@
import { MouseEventHandler, useEffect, useRef, useState } from 'react' import { MouseEventHandler, useEffect, useRef, useState } from 'react'
import { v4 as uuidv4 } from 'uuid'
import { useStore } from '../useStore' import { useStore } from '../useStore'
import { getNormalisedCoordinates } from '../lib/utils' import { getNormalisedCoordinates } from '../lib/utils'
import Loading from './Loading' import Loading from './Loading'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { Models } from '@kittycad/lib'
import { engineCommandManager } from '../lang/std/engineConnection'
import { useModelingContext } from 'hooks/useModelingContext' import { useModelingContext } from 'hooks/useModelingContext'
import { useKclContext } from 'lang/KclSingleton' import { useKclContext } from 'lang/KclSingleton'
import { ClientSideScene } from 'clientSideScene/ClientSideSceneComp' import { ClientSideScene } from 'clientSideScene/ClientSideSceneComp'
import { NetworkHealthState, useNetworkStatus } from './NetworkHealthIndicator' import { NetworkHealthState, useNetworkStatus } from './NetworkHealthIndicator'
import { butName } from 'lib/cameraControls'
import { sendSelectEventToEngine } from 'lib/selections'
export const Stream = ({ className = '' }: { className?: string }) => { export const Stream = ({ className = '' }: { className?: string }) => {
const [isLoading, setIsLoading] = useState(true) const [isLoading, setIsLoading] = useState(true)
@ -60,50 +59,14 @@ export const Stream = ({ className = '' }: { className?: string }) => {
setClickCoords({ x, y }) setClickCoords({ x, y })
} }
const handleMouseUp: MouseEventHandler<HTMLDivElement> = ({ const handleMouseUp: MouseEventHandler<HTMLDivElement> = (e) => {
clientX,
clientY,
ctrlKey,
}) => {
if (!videoRef.current) return if (!videoRef.current) return
setButtonDownInStream(undefined) setButtonDownInStream(undefined)
if (state.matches('Sketch')) return if (state.matches('Sketch')) return
if (state.matches('Sketch no face')) return if (state.matches('Sketch no face')) return
const { x, y } = getNormalisedCoordinates({
clientX,
clientY,
el: videoRef.current,
...streamDimensions,
})
const newCmdId = uuidv4() if (!didDragInStream && butName(e).left) {
const interaction = ctrlKey ? 'pan' : 'rotate' sendSelectEventToEngine(e, videoRef.current, streamDimensions)
const command: Models['WebSocketRequest_type'] = {
type: 'modeling_cmd_req',
cmd: {
type: 'camera_drag_end',
interaction,
window: { x, y },
},
cmd_id: newCmdId,
}
if (!didDragInStream) {
command.cmd = {
type: 'select_with_point',
selected_at_window: { x, y },
selection_type: 'add',
}
engineCommandManager.sendSceneCommand(command)
} else if (didDragInStream) {
command.cmd = {
type: 'handle_mouse_drag_end',
window: { x, y },
}
void engineCommandManager.sendSceneCommand(command)
} else {
engineCommandManager.sendSceneCommand(command)
} }
setDidDragInStream(false) setDidDragInStream(false)
@ -143,6 +106,7 @@ export const Stream = ({ className = '' }: { className?: string }) => {
className={`w-full cursor-pointer h-full ${isExecuting && 'blur-md'}`} className={`w-full cursor-pointer h-full ${isExecuting && 'blur-md'}`}
disablePictureInPicture disablePictureInPicture
style={{ transitionDuration: '200ms', transitionProperty: 'filter' }} style={{ transitionDuration: '200ms', transitionProperty: 'filter' }}
id="video-stream"
/> />
<ClientSideScene cameraControls={settings.context?.cameraControls} /> <ClientSideScene cameraControls={settings.context?.cameraControls} />
{!isNetworkOkay && !isLoading && ( {!isNetworkOkay && !isLoading && (

View File

@ -3,6 +3,7 @@ import ReactCodeMirror, {
Extension, Extension,
ViewUpdate, ViewUpdate,
keymap, keymap,
SelectionRange,
} from '@uiw/react-codemirror' } from '@uiw/react-codemirror'
import { TEST } from 'env' import { TEST } from 'env'
import { useCommandsContext } from 'hooks/useCommandsContext' import { useCommandsContext } from 'hooks/useCommandsContext'
@ -75,7 +76,7 @@ export const TextEditor = ({
}) })
const { const {
context: { selectionRanges, selectionRangeTypeMap }, context: { selectionRanges },
send, send,
state, state,
} = useModelingContext() } = useModelingContext()
@ -91,10 +92,27 @@ export const TextEditor = ({
if (isNetworkOkay) kclManager.setCodeAndExecute(newCode) if (isNetworkOkay) kclManager.setCodeAndExecute(newCode)
else kclManager.setCode(newCode) else kclManager.setCode(newCode)
} //, []); } //, []);
const lastSelection = useRef('')
const onUpdate = (viewUpdate: ViewUpdate) => { const onUpdate = (viewUpdate: ViewUpdate) => {
if (!editorView) { if (!editorView) {
setEditorView(viewUpdate.view) setEditorView(viewUpdate.view)
} }
const selString = stringifyRanges(
viewUpdate?.state?.selection?.ranges || []
)
if (selString === lastSelection.current) {
// onUpdate is noisy and is fired a lot by extensions
// since we're only interested in selections changes we can ignore most of these.
return
}
lastSelection.current = selString
if (
// TODO find a less lazy way of getting the last
Date.now() - useStore.getState().lastCodeMirrorSelectionUpdatedFromScene <
150
)
return // update triggered by scene selection
if (sceneInfra.selected) return // mid drag if (sceneInfra.selected) return // mid drag
const ignoreEvents: ModelingMachineEvent['type'][] = [ const ignoreEvents: ModelingMachineEvent['type'][] = [
'Equip Line tool', 'Equip Line tool',
@ -104,7 +122,6 @@ export const TextEditor = ({
const eventInfo = processCodeMirrorRanges({ const eventInfo = processCodeMirrorRanges({
codeMirrorRanges: viewUpdate.state.selection.ranges, codeMirrorRanges: viewUpdate.state.selection.ranges,
selectionRanges, selectionRanges,
selectionRangeTypeMap,
isShiftDown, isShiftDown,
}) })
if (!eventInfo) return if (!eventInfo) return
@ -226,3 +243,7 @@ export const TextEditor = ({
</div> </div>
) )
} }
function stringifyRanges(ranges: readonly SelectionRange[]): string {
return ranges.map(({ to, from }) => `${to}->${from}`).join('&')
}

View File

@ -1,6 +1,7 @@
import { executeAst, executeCode } from 'useStore' import { executeAst, executeCode } from 'useStore'
import { Selections } from 'lib/selections' import { Selections } from 'lib/selections'
import { KCLError } from './errors' import { KCLError } from './errors'
import { v4 as uuidv4 } from 'uuid'
import { import {
EngineCommandManager, EngineCommandManager,
engineCommandManager, engineCommandManager,
@ -14,6 +15,8 @@ import {
Program, Program,
ProgramMemory, ProgramMemory,
recast, recast,
SketchGroup,
ExtrudeGroup,
} from 'lang/wasm' } from 'lang/wasm'
import { bracket } from 'lib/exampleKcl' import { bracket } from 'lib/exampleKcl'
import { createContext, useContext, useEffect, useState } from 'react' import { createContext, useContext, useEffect, useState } from 'react'
@ -235,7 +238,6 @@ class KclManager {
updateCode = false, updateCode = false,
executionId?: number executionId?: number
) { ) {
console.trace('executeAst')
const currentExecutionId = executionId || Date.now() const currentExecutionId = executionId || Date.now()
this._cancelTokens.set(currentExecutionId, false) this._cancelTokens.set(currentExecutionId, false)
@ -245,6 +247,7 @@ class KclManager {
ast, ast,
engineCommandManager: this.engineCommandManager, engineCommandManager: this.engineCommandManager,
}) })
enterEditMode(programMemory)
this.isExecuting = false this.isExecuting = false
// Check the cancellation token for this execution before applying side effects // Check the cancellation token for this execution before applying side effects
if (this._cancelTokens.get(currentExecutionId)) { if (this._cancelTokens.get(currentExecutionId)) {
@ -333,6 +336,7 @@ class KclManager {
} }
if (!result.isChange) return if (!result.isChange) return
const { logs, errors, programMemory, ast } = result const { logs, errors, programMemory, ast } = result
enterEditMode(programMemory)
this.logs = logs this.logs = logs
this.kclErrors = errors this.kclErrors = errors
this.programMemory = programMemory this.programMemory = programMemory
@ -520,3 +524,18 @@ function safteLSSetItem(key: string, value: string) {
if (typeof window === 'undefined') return if (typeof window === 'undefined') return
localStorage?.setItem(key, value) localStorage?.setItem(key, value)
} }
function enterEditMode(programMemory: ProgramMemory) {
const firstSketchOrExtrudeGroup = Object.values(programMemory.root).find(
(node) => node.type === 'ExtrudeGroup' || node.type === 'SketchGroup'
) as SketchGroup | ExtrudeGroup
firstSketchOrExtrudeGroup &&
engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'edit_mode_enter',
target: firstSketchOrExtrudeGroup.id,
},
})
}

View File

@ -74,16 +74,56 @@ const mySketch001 = startSketchOn('XY')
expect(sketch001).toEqual({ expect(sketch001).toEqual({
type: 'ExtrudeGroup', type: 'ExtrudeGroup',
id: expect.any(String), id: expect.any(String),
value: [], value: [
{
type: 'extrudePlane',
position: [0, 0, 0],
rotation: [0, 0, 0, 1],
faceId: expect.any(String),
name: '',
id: expect.any(String),
sourceRange: [77, 102],
},
{
type: 'extrudePlane',
position: [0, 0, 0],
rotation: [0, 0, 0, 1],
faceId: expect.any(String),
name: '',
id: expect.any(String),
sourceRange: [108, 132],
},
],
sketchGroupValues: [
{
type: 'ToPoint',
from: [0, 0],
to: [-1.59, -1.54],
name: '',
__geoMeta: {
id: expect.any(String),
sourceRange: [77, 102],
},
},
{
type: 'ToPoint',
from: [-1.59, -1.54],
to: [0.46, -5.82],
name: '',
__geoMeta: {
id: expect.any(String),
sourceRange: [108, 132],
},
},
],
height: 2, height: 2,
position: [0, 0, 0], position: [0, 0, 0],
rotation: [0, 0, 0, 1], rotation: [0, 0, 0, 1],
endCapId: null,
startCapId: null,
sketchGroupValues: expect.any(Array),
xAxis: { x: 1, y: 0, z: 0 }, xAxis: { x: 1, y: 0, z: 0 },
yAxis: { x: 0, y: 1, z: 0 }, yAxis: { x: 0, y: 1, z: 0 },
zAxis: { x: 0, y: 0, z: 1 }, zAxis: { x: 0, y: 0, z: 1 },
startCapId: expect.any(String),
endCapId: expect.any(String),
__meta: [{ sourceRange: [46, 71] }], __meta: [{ sourceRange: [46, 71] }],
}) })
}) })
@ -117,32 +157,149 @@ const sk2 = startSketchOn('XY')
{ {
type: 'ExtrudeGroup', type: 'ExtrudeGroup',
id: expect.any(String), id: expect.any(String),
value: [], value: [
{
type: 'extrudePlane',
position: [0, 0, 0],
rotation: [0, 0, 0, 1],
faceId: expect.any(String),
name: '',
id: expect.any(String),
sourceRange: [69, 89],
},
{
type: 'extrudePlane',
position: [0, 0, 0],
rotation: [0, 0, 0, 1],
faceId: expect.any(String),
name: 'p',
id: expect.any(String),
sourceRange: [95, 118],
},
{
type: 'extrudePlane',
position: [0, 0, 0],
rotation: [0, 0, 0, 1],
faceId: expect.any(String),
name: '',
id: expect.any(String),
sourceRange: [124, 143],
},
],
sketchGroupValues: [
{
type: 'ToPoint',
from: [0, 0],
to: [-2.5, 0],
name: '',
__geoMeta: {
id: expect.any(String),
sourceRange: [69, 89],
},
},
{
type: 'ToPoint',
from: [-2.5, 0],
to: [0, 10],
name: 'p',
__geoMeta: {
id: expect.any(String),
sourceRange: [95, 118],
},
},
{
type: 'ToPoint',
from: [0, 10],
to: [2.5, 0],
name: '',
__geoMeta: {
id: expect.any(String),
sourceRange: [124, 143],
},
},
],
height: 2, height: 2,
position: [0, 0, 0], position: [0, 0, 0],
rotation: [0, 0, 0, 1], rotation: [0, 0, 0, 1],
endCapId: null,
startCapId: null,
sketchGroupValues: expect.any(Array),
xAxis: { x: 1, y: 0, z: 0 }, xAxis: { x: 1, y: 0, z: 0 },
yAxis: { x: 0, y: 1, z: 0 }, yAxis: { x: 0, y: 1, z: 0 },
zAxis: { x: 0, y: 0, z: 1 }, zAxis: { x: 0, y: 0, z: 1 },
startCapId: expect.any(String),
endCapId: expect.any(String),
__meta: [{ sourceRange: [38, 63] }], __meta: [{ sourceRange: [38, 63] }],
}, },
{ {
type: 'ExtrudeGroup', type: 'ExtrudeGroup',
id: expect.any(String), id: expect.any(String),
value: [], value: [
{
type: 'extrudePlane',
position: [0, 0, 0],
rotation: [0, 0, 0, 1],
faceId: expect.any(String),
name: '',
id: expect.any(String),
sourceRange: [374, 394],
},
{
type: 'extrudePlane',
position: [0, 0, 0],
rotation: [0, 0, 0, 1],
faceId: expect.any(String),
name: 'p',
id: expect.any(String),
sourceRange: [400, 422],
},
{
type: 'extrudePlane',
position: [0, 0, 0],
rotation: [0, 0, 0, 1],
faceId: expect.any(String),
name: '',
id: expect.any(String),
sourceRange: [428, 447],
},
],
sketchGroupValues: [
{
type: 'ToPoint',
from: [0, 0],
to: [-2.5, 0],
name: '',
__geoMeta: {
id: expect.any(String),
sourceRange: [374, 394],
},
},
{
type: 'ToPoint',
from: [-2.5, 0],
to: [0, 3],
name: 'p',
__geoMeta: {
id: expect.any(String),
sourceRange: [400, 422],
},
},
{
type: 'ToPoint',
from: [0, 3],
to: [2.5, 0],
name: '',
__geoMeta: {
id: expect.any(String),
sourceRange: [428, 447],
},
},
],
height: 2, height: 2,
position: [0, 0, 0], position: [0, 0, 0],
rotation: [0, 0, 0, 1], rotation: [0, 0, 0, 1],
endCapId: null,
startCapId: null,
sketchGroupValues: expect.any(Array),
xAxis: { x: 1, y: 0, z: 0 }, xAxis: { x: 1, y: 0, z: 0 },
yAxis: { x: 0, y: 1, z: 0 }, yAxis: { x: 0, y: 1, z: 0 },
zAxis: { x: 0, y: 0, z: 1 }, zAxis: { x: 0, y: 0, z: 1 },
startCapId: expect.any(String),
endCapId: expect.any(String),
__meta: [{ sourceRange: [343, 368] }], __meta: [{ sourceRange: [343, 368] }],
}, },
]) ])

View File

@ -12,8 +12,10 @@ import {
addSketchTo, addSketchTo,
giveSketchFnCallTag, giveSketchFnCallTag,
moveValueIntoNewVariable, moveValueIntoNewVariable,
sketchOnExtrudedFace,
} from './modifyAst' } from './modifyAst'
import { enginelessExecutor } from '../lib/testHelpers' import { enginelessExecutor } from '../lib/testHelpers'
import { getNodePathFromSourceRange } from './queryAst'
beforeAll(() => initPromise) beforeAll(() => initPromise)
@ -274,3 +276,89 @@ const yo2 = hmm([identifierGuy + 5])`
expect(newCode).toContain(`const yo2 = hmm([newVar])`) expect(newCode).toContain(`const yo2 = hmm([newVar])`)
}) })
}) })
describe('testing sketchOnExtrudedFace', () => {
test('it should be able to extrude on regular segments', async () => {
const code = `const part001 = startSketchOn('-XZ')
|> startProfileAt([3.58, 2.06], %)
|> line([9.7, 9.19], %)
|> line([8.62, -9.57], %)
|> close(%)
|> extrude(5 + 7, %)`
const ast = parse(code)
const programMemory = await enginelessExecutor(ast)
const snippet = `line([9.7, 9.19], %)`
const range: [number, number] = [
code.indexOf(snippet),
code.indexOf(snippet) + snippet.length,
]
const pathToNode = getNodePathFromSourceRange(ast, range)
const { modifiedAst } = sketchOnExtrudedFace(ast, pathToNode, programMemory)
const newCode = recast(modifiedAst)
expect(newCode).toContain(`const part001 = startSketchOn('-XZ')
|> startProfileAt([3.58, 2.06], %)
|> line([9.7, 9.19], %, 'seg01')
|> line([8.62, -9.57], %)
|> close(%)
|> extrude(5 + 7, %)
const part002 = startSketchOn(part001, 'seg01')`)
})
test('it should be able to extrude on close segments', async () => {
const code = `const part001 = startSketchOn('-XZ')
|> startProfileAt([3.58, 2.06], %)
|> line([9.7, 9.19], %)
|> line([8.62, -9.57], %)
|> close(%)
|> extrude(5 + 7, %)`
const ast = parse(code)
const programMemory = await enginelessExecutor(ast)
const snippet = `close(%)`
const range: [number, number] = [
code.indexOf(snippet),
code.indexOf(snippet) + snippet.length,
]
const pathToNode = getNodePathFromSourceRange(ast, range)
const { modifiedAst } = sketchOnExtrudedFace(ast, pathToNode, programMemory)
const newCode = recast(modifiedAst)
expect(newCode).toContain(`const part001 = startSketchOn('-XZ')
|> startProfileAt([3.58, 2.06], %)
|> line([9.7, 9.19], %)
|> line([8.62, -9.57], %)
|> close(%, 'seg01')
|> extrude(5 + 7, %)
const part002 = startSketchOn(part001, 'seg01')`)
})
test('it should be able to extrude on start-end caps', async () => {
const code = `const part001 = startSketchOn('-XZ')
|> startProfileAt([3.58, 2.06], %)
|> line([9.7, 9.19], %)
|> line([8.62, -9.57], %)
|> close(%)
|> extrude(5 + 7, %)`
const ast = parse(code)
const programMemory = await enginelessExecutor(ast)
const snippet = `startProfileAt([3.58, 2.06], %)`
const range: [number, number] = [
code.indexOf(snippet),
code.indexOf(snippet) + snippet.length,
]
const pathToNode = getNodePathFromSourceRange(ast, range)
const { modifiedAst } = sketchOnExtrudedFace(
ast,
pathToNode,
programMemory,
'end'
)
const newCode = recast(modifiedAst)
expect(newCode).toContain(`const part001 = startSketchOn('-XZ')
|> startProfileAt([3.58, 2.06], %)
|> line([9.7, 9.19], %)
|> line([8.62, -9.57], %)
|> close(%)
|> extrude(5 + 7, %)
const part002 = startSketchOn(part001, 'END')`)
})
})

View File

@ -1,4 +1,3 @@
import { ToolTip } from '../useStore'
import { Selection } from 'lib/selections' import { Selection } from 'lib/selections'
import { import {
Program, Program,
@ -64,7 +63,6 @@ export function addStartProfileAt(
pathToNode: PathToNode, pathToNode: PathToNode,
at: [number, number] at: [number, number]
): { modifiedAst: Program; pathToNode: PathToNode } { ): { modifiedAst: Program; pathToNode: PathToNode } {
console.log('addStartProfileAt called')
const variableDeclaration = getNodeFromPath<VariableDeclaration>( const variableDeclaration = getNodeFromPath<VariableDeclaration>(
node, node,
pathToNode, pathToNode,
@ -317,12 +315,12 @@ export function extrudeSketch(
export function sketchOnExtrudedFace( export function sketchOnExtrudedFace(
node: Program, node: Program,
pathToNode: PathToNode, pathToNode: PathToNode,
programMemory: ProgramMemory programMemory: ProgramMemory,
cap: 'none' | 'start' | 'end' = 'none'
): { modifiedAst: Program; pathToNode: PathToNode } { ): { modifiedAst: Program; pathToNode: PathToNode } {
let _node = { ...node } let _node = { ...node }
const newSketchName = findUniqueName(node, 'part') const newSketchName = findUniqueName(node, 'part')
const { node: oldSketchNode, shallowPath: pathToOldSketch } = const { node: oldSketchNode } = getNodeFromPath<VariableDeclarator>(
getNodeFromPath<VariableDeclarator>(
_node, _node,
pathToNode, pathToNode,
'VariableDeclarator', 'VariableDeclarator',
@ -335,6 +333,8 @@ export function sketchOnExtrudedFace(
'CallExpression' 'CallExpression'
) )
let _tag = ''
if (cap === 'none') {
const { modifiedAst, tag } = addTagForSketchOnFace( const { modifiedAst, tag } = addTagForSketchOnFace(
{ {
previousProgramMemory: programMemory, previousProgramMemory: programMemory,
@ -343,34 +343,34 @@ export function sketchOnExtrudedFace(
}, },
expression.callee.name expression.callee.name
) )
_tag = tag
_node = modifiedAst _node = modifiedAst
} else {
_tag = cap.toUpperCase()
}
const newSketch = createVariableDeclaration( const newSketch = createVariableDeclaration(
newSketchName, newSketchName,
createPipeExpression([ createCallExpressionStdLib('startSketchOn', [
createCallExpressionStdLib('startSketchAt', [
createArrayExpression([createLiteral(0), createLiteral(0)]),
]),
createCallExpressionStdLib('lineTo', [
createArrayExpression([createLiteral(1), createLiteral(1)]),
createPipeSubstitution(),
]),
createCallExpression('transform', [
createCallExpressionStdLib('getExtrudeWallTransform', [
createLiteral(tag),
createIdentifier(oldSketchName), createIdentifier(oldSketchName),
]), createLiteral(_tag),
createPipeSubstitution(),
]),
]), ]),
'const' 'const'
) )
const expressionIndex = getLastIndex(pathToOldSketch)
const expressionIndex = pathToNode[1][0] as number
_node.body.splice(expressionIndex + 1, 0, newSketch) _node.body.splice(expressionIndex + 1, 0, newSketch)
const newpathToNode: PathToNode = [
['body', ''],
[expressionIndex + 1, 'index'],
['declarations', 'VariableDeclaration'],
[0, 'index'],
['init', 'VariableDeclarator'],
]
return { return {
modifiedAst: _node, modifiedAst: _node,
pathToNode: [...pathToNode.slice(0, -1), [expressionIndex, 'index']], pathToNode: newpathToNode,
} }
} }

View File

@ -150,7 +150,7 @@ log(5, myVar)
const recasted = recast(ast) const recasted = recast(ast)
expect(recasted.trim()).toBe(code.trim()) expect(recasted.trim()).toBe(code.trim())
}) })
it('recast long object exectution', () => { it('recast long object execution', () => {
const code = `const three = 3 const code = `const three = 3
const yo = { const yo = {
aStr: 'str', aStr: 'str',
@ -163,7 +163,7 @@ const yo = {
const recasted = recast(ast) const recasted = recast(ast)
expect(recasted).toBe(code) expect(recasted).toBe(code)
}) })
it('recast short object exectution', () => { it('recast short object execution', () => {
const code = `const yo = { key: 'val' } const code = `const yo = { key: 'val' }
` `
const { ast } = code2ast(code) const { ast } = code2ast(code)

View File

@ -13,6 +13,10 @@ interface CommandInfo {
range: SourceRange range: SourceRange
pathToNode: PathToNode pathToNode: PathToNode
parentId?: string parentId?: string
additionalData?: {
type: 'cap'
info: 'start' | 'end'
}
} }
type WebSocketResponse = Models['OkWebSocketResponseData_type'] type WebSocketResponse = Models['OkWebSocketResponseData_type']
@ -1069,14 +1073,42 @@ export class EngineCommandManager {
} as const } as const
this.artifactMap[id] = artifact this.artifactMap[id] = artifact
if ( if (
command.commandType === 'entity_linear_pattern' || (command.commandType === 'entity_linear_pattern' &&
command.commandType === 'entity_circular_pattern' modelingResponse.type === 'entity_linear_pattern') ||
(command.commandType === 'entity_circular_pattern' &&
modelingResponse.type === 'entity_circular_pattern')
) { ) {
const entities = (modelingResponse as any)?.data?.entity_ids const entities = modelingResponse.data.entity_ids
entities?.forEach((entity: string) => { entities?.forEach((entity: string) => {
this.artifactMap[entity] = artifact this.artifactMap[entity] = artifact
}) })
} }
if (
command?.commandType === 'solid3d_get_extrusion_face_info' &&
modelingResponse.type === 'solid3d_get_extrusion_face_info'
) {
console.log('modelingResposne', modelingResponse)
const parent = this.artifactMap[command?.parentId || '']
modelingResponse.data.faces.forEach((face) => {
if (face.cap !== 'none' && face.face_id && parent) {
this.artifactMap[face.face_id] = {
...parent,
commandType: 'solid3d_get_extrusion_face_info',
additionalData: {
type: 'cap',
info: face.cap === 'bottom' ? 'start' : 'end',
},
}
}
const curveArtifact = this.artifactMap[face?.curve_id || '']
if (curveArtifact && face?.face_id) {
this.artifactMap[face.face_id] = {
...curveArtifact,
commandType: 'solid3d_get_extrusion_face_info',
}
}
})
}
resolve({ resolve({
id, id,
commandType: command.commandType, commandType: command.commandType,
@ -1388,12 +1420,6 @@ export class EngineCommandManager {
const promise = new Promise((_resolve, reject) => { const promise = new Promise((_resolve, reject) => {
resolve = _resolve resolve = _resolve
}) })
const getParentId = (): string | undefined => {
if (command.type === 'extend_path') {
return command.path
}
// TODO handle other commands that have a parent
}
const pathToNode = ast const pathToNode = ast
? getNodePathFromSourceRange(ast, range || [0, 0]) ? getNodePathFromSourceRange(ast, range || [0, 0])
: [] : []
@ -1402,7 +1428,6 @@ export class EngineCommandManager {
pathToNode, pathToNode,
type: 'pending', type: 'pending',
commandType: command.type, commandType: command.type,
parentId: getParentId(),
promise, promise,
resolve, resolve,
} }
@ -1419,10 +1444,14 @@ export class EngineCommandManager {
resolve = _resolve resolve = _resolve
}) })
const getParentId = (): string | undefined => { const getParentId = (): string | undefined => {
if (command.type === 'extend_path') { if (command.type === 'extend_path') return command.path
return command.path if (command.type === 'solid3d_get_extrusion_face_info') {
const edgeArtifact = this.artifactMap[command.edge_id]
// edges's parent id is to the original "start_path" artifact
if (edgeArtifact?.parentId) return edgeArtifact.parentId
} }
// TODO handle other commands that have a parent if (command.type === 'close_path') return command.path_id
// handle other commands that have a parent here
} }
const pathToNode = ast const pathToNode = ast
? getNodePathFromSourceRange(ast, range || [0, 0]) ? getNodePathFromSourceRange(ast, range || [0, 0])

View File

@ -1155,11 +1155,14 @@ export function addTagForSketchOnFace(
a: ModifyAstBase, a: ModifyAstBase,
expressionName: string expressionName: string
) { ) {
if (expressionName === 'close') {
return addTag(1)(a)
}
if (expressionName in sketchLineHelperMap) { if (expressionName in sketchLineHelperMap) {
const { addTag } = sketchLineHelperMap[expressionName] const { addTag } = sketchLineHelperMap[expressionName]
return addTag(a) return addTag(a)
} }
throw new Error('not a sketch line helper') throw new Error(`"${expressionName}" is not a sketch line helper`)
} }
function isAngleLiteral(lineArugement: Value): boolean { function isAngleLiteral(lineArugement: Value): boolean {
@ -1174,7 +1177,7 @@ function isAngleLiteral(lineArugement: Value): boolean {
type addTagFn = (a: ModifyAstBase) => { modifiedAst: Program; tag: string } type addTagFn = (a: ModifyAstBase) => { modifiedAst: Program; tag: string }
function addTag(): addTagFn { function addTag(tagIndex = 2): addTagFn {
return ({ node, pathToNode }) => { return ({ node, pathToNode }) => {
const _node = { ...node } const _node = { ...node }
const { node: primaryCallExp } = getNodeFromPath<CallExpression>( const { node: primaryCallExp } = getNodeFromPath<CallExpression>(
@ -1184,12 +1187,12 @@ function addTag(): addTagFn {
) )
// Tag is always 3rd expression now, using arg index feels brittle // Tag is always 3rd expression now, using arg index feels brittle
// but we can come up with a better way to identify tag later. // but we can come up with a better way to identify tag later.
const thirdArg = primaryCallExp.arguments?.[2] const thirdArg = primaryCallExp.arguments?.[tagIndex]
const tagLiteral = const tagLiteral =
thirdArg || (createLiteral(findUniqueName(_node, 'seg', 2)) as Literal) thirdArg || (createLiteral(findUniqueName(_node, 'seg', 2)) as Literal)
const isTagExisting = !!thirdArg const isTagExisting = !!thirdArg
if (!isTagExisting) { if (!isTagExisting) {
primaryCallExp.arguments[2] = tagLiteral primaryCallExp.arguments[tagIndex] = tagLiteral
} }
if ('value' in tagLiteral) { if ('value' in tagLiteral) {
// Now TypeScript knows tagLiteral has a value property // Now TypeScript knows tagLiteral has a value property

View File

@ -66,6 +66,7 @@ export type { Position } from '../wasm-lib/kcl/bindings/Position'
export type { Rotation } from '../wasm-lib/kcl/bindings/Rotation' export type { Rotation } from '../wasm-lib/kcl/bindings/Rotation'
export type { Path } from '../wasm-lib/kcl/bindings/Path' export type { Path } from '../wasm-lib/kcl/bindings/Path'
export type { SketchGroup } from '../wasm-lib/kcl/bindings/SketchGroup' export type { SketchGroup } from '../wasm-lib/kcl/bindings/SketchGroup'
export type { ExtrudeGroup } from '../wasm-lib/kcl/bindings/ExtrudeGroup'
export type { MemoryItem } from '../wasm-lib/kcl/bindings/MemoryItem' export type { MemoryItem } from '../wasm-lib/kcl/bindings/MemoryItem'
export type { ExtrudeSurface } from '../wasm-lib/kcl/bindings/ExtrudeSurface' export type { ExtrudeSurface } from '../wasm-lib/kcl/bindings/ExtrudeSurface'

View File

@ -39,7 +39,7 @@ export interface MouseGuard {
rotate: MouseGuardHandler rotate: MouseGuardHandler
} }
const butName = (e: React.MouseEvent) => ({ export const butName = (e: React.MouseEvent) => ({
middle: !!(e.buttons & 4) || e.button === 1, middle: !!(e.buttons & 4) || e.button === 1,
right: !!(e.buttons & 2) || e.button === 2, right: !!(e.buttons & 2) || e.button === 2,
left: !!(e.buttons & 1) || e.button === 0, left: !!(e.buttons & 1) || e.button === 0,

View File

@ -117,7 +117,7 @@ export const modelingMachineConfig: CommandSetConfig<
args: { args: {
selection: { selection: {
inputType: 'selection', inputType: 'selection',
selectionTypes: ['face'], selectionTypes: ['extrude-wall', 'start-cap', 'end-cap'],
multiple: false, // TODO: multiple selection multiple: false, // TODO: multiple selection
required: true, required: true,
skip: true, skip: true,

View File

@ -6,7 +6,7 @@ import { v4 as uuidv4 } from 'uuid'
import { EditorSelection } from '@codemirror/state' import { EditorSelection } from '@codemirror/state'
import { kclManager } from 'lang/KclSingleton' import { kclManager } from 'lang/KclSingleton'
import { SelectionRange } from '@uiw/react-codemirror' import { SelectionRange } from '@uiw/react-codemirror'
import { isOverlap } from 'lib/utils' import { getNormalisedCoordinates, isOverlap } from 'lib/utils'
import { isCursorInSketchCommandRange } from 'lang/util' import { isCursorInSketchCommandRange } from 'lang/util'
import { Program } from 'lang/wasm' import { Program } from 'lang/wasm'
import { import {
@ -22,70 +22,12 @@ import {
getParentGroup, getParentGroup,
PROFILE_START, PROFILE_START,
} from 'clientSideScene/sceneEntities' } from 'clientSideScene/sceneEntities'
import { Mesh } from 'three' import { Mesh, Object3D, Object3DEventMap } from 'three'
import { AXIS_GROUP, X_AXIS } from 'clientSideScene/sceneInfra' import { AXIS_GROUP, X_AXIS } from 'clientSideScene/sceneInfra'
export const X_AXIS_UUID = 'ad792545-7fd3-482a-a602-a93924e3055b' export const X_AXIS_UUID = 'ad792545-7fd3-482a-a602-a93924e3055b'
export const Y_AXIS_UUID = '680fd157-266f-4b8a-984f-cdf46b8bdf01' export const Y_AXIS_UUID = '680fd157-266f-4b8a-984f-cdf46b8bdf01'
/*
How selections work is complex due to the nature that we rely on the engine
to tell what has been selected after we send a click command. But than the
app needs these selections to be based on cursors, therefore the app must
be in control of selections. On top of that because we need to set cursor
positions in code-mirror for selections, both from app logic, and still
allow the user to add multiple cursors like a normal editor, it's best to
let code mirror control cursor positions and associate those source ranges
with entity ids from code-mirror events later.
So it's a lot of back and forth. conceptually the back and forth is:
1) we send a click command to the engine
2) the engine sends back ids of entities that were clicked
3) we associate that source ranges with those ids
4) we set the codemirror selection based on those source ranges (taking
into account if the user is holding shift to add to current selections
or not). we also create and remember a SelectionRangeTypeMap
5) Code mirror fires a an event that cursors have changed, we loop through
these ranges and associate them with entity ids again with the ArtifactMap,
but also we can pick up selection types using the SelectionRangeTypeMap
6) we clear all previous selections in the engine and set the new ones
The above is less likely to get stale but below is some more details,
because this wonders all over the code-base, I've tried to centeralise it
by putting relevant utils in this file. All of the functions below are
pure with the exception of getEventForSelectWithPoint which makes a call
to the engine, but it's a query call (not mutation) so I'm okay with this.
Actual side effects that change cursors or tell the engine what's selected
are still done throughout the in their relevant parts in the codebase.
In detail:
1) Click commands are mostly sent in stream.tsx search for
"select_with_point"
2) The handler for when the engine sends back entity ids calls
getEventForSelectWithPoint, it fires an XState event to update our
selections is xstate context
3 and 4) The XState handler for the above uses handleSelectionBatch and
handleSelectionWithShift to update the selections in xstate context as
well as returning our SelectionRangeTypeMap and a codeMirror specific
event to be dispatched.
5) The codeMirror handler for changes to the cursor uses
processCodeMirrorRanges to associate the ranges back with their original
types and the entity ids (the id can vary depending on the type, as
there's only one source range for a given segment, but depending on if
the user selected the segment directly or the vertex, the id will be
different)
6) We take all of the ids and create events for the engine with
resetAndSetEngineEntitySelectionCmds
An important note is that if a user changes the cursor directly themselves
then they skip directly to step 5, And these selections get a type of
"default".
There are a few more nuances than this, but best to find them in the code.
*/
export type Axis = 'y-axis' | 'x-axis' | 'z-axis' export type Axis = 'y-axis' | 'x-axis' | 'z-axis'
export type Selection = { export type Selection = {
@ -93,7 +35,9 @@ export type Selection = {
| 'default' | 'default'
| 'line-end' | 'line-end'
| 'line-mid' | 'line-mid'
| 'face' | 'extrude-wall'
| 'start-cap'
| 'end-cap'
| 'point' | 'point'
| 'edge' | 'edge'
| 'line' | 'line'
@ -106,15 +50,6 @@ export type Selections = {
codeBasedSelections: Selection[] codeBasedSelections: Selection[]
} }
export interface SelectionRangeTypeMap {
[key: number]: Selection['type']
}
interface RangeAndId {
id: string
range: SourceRange
}
export async function getEventForSelectWithPoint( export async function getEventForSelectWithPoint(
{ {
data, data,
@ -139,8 +74,32 @@ export async function getEventForSelectWithPoint(
}, },
} }
} }
const sourceRange = engineCommandManager.artifactMap[data.entity_id]?.range const _artifact = engineCommandManager.artifactMap[data.entity_id]
if (engineCommandManager.artifactMap[data.entity_id]) { const sourceRange = _artifact?.range
if (_artifact) {
if (_artifact.commandType === 'solid3d_get_extrusion_face_info') {
if (_artifact?.additionalData)
return {
type: 'Set selection',
data: {
selectionType: 'singleCodeCursor',
selection: {
range: sourceRange,
type:
_artifact?.additionalData.info === 'end'
? 'end-cap'
: 'start-cap',
},
},
}
return {
type: 'Set selection',
data: {
selectionType: 'singleCodeCursor',
selection: { range: sourceRange, type: 'extrude-wall' },
},
}
}
return { return {
type: 'Set selection', type: 'Set selection',
data: { data: {
@ -148,46 +107,17 @@ export async function getEventForSelectWithPoint(
selection: { range: sourceRange, type: 'default' }, selection: { range: sourceRange, type: 'default' },
}, },
} }
} } else {
if (!sketchEnginePathId) return null // if we don't recognise the entity, select nothing
// selected a vertex
const res = await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'path_get_curve_uuids_for_vertices',
vertex_ids: [data.entity_id],
path_id: sketchEnginePathId,
},
})
const curveIds = res?.data?.data?.curve_ids
const ranges: RangeAndId[] = curveIds
.map(
(id: string): RangeAndId => ({
id,
range: engineCommandManager.artifactMap[id].range,
})
)
.sort((a: RangeAndId, b: RangeAndId) => a.range[0] - b.range[0])
// default to the head of the curve selected
const _sourceRange = ranges?.[0].range
const artifact = engineCommandManager.artifactMap[ranges?.[0]?.id]
if (artifact.type === 'result') {
artifact.headVertexId = data.entity_id
}
return { return {
type: 'Set selection', type: 'Set selection',
data: { data: { selectionType: 'singleCodeCursor' },
selectionType: 'singleCodeCursor', }
// line-end is used to indicate that headVertexId should be sent to the engine as "selected"
// not the whole curve
selection: { range: _sourceRange, type: 'line-end' },
},
} }
} }
export function getEventForSegmentSelection( export function getEventForSegmentSelection(
obj: any obj: Object3D<Object3DEventMap>
): ModelingMachineEvent | null { ): ModelingMachineEvent | null {
const group = getParentGroup(obj, [ const group = getParentGroup(obj, [
STRAIGHT_SEGMENT, STRAIGHT_SEGMENT,
@ -231,107 +161,54 @@ export function handleSelectionBatch({
}: { }: {
selections: Selections selections: Selections
}): { }): {
selectionRangeTypeMap: SelectionRangeTypeMap engineEvents: Models['WebSocketRequest_type'][]
codeMirrorSelection?: EditorSelection codeMirrorSelection: EditorSelection
otherSelections: Axis[] otherSelections: Axis[]
updateSceneObjectColors: () => void
} { } {
const ranges: ReturnType<typeof EditorSelection.cursor>[] = [] const ranges: ReturnType<typeof EditorSelection.cursor>[] = []
const selectionRangeTypeMap: SelectionRangeTypeMap = {} const engineEvents: Models['WebSocketRequest_type'][] =
resetAndSetEngineEntitySelectionCmds(
codeToIdSelections(selections.codeBasedSelections)
)
selections.codeBasedSelections.forEach(({ range, type }) => { selections.codeBasedSelections.forEach(({ range, type }) => {
if (range?.[1]) { if (range?.[1]) {
ranges.push(EditorSelection.cursor(range[1])) ranges.push(EditorSelection.cursor(range[1]))
selectionRangeTypeMap[range[1]] = type
} }
}) })
if (ranges.length) if (ranges.length)
return { return {
selectionRangeTypeMap, engineEvents,
codeMirrorSelection: EditorSelection.create( codeMirrorSelection: EditorSelection.create(
ranges, ranges,
selections.codeBasedSelections.length - 1 selections.codeBasedSelections.length - 1
), ),
otherSelections: selections.otherSelections, otherSelections: selections.otherSelections,
updateSceneObjectColors: () =>
updateSceneObjectColors(selections.codeBasedSelections),
} }
return { return {
selectionRangeTypeMap, codeMirrorSelection: EditorSelection.create(
[EditorSelection.cursor(kclManager.code.length)],
0
),
engineEvents,
otherSelections: selections.otherSelections, otherSelections: selections.otherSelections,
updateSceneObjectColors: () =>
updateSceneObjectColors(selections.codeBasedSelections),
} }
} }
export function handleSelectionWithShift({
codeSelection,
otherSelection,
currentSelections,
isShiftDown,
}: {
codeSelection?: Selection
otherSelection?: Axis
currentSelections: Selections
isShiftDown: boolean
}): {
selectionRangeTypeMap: SelectionRangeTypeMap
otherSelections: Axis[]
codeMirrorSelection?: EditorSelection
} {
const code = kclManager.code
if (codeSelection && otherSelection) {
throw new Error('cannot have both code and other selection')
}
if (!codeSelection && !otherSelection) {
return handleSelectionBatch({
selections: {
otherSelections: [],
codeBasedSelections: [
{
range: [0, code.length ? code.length : 0],
type: 'default',
},
],
},
})
}
if (otherSelection) {
return handleSelectionBatch({
selections: {
codeBasedSelections: isShiftDown
? currentSelections.codeBasedSelections
: [
{
range: [0, code.length ? code.length : 0],
type: 'default',
},
],
otherSelections: [otherSelection],
},
})
}
const isEndOfFileDumbySelection =
currentSelections.codeBasedSelections.length === 1 &&
currentSelections.codeBasedSelections[0].range[0] === kclManager.code.length
const newCodeBasedSelections = !isShiftDown
? [codeSelection!]
: isEndOfFileDumbySelection
? [codeSelection!]
: [...currentSelections.codeBasedSelections, codeSelection!]
const selections: Selections = {
otherSelections: isShiftDown ? currentSelections.otherSelections : [],
codeBasedSelections: newCodeBasedSelections,
}
return handleSelectionBatch({ selections })
}
type SelectionToEngine = { type: Selection['type']; id: string } type SelectionToEngine = { type: Selection['type']; id: string }
export function processCodeMirrorRanges({ export function processCodeMirrorRanges({
codeMirrorRanges, codeMirrorRanges,
selectionRanges, selectionRanges,
selectionRangeTypeMap,
isShiftDown, isShiftDown,
}: { }: {
codeMirrorRanges: readonly SelectionRange[] codeMirrorRanges: readonly SelectionRange[]
selectionRanges: Selections selectionRanges: Selections
selectionRangeTypeMap: SelectionRangeTypeMap
isShiftDown: boolean isShiftDown: boolean
}): null | { }): null | {
modelingEvent: ModelingMachineEvent modelingEvent: ModelingMachineEvent
@ -349,41 +226,13 @@ export function processCodeMirrorRanges({
if (!isChange) return null if (!isChange) return null
const codeBasedSelections: Selections['codeBasedSelections'] = const codeBasedSelections: Selections['codeBasedSelections'] =
codeMirrorRanges.map(({ from, to }) => { codeMirrorRanges.map(({ from, to }) => {
if (selectionRangeTypeMap[to]) {
return {
type: selectionRangeTypeMap[to],
range: [from, to],
}
}
return { return {
type: 'default', type: 'default',
range: [from, to], range: [from, to],
} }
}) })
const idBasedSelections: SelectionToEngine[] = codeBasedSelections const idBasedSelections: SelectionToEngine[] =
.flatMap(({ type, range }): null | SelectionToEngine[] => { codeToIdSelections(codeBasedSelections)
// TODO #868: loops over all artifacts will become inefficient at a large scale
const entriesWithOverlap = Object.entries(
engineCommandManager.artifactMap || {}
).filter(([_, artifact]) => {
return artifact.range && isOverlap(artifact.range, range)
? artifact
: false
})
if (entriesWithOverlap.length) {
return entriesWithOverlap.map(([id, artifact]) => ({
type,
id:
type === 'line-end' &&
artifact.type === 'result' &&
artifact.headVertexId
? artifact.headVertexId
: id,
}))
}
return null
})
.filter(Boolean) as any
if (!selectionRanges) return null if (!selectionRanges) return null
updateSceneObjectColors(codeBasedSelections) updateSceneObjectColors(codeBasedSelections)
@ -486,24 +335,21 @@ export type CommonASTNode = {
ast: Program ast: Program
} }
export function buildCommonNodeFromSelection( function buildCommonNodeFromSelection(selectionRanges: Selections, i: number) {
selectionRanges: Selections,
i: number
) {
return { return {
selection: selectionRanges.codeBasedSelections[i], selection: selectionRanges.codeBasedSelections[i],
ast: kclManager.ast, ast: kclManager.ast,
} }
} }
export function nodeHasExtrude(node: CommonASTNode) { function nodeHasExtrude(node: CommonASTNode) {
return doesPipeHaveCallExp({ return doesPipeHaveCallExp({
calleeName: 'extrude', calleeName: 'extrude',
...node, ...node,
}) })
} }
export function nodeHasClose(node: CommonASTNode) { function nodeHasClose(node: CommonASTNode) {
return doesPipeHaveCallExp({ return doesPipeHaveCallExp({
calleeName: 'close', calleeName: 'close',
...node, ...node,
@ -521,7 +367,7 @@ export function canExtrudeSelection(selection: Selections) {
) )
} }
export function canExtrudeSelectionItem(selection: Selections, i: number) { function canExtrudeSelectionItem(selection: Selections, i: number) {
const commonNode = buildCommonNodeFromSelection(selection, i) const commonNode = buildCommonNodeFromSelection(selection, i)
return ( return (
@ -547,7 +393,7 @@ export function getSelectionType(
return selection.codeBasedSelections return selection.codeBasedSelections
.map((s, i) => { .map((s, i) => {
if (canExtrudeSelectionItem(selection, i)) { if (canExtrudeSelectionItem(selection, i)) {
return ['face', 1] as ResolvedSelectionType // This is implicitly determining what a face is, which is bad return ['extrude-wall', 1] as ResolvedSelectionType // This is implicitly determining what a face is, which is bad
} else { } else {
return ['other', 1] as ResolvedSelectionType return ['other', 1] as ResolvedSelectionType
} }
@ -590,3 +436,100 @@ export function canSubmitSelectionArg(
}) })
) )
} }
function codeToIdSelections(
codeBasedSelections: Selection[]
): SelectionToEngine[] {
return codeBasedSelections
.flatMap(({ type, range, ...rest }): null | SelectionToEngine[] => {
// TODO #868: loops over all artifacts will become inefficient at a large scale
const entriesWithOverlap = Object.entries(
engineCommandManager.artifactMap || {}
)
.map(([id, artifact]) => {
return artifact.range && isOverlap(artifact.range, range)
? {
artifact,
selection: { type, range, ...rest },
id,
}
: false
})
.filter(Boolean)
let bestCandidate
entriesWithOverlap.forEach((entry) => {
if (!entry) return
if (
type === 'default' &&
entry.artifact.commandType === 'extend_path'
) {
bestCandidate = entry
return
}
if (
type === 'start-cap' &&
entry.artifact.commandType === 'solid3d_get_extrusion_face_info' &&
entry?.artifact?.additionalData?.info === 'start'
) {
bestCandidate = entry
return
}
if (
type === 'end-cap' &&
entry.artifact.commandType === 'solid3d_get_extrusion_face_info' &&
entry?.artifact?.additionalData?.info === 'end'
) {
bestCandidate = entry
return
}
if (
type === 'extrude-wall' &&
entry.artifact.commandType === 'solid3d_get_extrusion_face_info'
) {
bestCandidate = entry
return
}
})
if (bestCandidate) {
const _bestCandidate = bestCandidate as {
artifact: any
selection: any
id: string
}
return [
{
type,
id: _bestCandidate.id,
},
]
}
return null
})
.filter(Boolean) as any
}
export function sendSelectEventToEngine(
e: MouseEvent | React.MouseEvent<HTMLDivElement, MouseEvent>,
el: HTMLVideoElement,
streamDimensions: { streamWidth: number; streamHeight: number }
) {
const { x, y } = getNormalisedCoordinates({
clientX: e.clientX,
clientY: e.clientY,
el,
...streamDimensions,
})
const result: Promise<Models['SelectWithPoint_type']> = engineCommandManager
.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
type: 'select_with_point',
selected_at_window: { x, y },
selection_type: 'add',
},
cmd_id: uuidv4(),
})
.then((res) => res.data.data)
return result
}

View File

@ -1,13 +1,6 @@
import { PathToNode, VariableDeclarator } from 'lang/wasm' import { PathToNode, VariableDeclarator } from 'lang/wasm'
import { engineCommandManager } from 'lang/std/engineConnection' import { Axis, Selection, Selections } from 'lib/selections'
import {
Axis,
Selection,
SelectionRangeTypeMap,
Selections,
} from 'lib/selections'
import { assign, createMachine } from 'xstate' import { assign, createMachine } from 'xstate'
import { isCursorInSketchCommandRange } from 'lang/util'
import { getNodePathFromSourceRange } from 'lang/queryAst' import { getNodePathFromSourceRange } from 'lang/queryAst'
import { kclManager } from 'lang/KclSingleton' import { kclManager } from 'lang/KclSingleton'
import { import {
@ -26,7 +19,6 @@ import {
} from 'components/Toolbar/EqualLength' } from 'components/Toolbar/EqualLength'
import { addStartProfileAt, extrudeSketch } from 'lang/modifyAst' import { addStartProfileAt, extrudeSketch } from 'lang/modifyAst'
import { getNodeFromPath } from '../lang/queryAst' import { getNodeFromPath } from '../lang/queryAst'
import { CallExpression, PipeExpression } from '../lang/wasm'
import { import {
applyConstraintEqualAngle, applyConstraintEqualAngle,
equalAngleInfo, equalAngleInfo,
@ -45,10 +37,10 @@ import { ModelingCommandSchema } from 'lib/commandBarConfigs/modelingCommandConf
import { import {
DefaultPlaneStr, DefaultPlaneStr,
sceneEntitiesManager, sceneEntitiesManager,
quaternionFromSketchGroup,
sketchGroupFromPathToNode,
} from 'clientSideScene/sceneEntities' } from 'clientSideScene/sceneEntities'
import { sceneInfra } from 'clientSideScene/sceneInfra' import { sceneInfra } from 'clientSideScene/sceneInfra'
import { Vector3 } from 'three'
import { quaternionFromUpNForward } from 'clientSideScene/helpers'
export const MODELING_PERSIST_KEY = 'MODELING_PERSIST_KEY' export const MODELING_PERSIST_KEY = 'MODELING_PERSIST_KEY'
@ -70,6 +62,13 @@ export type SetSelections =
selection: Selections selection: Selections
} }
export interface SketchDetails {
sketchPathToNode: PathToNode
zAxis: [number, number, number]
yAxis: [number, number, number]
origin: [number, number, number]
}
export type ModelingMachineEvent = export type ModelingMachineEvent =
| { | {
type: 'Enter sketch' type: 'Enter sketch'
@ -77,9 +76,24 @@ export type ModelingMachineEvent =
forceNewSketch?: boolean forceNewSketch?: boolean
} }
} }
| { type: 'Sketch On Face' }
| { | {
type: 'Select default plane' type: 'Select default plane'
data: { plane: DefaultPlaneStr; normal: [number, number, number] } data: {
zAxis: [number, number, number]
yAxis: [number, number, number]
} & (
| {
type: 'defaultPlane'
plane: DefaultPlaneStr
}
| {
type: 'extrudeFace'
position: [number, number, number]
extrudeSegmentPathToNode: PathToNode
cap: 'start' | 'end' | 'none'
}
)
} }
| { type: 'Set selection'; data: SetSelections } | { type: 'Set selection'; data: SetSelections }
| { type: 'Sketch no face' } | { type: 'Sketch no face' }
@ -109,18 +123,15 @@ export type ModelingMachineEvent =
| { type: 'Equip Line tool' } | { type: 'Equip Line tool' }
| { type: 'Equip tangential arc to' } | { type: 'Equip tangential arc to' }
| { | {
type: 'done.invoke.animate-to-face' type: 'done.invoke.animate-to-face' | 'done.invoke.animate-to-sketch'
data: { data: SketchDetails
sketchPathToNode: PathToNode
sketchNormalBackUp: [number, number, number] | null
}
} }
export type MoveDesc = { line: number; snippet: string } export type MoveDesc = { line: number; snippet: string }
export const modelingMachine = createMachine( export const modelingMachine = createMachine(
{ {
/** @xstate-layout N4IgpgJg5mDOIC5QFkD2EwBsCWA7KAxAMICGuAxlgNoAMAuoqAA6qzYAu2qujIAHogC0AdgCsAZgB04gEyjhADnEA2GgoUAWJQBoQAT0QBGGuICckmoZkbTM42YWKAvk91oMOfAQDKYdgAJYLDByTm5aBiQQFjYwniiBBFtpGhlxDRphGg1ZURlhXQMEcRLDSVF5UwV84XEK8Rc3dCw8KElsCEwwHz9A4NCuXAjeGI5B3kTBLWFJDWV5hVSZZTrlBUKjS1FZiUrDeWVDasaQdxb8ds7ugFFcdjAAJ0CAaz9yAAthqNG4iaF07bCNLiRYlGiZUyZDbFYQzfIaLSQ5Sw0TGZQnM6eNodLoEW73J6wV7sD5UQyRZisMbcP4IQRrGiSdRmfIqUxrCrQlbKcoQ4RHDTWUx5DHNLGXXHXPjsB4AVwwX0psXGCSEFThkNEymFGlECkM0MM+0ZMgUa1MwuypgRhlFHlaEpufBYD3YiuiVN+qrpdg0klMwkFaU0ermwuhwlUkiBhxUyKUazt5za3mJH2IZEomFTb0+9BGnpVoESRrNkmMga0ahMplkhqOZVL+sM4ksCj1SfFOZJ70k3Y+AEkrj0AkEugNwvnvoWad7DIGyuqOe2rPNDRCLIKaKJa6JBXrRJ2Hf3eyeh7jkCRXn0oABbMB3fwAN0enHIJEw7p+Rf4RhkpkbapNEjTRUgKfQjA0FsUnBOQJHyJQNCPC4Tz7NN3nPbpL2vII7wfAJ3lQB5sAAL24dgPy-Gd4mLP9A0kVQzW3I18mRGRDXEI1yl1LJDBWYwKhkZCU3QtDc0w4huFgGUSDwfxCOIsi7g-fwIGwaTMzAKjlVnWiECsPdNzSGgqnSAD1GhNJ2QsCpRHBXUAJbYSxJ7FzB2HIgpJkuSX1dbB30wVT1IoigtKnJVqRo399OWBR-TyFdlCgtRBUs1lymsAVYRjPdnNQs8PK8h5ZNwfwAEEACFvH8AANbTItpKx21mcQxEMGxOP-NJLJ1eL41gndagaVxTjFY9RIK3FPNwaTirkyrqoATXqr09Ka7ZrDsZQ5H2ZQtXYiDijUOKgVsvi1ErPKJvQiTptmkr-DIKAuhWn8S3SKQQRBU15H1YRTEsuy4QPbKtX+gMrtzNyMMKmbvNKrp8HYPMKQ9HSove1rpD21QANkTjREsw4Tu1KD-oRBNhEh1zJu6O74f8JhHiZ3A1PIWVMBIJ41I00LXt06KrFNWYLU6jIjlUZRLP1P1+SqAMgQRDQxGpj5oduoqHoU0jyI-TA9EenAoCGcK0YaudlhkJlQ3bAEMhWQ1UR5f9Q1hfItRKVXTxu2H7p819-L1g2P2wY3+YxujzByU1-qdsxUUNbbtj1AMqjWf9HGGpp7RQ67xN9hnYFwEgmH8dhUFq8PGs4qRlhBNQA2qMxLOyK2ZDkeZ5m3Rx2699WC7m0qi5LsuK+W03vwF97oMyfjYVa4wNEs9qZj3Hud3nOYcj72nJLhwf-DAABHWUVMRqBkari3a3KFf-qyPIJHAop51bFqTHBbccdMHefamzW5JMC5nragE9qLV3yBYdu-EdzpE3oaWsMwTCcUOBaTIahDwjUxONKGu96YHweGAW8qAXz+HIAAu4sAr5rWsHFQ41h9qaH2DoQ6RxOIWD4rYbIYIzRCSwWNXOuC-7dAAEpgEEGAPgIRZT3GoYLOoUgWyGFrKkdIYgE6HXUFbdIqRkSdwTLafhOcRJCPzpKE+2BS4ABk8BgFHqgT8YD0aNQUNZCQKgbDCnSBZTRdReSK3bnuPaRpf5mJuBY0uIUYB3GwCpLm5BR5yMSFtOKIIPZ8V+go+slhoxrFUJkhESU+5lQAO6yQIkRHWylAo8xCpQfweAABmqACAQG4GAdouAnyoFeJIGA7BBDayUhRTAggmmoCSYgYyfpCkZADGYBW4hLJQXMO2U0aROEdypkY5M0NJClPKfJSpwyVK1M0g03AzSCCPAeERSQTBObsGaQ8W8fS-CDOObrUZ4zJkIGMtsI0NoWy6lNAGSy31Nw0FUCYPIkZDHZ12ahA5HBnwBwCkFXm9TxmtPaZ07pvT+mCF8m+D8YzLkTKcebPSGyrYtkUPYZEyj8hpSUEyMQcg7ClCBMUspKLiWBxqcFc52Kbl3IeSQJ5RFXmEv5QFMlzTfk0qZNuMQAExCaDSoKcsONVFaOZDyw5C1aoXKuW03AHS8D4o6YSkgAAjWAgg+DyopajSeEc-koNyTIWCcg6jrEOv+ba5YMhQSSrIbatQDUoqNTVE1LTRUPHuY8550r3l2odU6n5lLVrRQ2TMHIIIlA7m3FoHqfFpB6j2pxbIgYf47K7KJZFAQjWLTjTi81eKenWrTfawQehnWKs9XUeQFRbCIN1NCVsyIRauLsnqVsuV604Nck28qVV-CtpFQ8W5ibxWSpeW8gZ6a+0DuzW9KZnEkGsVassFYobJ18T9FC9k20do3qjQEJ6XQ21motV0rth7BBfvEVm114DvR5pSEaOocgLQaKKHBHkKi7JGgqGaWEH7Hr4G-VundSaJUpsA8B09YHnEQc4idbITUrAN3nIDec5YWzyFSHO5Y2yEUNqhqu8+yMf24stQBwlPH3gkYLGR6lBMWpWTJluZlAaOQWBWJelQ84rCYeE22hN+H92poGcJ0T05xO5s9cYGF+o1jKwOghpKVt5i0OtGIFE6Il2CJXbygITMHgszZhzLmGK6l2Oxb+ztBL3mee8-5XzDxBBnNCgZiKObkmW3ikoawr706EwDdUbYC85h7Tg76zD4WHw+c5tzIVoVNPbrFcmqVgHius0i2VmLFXKDxbNolqZyXhSpbmNA7LllsrSH5HlncwpCsuZMa5Gx5r7GYBHH0cccRFXeqjjRo4mQjh5CWQGgM5YesmVoYxPhHHl1qxm3Y8uDi8QRLLk9fCsTArxMSWeqeUzYR+n2GYcE6h5ZS0OgrFqrVHBZHZH3C7c3JADlwBwAgK2VBMjSRt60sGuT6kkMGWskJWr-TSOD2xkPoew7JKRqlub8hWzsqaR9NHtQRn+rMFYEhbBakhNufHs2ruYEkLgKVH4FtjhCMt177rrC1GkKafUvCMhan+0UJKMwFzKz1O1KCnEOeXdQA4yQAA5CuAAFVAeB2CwAIGVCAEBAgUVdIzI3dxFXzD9JqfLgpFhJS5NqFqqcsiKCUAoDXkO9f+EN8b03pBQqONJ51-SmhzAmGDKtrQep-VFF4iLVBpRTQrD7kT9gcOReNUsDyJPWQVDbkT4aYEjPIzLChboqCOeYd55J2JsnJZmJMkhJCOYshwTtUrwBaMts5irg6s507rm1YABV7sxLiQ8BJ5cBf9GF1H89fztrmG1EoLxh3-yV6gkyR93rLC6g7JNvZM-8APfn4vlp1xbsQ65w7hE79FA7jsttaE1oeRuzSMxqFOYdjUaYxPZWUVmCuDSV0E8AAeVwHbT-StX2W8Cn0EHALaUECgPYFgJNjXze30nxkZxMF1BMkyEWGfkQB3DihXDQzpU8UwQnymw+H8F538EaRIEoB6CW1UjAHYI5g805nNV+UEHanMDSRhSNEDAkGUS5DkH9BsEODmEsDgmAOwQuDIGwFvAlVaFHkZkEO6GCwE16Q0K0PuEEHLkEHYMoF+QqEAjIOyB9ShEOm2lSSsDBFBUDHUGchMO0PwF0PFXNWXyW0GEVSBEYw5R3EYkDGhDXnLFWDsHBBWBqG8Jh1MJ0PLkPj4GCh0KJFzAQJCw6R8LMIsNyJ7BsL2gYisFqAtHbjSFhGhCghywSkAN9BcKQhOF5wwHgCiDUKgFb2j0EEWDKFyEUDL3UC0B2yKEGJNC1EsFqCFGMDNGchxDAH6PX3pC0HKGRHmRDGAmhHUEXFbH1GkOMmUS9jWPwPZDihLxMHyQr0OiQQWGtEGlsHmAwwv3ymEQuPdRbAtAxySn1GB19UyxfiZTiNalSGqAAmhMwyGS+X82FXJW+JcTWFmDmUUH5BqFSHBWSABFbChW1HyHhRAMRUbXc1RT8nRViyxSRMMzbyMC0W1T7wUU3wNADTkEZFSCghlnmFsMwxjTjWRLnFcSkDUE6lrG2lMwBl23YV1HjFUHOj1BOxJM4zc0NXXU3VpIS3XyOGSAShyA21SkOinQ2jghHSqChMw2A0FLpOj11O2BWHshKCqDyFrEBjUCZGDAqFUBrzqHUwfAvneBtO1PwN1MZAyFWBVwlLXHkwR3+mUUcG2LlKzhVLO17FXQa1Kz82pMCy1I6x1PbF-3ZDslAgUJKEG3RzlnbAAnVDBw+NEify10wCFOpXsAsCGnbBsHUDWBiKjG5MEmQVtnH1TMn17EbO11zxbNzX2DKEyBKE7ItJ7OcOSD4jvR72721ADy5x5z52bNtPXyFHzXnn-GrFkCs0QDTwUOUUzzNFkC3KbN1wNztxNynOSV1FriUHan1HjxUEmIvKDWp2gyOEjC0GJN6L2UnP3NDO3C311EFAjJ3371YQbEYwOJbDbHP0YMv1n04Fv1HlfKmVDGjBjBUB+nBnXDyH+LNA5AfgMj7nQMgOt2wPQjgIIv0nNHKDqCTyZ1S2lKKCCUZyZTP3jC1C9hYIrisNWKgvdSmDkIkBWVUDqH+i-2cLkKgnsDnQmNNBSM0N8KgH8P0LYr4ikADG1CtHkFp1kJ5BMHUEjDXiONUIETaCKPSIrkkWyL8NKI+CMscAx1hCYlanZE5UnUjF5HsHIMswAnaKwpcr8IyICLsQAApyEmA9AABKNikQCQCXTJRiTQFhIoHhRTF9QJOlZU8C2K-S+K-Q-wZK1AVKtK2qlK9KzKsQT6OQUYvKiYydBESnHcawA0muYUFwFwIAA */ /** @xstate-layout N4IgpgJg5mDOIC5QFkD2EwBsCWA7KAxAMICGuAxlgNoAMAuoqAA6qzYAu2qujIAHogC0AdgCsAZgB04gEyjhADnEA2GgoUAWJQBoQAT0QBGGuICckmoZkbTM42YWKAvk91oMOfAQDKYdgAJYLDByTm5aBiQQFjYwniiBBFtpGhlxDRphGg1ZURlhXQMEcRLDSVF5UwV84XEK8Rc3dCw8KElsCEwwHz9A4NCuXAjeGI5B3kTBLWFJDWV5hVSZZTrlBUKjS1FZiUrDeWVDasaQdxb8ds7ugFFcdjAAJ0CAaz9yAAthqNG4iaFlmZWZSmZRaKyGQzKYQaDYIGSmDSzdKyDSGcQ0KrAk5nTxtDpdAi3e5PWCvdgfKiGSLMVhjbh-BCCaw0WaQrZqdGLAr6RAZGSSWzCZSiIEKNFi5TY5q4y4E658dgPACuGC+NNi4wSQgqM3hNFEwNEGlEYthENELJkCjWplMFpsGlRUo8rVlNz4LAe7DV0Vpvy1jKtCkkVuZMjsDpNsOEwjKUIkcxNqWhomd5za3jJH2IZEomEzb0+9BGfs1oESEOtkmM0K0ahMplkZqOZUrYrRlgUJrTMoL5Pekj7HwAklcegEgl0BuFi99S-SA4ZoWUdWsTSLlsozZlzNkZPrG0a5F2e66hwPz6OCcgSK8+lAALZgO7+ABuj045BImB9PzL-CMeFW2qTQhU0ZMzVRKR0RoVJ6nyJQNFPC5z0HLN3ivbobzvIJH2fAJ3lQB5sAAL24dhv1-ed4nLQDoUkVRrX1CF8ihGQzXECFymNLJIU4-U8mQjN0LQwtMOIbhYEVEg8H8QjiLIu5v38CBsCk3MwCojUF1ohArCNCwNDSDFENMI51h5OEzGUCwKgtbJRDMtEhNE-tXJHMciEk6TZPfL1sC-TAVLUiiKE02d1TpGiAL05ZgztK0RVBYxNHYyzw1qcprCODQY2EZYjRc1DL087yHhk3B-AAQQAIW8fwAA0tKihkrC7JExEMGxOPhNJYXhAy7ShRY4NMWoGlcU5pTPESSoJLzcCk8rZNq+qAE1mv9XS2u2aw7GUOR9mFA7YU5YN8rstlHEdIrZvQ8SFqWir-DIKAuk2-8K2RaQlBA+QxWEUx+otXV1zyg1AbG27C3cjDSsWnzKq6fB2CLalfW06KvvEGYVANDE0TSfZ+sOc7gVRQHHSUKFobcubukexH-CYR4WdwVTyCVTASCeVT1LCj6dJiqwrVmW0eoyI5VC3dKxURWMqjG-LHVy1NJpxGaYfpiSEeWyr5NI8jv0wPQXpwKAhgijGWsXZZ+S7TQu3SI0aBWM0kpDO1QPysQVgmpoXRQu6xPhp7fI-ALjdN79sAtwWsbo8wcitQGkrMEUzQO7YTTGzFqiqWpaY+WGHrK57YFwEgmH8dhUEa+PWs4qRlnERYFDG6ozH6vcQzkeZ5n1RxwyLi97tDpmK6rmu642q2-yFr60QsIVOJjHHjBhdKupmI0h8cpc5hyEeS-HvX-DAABHJVlORqBUYb23G3KbfAayPIJG5Iol3RJETFg-VjqmGPtrRmZ8mA82NtQOe1FG75AsOGFY+xGwU03l-RsMwTCcUOLaTIag1YB3TLDE+80y6yQeGAB8qB3z+HIKQu4sAH7bWsMGQ41gDRWlRCacQZolBlEsMCfcOROQHWAWPAkAAlMAggwB8BCEqe4jDhZ1CkGiVQ+kdQGlhCCGyjk14misGZbIoiQ5yivtgauAAZPAYBp6oB-NAzGrV246JKKCW0CZ1CwiUNnTIe05CJghMY-s4lrhmOrqFGAdxsDKR5uQaeijEj7WDK3A0WD-rKObJYSQYFVDpMdKCY+VUADuMkCJEUNkpIKfNQqUH8HgAAZqgAgEBuBgHaLgV8qBXiSBgOwQQBtFIUUwIIBpqAEmIGMoifJGQxpmCVtw9KqJzBdlDFg2wENCklI4HJcpgzlLVI0nU3AjSCCPAeERSQTBubsEaQ8B8PS-D9N2UbYZozxlWX1NWLqqI0TGitGNfqrd+TZFdrBOorFDCbNKW+COgVgr81qaM5prT2mdO6b0wQflPzfhGccsZDiba6SJvyNEih7BQkMIKfqVopCOHfnYUo+UoXbKxZHKpIVDlIrORcq5JAblEXuRi1lgVcWNPecSyQix5D7zEJoalRlqzHVSK3ECShmUBFWo1I5JyWm4DaXgNFbSMUkAAEawEEHwUV+L0bzwTlZSE2SDqwXDLsCyRR4QHWrBkVEoJZAHULuraaQcYbFOhZqhq2qmncoeJc65tzBWPNNeay1byCVbRikTGYQj1B1FMPqLQ-UzI2TqNaCQG9oRAMDYHYSIatkarqv4NakbkV6tRV0o1iazWCD0Fa8VWDpAaMcvCHGxpTquyzYrC0XD83quqg2ptXKHjnJjby-ldyHl9KTd23tabPoTNXvAle+V5jpEhKdSEiJXbaJdaSmQs7XpdGbbq-VHT20bsEA+qRqabUwIDJmlIEJwWOTtIYYGBoBTogtOaE0UJhD3vwI+xdy7Y18vje+z9O6f2OL-Zxc62Q2pWDUGZT+EyRSAjRPIOC+oASztvqjJ9KKDVvoxXR94mGSzYaJZxbYyczAoP3PkEm7VXaMpUEuKwtHnx33eM26NKG10Jr6ax9jc5OMZv7cYEw+i1i5TSu60E-J5jMIRGIGMBpZ0sweGzDmXMebwpqTYpFz623oseZZ6zAVbMPEEAcsKKnIrpsSXbAUXDrAHTinIfq1RtjrzmMKdxch-ZTWrUQ0N2z3PPhs9zXmHKwqyaXTyuNAr30ZfZp57LPncuUH89bQLEzgtexROFtYkX0p5WkLGOLwH35JY1sGtyVi9W2MwOOPoU44jiv3EnQjRxMhHDyAs91Y1qxewxMwxid6q2ENQoNmxtc7GEjCTXV6+FolBVifE3dC8JkxkRPsMwsF1CKxlkUJWSIcaOCyCCY+u3huSGHLgDgBBJsqElSk2bCI5BA0smsMoaR24PZxoDNIP3rF-YB0DykWHCUZvyPyC0VoL2EeBNGQGswVgSHWcCWC+DkvbZEr9-bmBJC4AFd+Ubk4QgTau3a6wmVZDqClsyA0L3ECghmMuVWcsoKQq272BnaOmeSAAHJ1wAAqoDwOwWABAqoQAgIECiXpmaa7uOK+YiI80GkckZRYoJYTzCTmYV+mQc0KFR0NpXqv-Aa61zr0gYV7HY7q3pTQu5ZBAoxFoE0brEC8TFtg0oVoVjHwx+wYHPPWr8NmF2LIKhqNR7NGkKQcwhTLFBcsJ0cvNZuTT8DqkHGccVmYpKvNea5iyFgl1IvZlslOzmECbqkpq-9eLgAFRO1EmJDw4m1w5-0bnwe91wgOuYYESg7TolsPCIvqJJUXv3JYY03YR81rchP-Ap3p+z6aaEpU5j-CM9QHY83jpf6KEchaE6lkEQ2RjOGOoLIV2UvY+JUdmOudSL0c8AAeVwBbRfUNUkCqm8DH0EDAJaUEEgPYBgMtiX2uz0hKGDDyFqCckdDqDUFhCTAFGwTUFylbhKDg1P1hn8FZ38HqRIEoB6HGxUjAHYK5gCF5T1XeSZC4iOARCRy0ChBsFEAd2WH3zSByEhiFBsBHhYLrnYM4N8AnAX0GHeSFC+R0wkDmWyEOFhAdAgxKDsCUAGhXhcjIGwAfD5VaGnmZm5j1XgJczaXsMcPuEEFrkEA0PCjwLtQqGAldxBSWEyFkOSSsBKFSHbmhHUDsMBx8OcNrlcLIG6C0LGy510Mzz-Xyi+TkCh0YmhEoK0GrFWDsFghWBqGSIcKcPwBcJkRCmcNJELA8KY26W8L5SkX8PaP7HeSzngX9SqH2EUL00QFwwYihFbhBCe1yndxOFZwwHgCiD6ygEbxD0EEWDKFyEUHz3UC0EWyEEWElT3ByE03UCyCQiYPxDAC2OX0EFBCIKhFmU0BNFyi8TFHKE5Hu1iMsErQIXl0LEePwPmJz0o3zzyELxhwYjtDAnZBXm+yYOKjEQeNUybyMAyGbjGlylsA+MBljz0kpTu1RGMj9jz16yDTP2LjSzKQUheXs05TxTBLtSlmDEdAxEUFjBqFSEBWSGdhgkd3yFl2BJrzpLrRhX8jhV80RVZMxJD3MhJVUEsGUVX1A3SjkBZFSFRDlnmFCNnXDUjTZKcSfg5AMRUGqIxELU4h2GGjUVyWOFRJEnpLnXWhNMVOXzEPxw4Vwz3DHWFF7jsjECqHzngzekcwVIC29Pbm2BWFgiESqDyEbGBjUElXhwqFUDLzqEkxRhk1GVNMXHbhZBxOtzFEbH7hJlB0BkpUcDeONBTxdNrWhVKyyzszlKjMaSLO2i7D-xBAtHAhsD9n6jJWyTrP3h1BRPFNHwHCfzsR7IzXsGXkIKNDDLWEoNUBDFREPnX0dGnLpxBIG0V2f2ZzT0XMSX2D4XGi7BsHUA3Jh2SEhBWHyQezmCBMPIlLnJPLsRZzZ0wAvImRsCzTXj1EWFkEmIQHj2HMpST2tFkA9z21PJV3V1N210ArhGNGbl4TBBMDSDdksjLxDHbDRCOGULFFT0B3YAwuMEcgYmNCMhxI3270skrFbHMkA07BPxnNpIHAv0iU4Gv2ngwuPHlmPStNz1tG3DyBDDWAWEow3Fpw2KIXQIgKN2wPQlgJoptF+N3lSVC2hyKCNBsgH1sGP2GnM2bP7DULYI4IxJjPwKZBKAYhNAEyMgM35MsjoJDHYRBBqBWEbHqNSKaPSMEPstq29JWAFCFHbwROJ1kJshMGuOFGOLUEYJ4skB6M4BCrrhaKkjaPQhouW19mFCyAjE4lsCixZAHhLOZGtCtBcBcCAA */
id: 'Modeling', id: 'Modeling',
tsTypes: {} as import('./modelingMachine.typegen').Typegen0, tsTypes: {} as import('./modelingMachine.typegen').Typegen0,
@ -135,11 +146,14 @@ export const modelingMachine = createMachine(
otherSelections: [], otherSelections: [],
codeBasedSelections: [], codeBasedSelections: [],
} as Selections, } as Selections,
selectionRangeTypeMap: {} as SelectionRangeTypeMap, sketchDetails: {
sketchPathToNode: null as PathToNode | null, // maybe too specific, and we should have a generic pathToNode, but being specific seems less risky when I'm not sure sketchPathToNode: [],
sketchEnginePathId: '' as string, zAxis: [0, 0, 1],
yAxis: [0, 1, 0],
origin: [0, 0, 0],
} as null | SketchDetails,
sketchPlaneId: '' as string, sketchPlaneId: '' as string,
sketchNormalBackUp: null as null | [number, number, number], sketchEnginePathId: '' as string,
moveDescs: [] as MoveDesc[], moveDescs: [] as MoveDesc[],
}, },
@ -160,7 +174,6 @@ export const modelingMachine = createMachine(
{ {
target: 'animating to existing sketch', target: 'animating to existing sketch',
cond: 'Selection is on face', cond: 'Selection is on face',
actions: ['set sketch metadata'],
}, },
'Sketch no face', 'Sketch no face',
], ],
@ -504,6 +517,11 @@ export const modelingMachine = createMachine(
target: 'animating to plane', target: 'animating to plane',
actions: ['reset sketch metadata'], actions: ['reset sketch metadata'],
}, },
'Set selection': {
target: 'Sketch no face',
internal: true,
},
}, },
}, },
@ -532,15 +550,15 @@ export const modelingMachine = createMachine(
{ {
src: 'animate-to-sketch', src: 'animate-to-sketch',
id: 'animate-to-sketch', id: 'animate-to-sketch',
onDone: 'Sketch', onDone: {
target: 'Sketch',
actions: 'set new sketch metadata',
},
}, },
], ],
entry: 'clientToEngine cam sync direction', entry: 'clientToEngine cam sync direction',
}, },
'animating to plane (copy)': {},
'animating to plane (copy) (copy)': {},
}, },
initial: 'idle', initial: 'idle',
@ -562,13 +580,13 @@ export const modelingMachine = createMachine(
}, },
{ {
guards: { guards: {
'is editing existing sketch': ({ sketchPathToNode }) => { 'is editing existing sketch': ({ sketchDetails }) => {
// should check that the variable declaration is a pipeExpression // should check that the variable declaration is a pipeExpression
// and that the pipeExpression contains a "startProfileAt" callExpression // and that the pipeExpression contains a "startProfileAt" callExpression
if (!sketchPathToNode) return false if (!sketchDetails?.sketchPathToNode) return false
const variableDeclaration = getNodeFromPath<VariableDeclarator>( const variableDeclaration = getNodeFromPath<VariableDeclarator>(
kclManager.ast, kclManager.ast,
sketchPathToNode, sketchDetails.sketchPathToNode,
'VariableDeclarator' 'VariableDeclarator'
).node ).node
if (variableDeclaration.type !== 'VariableDeclarator') return false if (variableDeclaration.type !== 'VariableDeclarator') return false
@ -621,128 +639,154 @@ export const modelingMachine = createMachine(
}, },
// end guards // end guards
actions: { actions: {
'set sketchMetadata from pathToNode': assign(({ sketchPathToNode }) => { 'set sketchMetadata from pathToNode': assign(({ sketchDetails }) => {
if (!sketchPathToNode) return {} if (!sketchDetails?.sketchPathToNode || !sketchDetails) return {}
return getSketchMetadataFromPathToNode(sketchPathToNode) return {
sketchDetails: {
...sketchDetails,
sketchPathToNode: sketchDetails.sketchPathToNode,
},
}
}), }),
'hide default planes': () => { 'hide default planes': () => {
sceneInfra.removeDefaultPlanes() sceneInfra.removeDefaultPlanes()
kclManager.hidePlanes() kclManager.hidePlanes()
}, },
'reset sketch metadata': assign({ 'reset sketch metadata': assign({
sketchPathToNode: null, sketchDetails: null,
sketchEnginePathId: '', sketchEnginePathId: '',
sketchPlaneId: '', sketchPlaneId: '',
}), }),
'set sketch metadata': assign(({ selectionRanges }) => { 'set new sketch metadata': assign((_, { data }) => ({
const sourceRange = selectionRanges.codeBasedSelections[0].range sketchDetails: data,
const sketchPathToNode = getNodePathFromSourceRange( })),
kclManager.ast,
sourceRange
)
return getSketchMetadataFromPathToNode(
sketchPathToNode,
selectionRanges
)
}),
'set new sketch metadata': assign((_, { data }) => data),
// TODO implement source ranges for all of these constraints // TODO implement source ranges for all of these constraints
// need to make the async like the modal constraints // need to make the async like the modal constraints
'Make selection horizontal': ({ selectionRanges, sketchPathToNode }) => { 'Make selection horizontal': ({ selectionRanges, sketchDetails }) => {
const { modifiedAst } = applyConstraintHorzVert( const { modifiedAst } = applyConstraintHorzVert(
selectionRanges, selectionRanges,
'horizontal', 'horizontal',
kclManager.ast, kclManager.ast,
kclManager.programMemory kclManager.programMemory
) )
if (!sketchDetails) return
sceneEntitiesManager.updateAstAndRejigSketch( sceneEntitiesManager.updateAstAndRejigSketch(
sketchPathToNode || [], sketchDetails.sketchPathToNode,
modifiedAst modifiedAst,
sketchDetails.zAxis,
sketchDetails.yAxis,
sketchDetails.origin
) )
}, },
'Make selection vertical': ({ selectionRanges, sketchPathToNode }) => { 'Make selection vertical': ({ selectionRanges, sketchDetails }) => {
const { modifiedAst } = applyConstraintHorzVert( const { modifiedAst } = applyConstraintHorzVert(
selectionRanges, selectionRanges,
'vertical', 'vertical',
kclManager.ast, kclManager.ast,
kclManager.programMemory kclManager.programMemory
) )
if (!sketchDetails) return
sceneEntitiesManager.updateAstAndRejigSketch( sceneEntitiesManager.updateAstAndRejigSketch(
sketchPathToNode || [], sketchDetails.sketchPathToNode || [],
modifiedAst modifiedAst,
sketchDetails.zAxis,
sketchDetails.yAxis,
sketchDetails.origin
) )
}, },
'Constrain horizontally align': ({ 'Constrain horizontally align': ({ selectionRanges, sketchDetails }) => {
selectionRanges,
sketchPathToNode,
}) => {
const { modifiedAst } = applyConstraintHorzVertAlign({ const { modifiedAst } = applyConstraintHorzVertAlign({
selectionRanges, selectionRanges,
constraint: 'setVertDistance', constraint: 'setVertDistance',
}) })
if (!sketchDetails) return
sceneEntitiesManager.updateAstAndRejigSketch( sceneEntitiesManager.updateAstAndRejigSketch(
sketchPathToNode || [], sketchDetails?.sketchPathToNode || [],
modifiedAst modifiedAst,
sketchDetails.zAxis,
sketchDetails.yAxis,
sketchDetails.origin
) )
}, },
'Constrain vertically align': ({ selectionRanges, sketchPathToNode }) => { 'Constrain vertically align': ({ selectionRanges, sketchDetails }) => {
const { modifiedAst } = applyConstraintHorzVertAlign({ const { modifiedAst } = applyConstraintHorzVertAlign({
selectionRanges, selectionRanges,
constraint: 'setHorzDistance', constraint: 'setHorzDistance',
}) })
if (!sketchDetails) return
sceneEntitiesManager.updateAstAndRejigSketch( sceneEntitiesManager.updateAstAndRejigSketch(
sketchPathToNode || [], sketchDetails?.sketchPathToNode || [],
modifiedAst modifiedAst,
sketchDetails.zAxis,
sketchDetails.yAxis,
sketchDetails.origin
) )
}, },
'Constrain snap to X': ({ selectionRanges, sketchPathToNode }) => { 'Constrain snap to X': ({ selectionRanges, sketchDetails }) => {
const { modifiedAst } = applyConstraintAxisAlign({ const { modifiedAst } = applyConstraintAxisAlign({
selectionRanges, selectionRanges,
constraint: 'snapToXAxis', constraint: 'snapToXAxis',
}) })
if (!sketchDetails) return
sceneEntitiesManager.updateAstAndRejigSketch( sceneEntitiesManager.updateAstAndRejigSketch(
sketchPathToNode || [], sketchDetails?.sketchPathToNode || [],
modifiedAst modifiedAst,
sketchDetails.zAxis,
sketchDetails.yAxis,
sketchDetails.origin
) )
}, },
'Constrain snap to Y': ({ selectionRanges, sketchPathToNode }) => { 'Constrain snap to Y': ({ selectionRanges, sketchDetails }) => {
const { modifiedAst } = applyConstraintAxisAlign({ const { modifiedAst } = applyConstraintAxisAlign({
selectionRanges, selectionRanges,
constraint: 'snapToYAxis', constraint: 'snapToYAxis',
}) })
if (!sketchDetails) return
sceneEntitiesManager.updateAstAndRejigSketch( sceneEntitiesManager.updateAstAndRejigSketch(
sketchPathToNode || [], sketchDetails?.sketchPathToNode || [],
modifiedAst modifiedAst,
sketchDetails.zAxis,
sketchDetails.yAxis,
sketchDetails.origin
) )
}, },
'Constrain equal length': ({ selectionRanges, sketchPathToNode }) => { 'Constrain equal length': ({ selectionRanges, sketchDetails }) => {
const { modifiedAst } = applyConstraintEqualLength({ const { modifiedAst } = applyConstraintEqualLength({
selectionRanges, selectionRanges,
}) })
if (!sketchDetails) return
sceneEntitiesManager.updateAstAndRejigSketch( sceneEntitiesManager.updateAstAndRejigSketch(
sketchPathToNode || [], sketchDetails?.sketchPathToNode || [],
modifiedAst modifiedAst,
sketchDetails.zAxis,
sketchDetails.yAxis,
sketchDetails.origin
) )
}, },
'Constrain parallel': ({ selectionRanges, sketchPathToNode }) => { 'Constrain parallel': ({ selectionRanges, sketchDetails }) => {
const { modifiedAst } = applyConstraintEqualAngle({ const { modifiedAst } = applyConstraintEqualAngle({
selectionRanges, selectionRanges,
}) })
if (!sketchDetails) return
sceneEntitiesManager.updateAstAndRejigSketch( sceneEntitiesManager.updateAstAndRejigSketch(
sketchPathToNode || [], sketchDetails?.sketchPathToNode || [],
modifiedAst modifiedAst,
sketchDetails.zAxis,
sketchDetails.yAxis,
sketchDetails.origin
) )
}, },
'Constrain remove constraints': ({ 'Constrain remove constraints': ({ selectionRanges, sketchDetails }) => {
selectionRanges,
sketchPathToNode,
}) => {
const { modifiedAst } = applyRemoveConstrainingValues({ const { modifiedAst } = applyRemoveConstrainingValues({
selectionRanges, selectionRanges,
}) })
if (!sketchDetails) return
sceneEntitiesManager.updateAstAndRejigSketch( sceneEntitiesManager.updateAstAndRejigSketch(
sketchPathToNode || [], sketchDetails?.sketchPathToNode || [],
modifiedAst modifiedAst,
sketchDetails.zAxis,
sketchDetails.yAxis,
sketchDetails.origin
) )
}, },
'AST extrude': (_, event) => { 'AST extrude': (_, event) => {
@ -754,7 +798,6 @@ export const modelingMachine = createMachine(
distance.variableName && distance.variableName &&
distance.insertIndex !== undefined distance.insertIndex !== undefined
) { ) {
console.log('adding variable!', distance)
const newBody = [...ast.body] const newBody = [...ast.body]
newBody.splice( newBody.splice(
distance.insertIndex, distance.insertIndex,
@ -785,18 +828,25 @@ export const modelingMachine = createMachine(
sceneInfra.modelingSend('Equip Line tool') sceneInfra.modelingSend('Equip Line tool')
} }
}, },
'setup client side sketch segments': ({ sketchPathToNode }, { type }) => { 'setup client side sketch segments': ({ sketchDetails }) => {
if (!sketchDetails) return
if (Object.keys(sceneEntitiesManager.activeSegments).length > 0) { if (Object.keys(sceneEntitiesManager.activeSegments).length > 0) {
sceneEntitiesManager sceneEntitiesManager
.tearDownSketch({ removeAxis: false }) .tearDownSketch({ removeAxis: false })
.then(() => { .then(() => {
sceneEntitiesManager.setupSketch({ sceneEntitiesManager.setupSketch({
sketchPathToNode: sketchPathToNode || [], sketchPathToNode: sketchDetails?.sketchPathToNode || [],
forward: sketchDetails.zAxis,
up: sketchDetails.yAxis,
position: sketchDetails.origin,
}) })
}) })
} else { } else {
sceneEntitiesManager.setupSketch({ sceneEntitiesManager.setupSketch({
sketchPathToNode: sketchPathToNode || [], sketchPathToNode: sketchDetails?.sketchPathToNode || [],
forward: sketchDetails.zAxis,
up: sketchDetails.yAxis,
position: sketchDetails.origin,
}) })
} }
}, },
@ -809,43 +859,60 @@ export const modelingMachine = createMachine(
} }
}, },
'remove sketch grid': () => sceneEntitiesManager.removeSketchGrid(), 'remove sketch grid': () => sceneEntitiesManager.removeSketchGrid(),
'set up draft line': ({ sketchPathToNode }) => { 'set up draft line': ({ sketchDetails }) => {
sceneEntitiesManager.setUpDraftLine(sketchPathToNode || []) if (!sketchDetails) return
sceneEntitiesManager.setUpDraftLine(
sketchDetails.sketchPathToNode,
sketchDetails.zAxis,
sketchDetails.yAxis
)
}, },
'set up draft arc': ({ sketchPathToNode }) => { 'set up draft arc': ({ sketchDetails }) => {
sceneEntitiesManager.setUpDraftArc(sketchPathToNode || []) if (!sketchDetails) return
sceneEntitiesManager.setUpDraftArc(
sketchDetails.sketchPathToNode,
sketchDetails.zAxis,
sketchDetails.yAxis
)
}, },
'set up draft line without teardown': ({ sketchPathToNode }) => 'set up draft line without teardown': ({ sketchDetails }) =>
sceneEntitiesManager.setupSketch({ sceneEntitiesManager.setupSketch({
sketchPathToNode: sketchPathToNode || [], sketchPathToNode: sketchDetails?.sketchPathToNode || [],
draftSegment: 'line', draftSegment: 'line',
forward: sketchDetails?.zAxis || [0, 1, 0],
up: sketchDetails?.yAxis || [0, 0, 1],
position: sketchDetails?.origin,
}), }),
'show default planes': () => { 'show default planes': () => {
sceneInfra.showDefaultPlanes() sceneInfra.showDefaultPlanes()
sceneEntitiesManager.setupDefaultPlaneHover() sceneEntitiesManager.setupDefaultPlaneHover()
kclManager.showPlanes() kclManager.showPlanes()
}, },
'setup noPoints onClick listener': ({ sketchPathToNode }) => { 'setup noPoints onClick listener': ({ sketchDetails }) => {
if (!sketchDetails) return
sceneEntitiesManager.createIntersectionPlane() sceneEntitiesManager.createIntersectionPlane()
const sketchGroup = sketchGroupFromPathToNode({ const quaternion = quaternionFromUpNForward(
pathToNode: sketchPathToNode || [], new Vector3(...sketchDetails.yAxis),
ast: kclManager.ast, new Vector3(...sketchDetails.zAxis)
programMemory: kclManager.programMemory, )
})
const quaternion = quaternionFromSketchGroup(sketchGroup)
sceneEntitiesManager.intersectionPlane && sceneEntitiesManager.intersectionPlane &&
sceneEntitiesManager.intersectionPlane.setRotationFromQuaternion( sceneEntitiesManager.intersectionPlane.setRotationFromQuaternion(
quaternion quaternion
) )
sceneEntitiesManager.intersectionPlane &&
sceneEntitiesManager.intersectionPlane.position.copy(
new Vector3(...(sketchDetails?.origin || [0, 0, 0]))
)
sceneInfra.setCallbacks({ sceneInfra.setCallbacks({
onClick: async (args) => { onClick: async (args) => {
if (!args) return if (!args) return
if (args.mouseEvent.which !== 1) return if (args.mouseEvent.which !== 1) return
const { intersectionPoint } = args const { intersectionPoint } = args
if (!intersectionPoint?.twoD || !sketchPathToNode) return if (!intersectionPoint?.twoD || !sketchDetails?.sketchPathToNode)
return
const { modifiedAst } = addStartProfileAt( const { modifiedAst } = addStartProfileAt(
kclManager.ast, kclManager.ast,
sketchPathToNode, sketchDetails.sketchPathToNode,
[intersectionPoint.twoD.x, intersectionPoint.twoD.y] [intersectionPoint.twoD.x, intersectionPoint.twoD.y]
) )
await kclManager.updateAst(modifiedAst, false) await kclManager.updateAst(modifiedAst, false)
@ -854,8 +921,15 @@ export const modelingMachine = createMachine(
}, },
}) })
}, },
'add axis n grid': ({ sketchPathToNode }) => 'add axis n grid': ({ sketchDetails }) => {
sceneEntitiesManager.createSketchAxis(sketchPathToNode || []), if (!sketchDetails) return
sceneEntitiesManager.createSketchAxis(
sketchDetails.sketchPathToNode || [],
sketchDetails.zAxis,
sketchDetails.yAxis,
sketchDetails.origin
)
},
'reset client scene mouse handlers': () => { 'reset client scene mouse handlers': () => {
// when not in sketch mode we don't need any mouse listeners // when not in sketch mode we don't need any mouse listeners
// (note the orbit controls are always active though) // (note the orbit controls are always active though)
@ -871,44 +945,3 @@ export const modelingMachine = createMachine(
// end actions // end actions
} }
) )
function getSketchMetadataFromPathToNode(
pathToNode: PathToNode,
selectionRanges?: Selections
) {
const pipeExpression = getNodeFromPath<PipeExpression>(
kclManager.ast,
pathToNode,
'PipeExpression'
).node
if (pipeExpression.type !== 'PipeExpression') return {}
const sketchCallExpression = pipeExpression.body.find(
(e) => e.type === 'CallExpression' && e.callee.name === 'startSketchOn'
) as CallExpression
if (!sketchCallExpression) return {}
let sketchEnginePathId: string
if (selectionRanges) {
sketchEnginePathId =
isCursorInSketchCommandRange(
engineCommandManager.artifactMap,
selectionRanges
) || ''
} else {
const _selectionRanges: Selections = {
otherSelections: [],
codeBasedSelections: [
{ range: [pipeExpression.start, pipeExpression.end], type: 'default' },
],
}
sketchEnginePathId =
isCursorInSketchCommandRange(
engineCommandManager.artifactMap,
_selectionRanges
) || ''
}
return {
sketchPathToNode: pathToNode,
sketchEnginePathId,
}
}

View File

@ -93,6 +93,8 @@ export interface StoreState {
path: string path: string
}[] }[]
setHomeMenuItems: (items: { name: string; path: string }[]) => void setHomeMenuItems: (items: { name: string; path: string }[]) => void
lastCodeMirrorSelectionUpdatedFromScene: number
setLastCodeMirrorSelectionUpdatedFromScene: (time: number) => void
} }
export const useStore = create<StoreState>()( export const useStore = create<StoreState>()(
@ -156,6 +158,9 @@ export const useStore = create<StoreState>()(
setHomeShowMenu: (showHomeMenu) => set({ showHomeMenu }), setHomeShowMenu: (showHomeMenu) => set({ showHomeMenu }),
homeMenuItems: [], homeMenuItems: [],
setHomeMenuItems: (homeMenuItems) => set({ homeMenuItems }), setHomeMenuItems: (homeMenuItems) => set({ homeMenuItems }),
lastCodeMirrorSelectionUpdatedFromScene: Date.now(),
setLastCodeMirrorSelectionUpdatedFromScene: (time) =>
set({ lastCodeMirrorSelectionUpdatedFromScene: time }),
} }
}, },
{ {

View File

@ -242,6 +242,8 @@ pub struct Face {
pub y_axis: Point3d, pub y_axis: Point3d,
/// The z-axis (normal). /// The z-axis (normal).
pub z_axis: Point3d, pub z_axis: Point3d,
/// the face id the sketch is on
pub face_id: uuid::Uuid,
#[serde(rename = "__meta")] #[serde(rename = "__meta")]
pub meta: Vec<Metadata>, pub meta: Vec<Metadata>,
} }

View File

@ -3,6 +3,7 @@
use anyhow::Result; use anyhow::Result;
use derive_docs::stdlib; use derive_docs::stdlib;
use schemars::JsonSchema; use schemars::JsonSchema;
use uuid::Uuid;
use crate::{ use crate::{
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
@ -113,8 +114,9 @@ async fn inner_extrude(length: f64, sketch_group: Box<SketchGroup>, args: Args)
// Create a hashmap for quick id lookup // Create a hashmap for quick id lookup
let mut face_id_map = std::collections::HashMap::new(); let mut face_id_map = std::collections::HashMap::new();
let mut start_cap_id = None; // creating fake ids for start and end caps is to make extrudes mock-execute safe
let mut end_cap_id = None; let mut start_cap_id = Some(Uuid::new_v4());
let mut end_cap_id = Some(Uuid::new_v4());
for face_info in face_infos { for face_info in face_infos {
match face_info.cap { match face_info.cap {
@ -160,6 +162,18 @@ async fn inner_extrude(length: f64, sketch_group: Box<SketchGroup>, args: Args)
new_value.push(extrude_surface); new_value.push(extrude_surface);
} }
} }
} else {
new_value.push(ExtrudeSurface::ExtrudePlane(crate::executor::ExtrudePlane {
position: sketch_group.position, // TODO should be for the extrude surface
rotation: sketch_group.rotation, // TODO should be for the extrude surface
// pushing this values with a fake face_id to make extrudes mock-execute safe
face_id: Uuid::new_v4(),
name: path.get_base().name.clone(),
geo_meta: GeoMeta {
id: path.get_base().geo_meta.id,
metadata: path.get_base().geo_meta.metadata.clone(),
},
}));
} }
} }

View File

@ -955,6 +955,7 @@ async fn start_sketch_on_face(
y_axis: extrude_group.y_axis, y_axis: extrude_group.y_axis,
z_axis: extrude_group.z_axis, z_axis: extrude_group.z_axis,
meta: vec![args.source_range.into()], meta: vec![args.source_range.into()],
face_id: extrude_plane_id,
})) }))
} }