Compare commits

..

17 Commits

Author SHA1 Message Date
29070a9b04 Hook up circle tool to client side scene, still missing displaying circle in sketch mode 2024-04-23 14:54:56 -04:00
e3861f9380 Add Circle tool to state machine 2024-04-23 13:40:52 -04:00
2d8d29b345 Bump tauri-plugin-fs from 2.0.0-beta.5 to 2.0.0-beta.6 in /src-tauri (#2205)
Bumps [tauri-plugin-fs](https://github.com/tauri-apps/plugins-workspace) from 2.0.0-beta.5 to 2.0.0-beta.6.
- [Release notes](https://github.com/tauri-apps/plugins-workspace/releases)
- [Commits](https://github.com/tauri-apps/plugins-workspace/compare/fs-v2.0.0-beta.5...fs-v2.0.0-beta.6)

---
updated-dependencies:
- dependency-name: tauri-plugin-fs
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-23 16:56:00 +00:00
00da062586 bump kittycad.rs (#2196)
* update lib

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix tests

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-04-23 03:46:54 +00:00
aafbaf6c50 human speed completions (#2193)
* human speed completions

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* add slowness

Signed-off-by: Jess Frazelle <github@jessfraz.com>

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

* empty

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-04-23 00:21:24 +00:00
2894c84a4e fix recast (#2194)
* fix recast

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fixes

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-04-22 17:14:20 -07:00
c01084feb0 Zoom to fit rust side (#2195)
* zoom to fit

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* zoom to fit

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* docs

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-04-22 17:14:10 -07:00
c461db5f54 fix const completion (#2192)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-04-22 21:53:49 +00:00
03fcb73aca Bump kittycad-modeling-cmds from 0.2.19 to 0.2.20 in /src/wasm-lib (#2186)
Bumps [kittycad-modeling-cmds](https://github.com/KittyCAD/modeling-api) from 0.2.19 to 0.2.20.
- [Commits](https://github.com/KittyCAD/modeling-api/compare/kittycad-modeling-cmds-0.2.19...kittycad-modeling-cmds-0.2.20)

---
updated-dependencies:
- dependency-name: kittycad-modeling-cmds
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jess Frazelle <jessfraz@users.noreply.github.com>
2024-04-22 20:09:53 +00:00
8065e7e51a Bump thiserror from 1.0.58 to 1.0.59 in /src/wasm-lib (#2187)
Bumps [thiserror](https://github.com/dtolnay/thiserror) from 1.0.58 to 1.0.59.
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/1.0.58...1.0.59)

---
updated-dependencies:
- dependency-name: thiserror
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jess Frazelle <jessfraz@users.noreply.github.com>
2024-04-22 18:46:00 +00:00
2d0ac249df Cut release v0.18.1 (#2189)
* Cut release v0.18.1

* Fix release script
2024-04-22 09:47:10 -07:00
3d73b82c23 project global origin for sketches and use engine animations (#2113)
* use engine animations for sketch on face, but not default planes

* massage things

* fix type issue

* small problem in playwright test<

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

* some tests fixes

* more test tweaks

* more test tweaks

* clean up

* more tidy

* tests are a pain

* more test stuff

* test stuff again

* fix micro think axes in sketch mode

* more test shit

* more test shit more

* more test tweaks

* more test tweaks

* more test stuff

* trigger ci

* clean up

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-04-22 20:14:06 +10:00
0b235dc1cd Cut release v0.18.0 (#2177)
* Cut release v0.18.0

* Update src-tauri/tauri.conf.json

* Update src-tauri/tauri.conf.json

* Update src-tauri/tauri.conf.json

* Dumb tauri.conf.json issue

---------

Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com>
2024-04-22 10:12:06 +02:00
0857415793 turn back on test (#2178)
* turn back on test

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* format

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-04-20 01:16:33 +00:00
1da4fd03ef Bump @headlessui/react from 1.7.18 to 1.7.19 (#2172)
Bumps [@headlessui/react](https://github.com/tailwindlabs/headlessui/tree/HEAD/packages/@headlessui-react) from 1.7.18 to 1.7.19.
- [Release notes](https://github.com/tailwindlabs/headlessui/releases)
- [Changelog](https://github.com/tailwindlabs/headlessui/blob/main/packages/@headlessui-react/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/headlessui/commits/@headlessui/react@v1.7.19/packages/@headlessui-react)

---
updated-dependencies:
- dependency-name: "@headlessui/react"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-19 14:54:50 -07:00
39d84c12ab generate new images (#2176)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-04-19 21:27:40 +00:00
537d86c8ff Editor singleton to prevent re-renders (#2163)
* move editor data into a singleton

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* debounce on update

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* make select on extrude work

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* highlight range

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* highlight range

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix errors

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* almost forgot the error pane

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* loint

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* call out to codemirror

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix tauri;

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* more efficient

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* create the modals in the hook

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* Revert "create the modals in the hook"

This reverts commit bbeba85030763cf7235a09fa24247dbf120f2a64.

* change todo

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-04-19 21:24:40 +00:00
287 changed files with 1945 additions and 1389 deletions

2
.nvmrc
View File

@ -1 +1 @@
v20.5.0 v21.7.1

View File

@ -1,3 +1,3 @@
module.exports = { module.exports = {
presets: ["@babel/preset-env"], presets: ['@babel/preset-env'],
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -104,6 +104,7 @@ test('Basic sketch', async ({ page }) => {
|> startProfileAt(${commonPoints.startAt}, %) |> startProfileAt(${commonPoints.startAt}, %)
|> line([${commonPoints.num1}, 0], %) |> line([${commonPoints.num1}, 0], %)
|> line([0, ${commonPoints.num1}], %)`) |> line([0, ${commonPoints.num1}], %)`)
await page.waitForTimeout(100)
await page.mouse.click(startXPx, 500 - PUR * 20) await page.mouse.click(startXPx, 500 - PUR * 20)
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ') .toHaveText(`const part001 = startSketchOn('-XZ')
@ -328,9 +329,7 @@ test('if you write invalid kcl you get inlined errors', async ({ page }) => {
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
}) })
/* Ignore this test for now since its causing engine to crash test('if your kcl gets an error from the engine it is inlined', async ({
*
* test('if your kcl gets an error from the engine it is inlined', async ({
page, page,
}) => { }) => {
const u = getUtils(page) const u = getUtils(page)
@ -349,7 +348,7 @@ const sketch001 = startSketchOn(box, "revolveAxis")
|> startProfileAt([5, 10], %) |> startProfileAt([5, 10], %)
|> line([0, -10], %) |> line([0, -10], %)
|> line([2, 0], %) |> line([2, 0], %)
|> line([0, 10], %) |> line([0, -10], %)
|> close(%) |> close(%)
|> revolve({ |> revolve({
axis: getEdge('revolveAxis', box), axis: getEdge('revolveAxis', box),
@ -364,7 +363,7 @@ angle: 90
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
u.openDebugPanel() await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]') await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel() await u.closeDebugPanel()
@ -378,7 +377,7 @@ angle: 90
'sketch profile must lie entirely on one side of the revolution axis' 'sketch profile must lie entirely on one side of the revolution axis'
) )
).toBeVisible() ).toBeVisible()
})*/ })
test('executes on load', async ({ page }) => { test('executes on load', async ({ page }) => {
const u = getUtils(page) const u = getUtils(page)
@ -566,7 +565,9 @@ test('Auto complete works', async ({ page }) => {
await page.keyboard.press('Tab') await page.keyboard.press('Tab')
await page.keyboard.type('12') await page.keyboard.type('12')
await page.waitForTimeout(100)
await page.keyboard.press('Tab') await page.keyboard.press('Tab')
await page.waitForTimeout(100)
await page.keyboard.press('Tab') await page.keyboard.press('Tab')
await page.keyboard.press('Tab') await page.keyboard.press('Tab')
await page.keyboard.press('Enter') await page.keyboard.press('Enter')
@ -736,7 +737,7 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
await u.openDebugPanel() await u.openDebugPanel()
const xAxisClick = () => const xAxisClick = () =>
page.mouse.click(700, 250).then(() => page.waitForTimeout(100)) page.mouse.click(700, 253).then(() => page.waitForTimeout(100))
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 = () =>
@ -761,6 +762,7 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
.toHaveText(`const part001 = startSketchOn('-XZ') .toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${commonPoints.startAt}, %)`) |> startProfileAt(${commonPoints.startAt}, %)`)
await page.waitForTimeout(100)
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
@ -768,12 +770,14 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|> startProfileAt(${commonPoints.startAt}, %) |> startProfileAt(${commonPoints.startAt}, %)
|> line([${commonPoints.num1}, 0], %)`) |> line([${commonPoints.num1}, 0], %)`)
await page.waitForTimeout(100)
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20) await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ') .toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${commonPoints.startAt}, %) |> startProfileAt(${commonPoints.startAt}, %)
|> line([${commonPoints.num1}, 0], %) |> line([${commonPoints.num1}, 0], %)
|> line([0, ${commonPoints.num1}], %)`) |> line([0, ${commonPoints.num1}], %)`)
await page.waitForTimeout(100)
await page.mouse.click(startXPx, 500 - PUR * 20) await page.mouse.click(startXPx, 500 - PUR * 20)
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ') .toHaveText(`const part001 = startSketchOn('-XZ')
@ -786,10 +790,14 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
await page.getByRole('button', { name: 'Line' }).click() await page.getByRole('button', { name: 'Line' }).click()
await u.closeDebugPanel() await u.closeDebugPanel()
const selectionSequence = async () => { const selectionSequence = async (isSecondTime = false) => {
await expect(page.getByTestId('hover-highlight')).not.toBeVisible() await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
await page.mouse.move(startXPx + PUR * 15, 500 - PUR * 10) await page.waitForTimeout(100)
await page.mouse.move(
startXPx + PUR * 15,
isSecondTime ? 430 : 500 - PUR * 10
)
await expect(page.getByTestId('hover-highlight')).toBeVisible() await expect(page.getByTestId('hover-highlight')).toBeVisible()
// bg-yellow-200 is more brittle than hover-highlight, but is closer to the user experience // bg-yellow-200 is more brittle than hover-highlight, but is closer to the user experience
@ -799,7 +807,10 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
// check mousing off, than mousing onto another line // check mousing off, than mousing onto another line
await page.mouse.move(startXPx + PUR * 10, 500 - PUR * 15) // mouse off await page.mouse.move(startXPx + PUR * 10, 500 - PUR * 15) // mouse off
await expect(page.getByTestId('hover-highlight')).not.toBeVisible() await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
await page.mouse.move(startXPx + PUR * 10, 500 - PUR * 20) // mouse onto another line await page.mouse.move(
startXPx + PUR * 10,
isSecondTime ? 295 : 500 - PUR * 20
) // mouse onto another line
await expect(page.getByTestId('hover-highlight')).toBeVisible() await expect(page.getByTestId('hover-highlight')).toBeVisible()
// now check clicking works including axis // now check clicking works including axis
@ -809,6 +820,7 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
await page.keyboard.down('Shift') await page.keyboard.down('Shift')
const absYButton = page.getByRole('button', { name: 'ABS Y' }) const absYButton = page.getByRole('button', { name: 'ABS Y' })
await expect(absYButton).toBeDisabled() await expect(absYButton).toBeDisabled()
await page.waitForTimeout(100)
await xAxisClick() await xAxisClick()
await page.keyboard.up('Shift') await page.keyboard.up('Shift')
await absYButton.and(page.locator(':not([disabled])')).waitFor() await absYButton.and(page.locator(':not([disabled])')).waitFor()
@ -817,10 +829,12 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
// clear selection by clicking on nothing // clear selection by clicking on nothing
await emptySpaceClick() await emptySpaceClick()
await page.waitForTimeout(100)
// same selection but click the axis first // same selection but click the axis first
await xAxisClick() await xAxisClick()
await expect(absYButton).toBeDisabled() await expect(absYButton).toBeDisabled()
await page.keyboard.down('Shift') await page.keyboard.down('Shift')
await page.waitForTimeout(100)
await topHorzSegmentClick() await topHorzSegmentClick()
await page.keyboard.up('Shift') await page.keyboard.up('Shift')
@ -833,6 +847,7 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
await page.getByText(` |> line([-${commonPoints.num2}, 0], %)`).click() await page.getByText(` |> line([-${commonPoints.num2}, 0], %)`).click()
await page.keyboard.down('Shift') await page.keyboard.down('Shift')
await expect(absYButton).toBeDisabled() await expect(absYButton).toBeDisabled()
await page.waitForTimeout(100)
await xAxisClick() await xAxisClick()
await page.keyboard.up('Shift') await page.keyboard.up('Shift')
await expect(absYButton).not.toBeDisabled() await expect(absYButton).not.toBeDisabled()
@ -875,11 +890,16 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
await page.waitForTimeout(100) await page.waitForTimeout(100)
// enter sketch again // enter sketch again
await page.getByRole('button', { name: 'Edit Sketch' }).click() await u.doAndWaitForCmd(
() => page.getByRole('button', { name: 'Edit Sketch' }).click(),
'default_camera_get_settings'
)
await page.waitForTimeout(150)
await page.waitForTimeout(300) // wait for animation await page.waitForTimeout(300) // wait for animation
// hover again and check it works // hover again and check it works
await selectionSequence() await selectionSequence(true)
}) })
test.describe('Command bar tests', () => { test.describe('Command bar tests', () => {
@ -1065,6 +1085,7 @@ test('Can add multiple sketches', async ({ page }) => {
|> startProfileAt(${commonPoints.startAt}, %) |> startProfileAt(${commonPoints.startAt}, %)
|> line([${commonPoints.num1}, 0], %) |> line([${commonPoints.num1}, 0], %)
|> line([0, ${commonPoints.num1}], %)`) |> line([0, ${commonPoints.num1}], %)`)
await page.waitForTimeout(100)
await page.mouse.click(startXPx, 500 - PUR * 20) await page.mouse.click(startXPx, 500 - PUR * 20)
const finalCodeFirstSketch = `const part001 = startSketchOn('-XZ') const finalCodeFirstSketch = `const part001 = startSketchOn('-XZ')
|> startProfileAt(${commonPoints.startAt}, %) |> startProfileAt(${commonPoints.startAt}, %)
@ -1080,24 +1101,33 @@ test('Can add multiple sketches', async ({ page }) => {
await u.expectCmdLog('[data-message-type="execution-done"]') await u.expectCmdLog('[data-message-type="execution-done"]')
await u.updateCamPosition([0, 100, 100]) await u.updateCamPosition([100, 100, 100])
await page.waitForTimeout(250)
// start a new sketch // start a new sketch
await u.clearCommandLogs() await u.clearCommandLogs()
await page.getByRole('button', { name: 'Start Sketch' }).click() await page.getByRole('button', { name: 'Start Sketch' }).click()
await page.waitForTimeout(100) await page.waitForTimeout(400)
await page.mouse.click(673, 384) await page.mouse.click(650, 450)
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
await u.clearAndCloseDebugPanel() await u.clearAndCloseDebugPanel()
// on mock os there are issues with getting the camera to update
// it should not be selecting the 'XZ' plane here if the camera updated
// properly, but if we just role with it we can still verify everything
// in the rest of the test
const plane = process.platform === 'darwin' ? 'XZ' : 'XY'
await page.waitForTimeout(100)
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
const startAt2 = '[0.93,-1.25]' const startAt2 =
process.platform === 'darwin' ? '[9.75, -13.16]' : '[0.93, -1.25]'
await expect( await expect(
(await page.locator('.cm-content').innerText()).replace(/\s/g, '') (await page.locator('.cm-content').innerText()).replace(/\s/g, '')
).toBe( ).toBe(
`${finalCodeFirstSketch} `${finalCodeFirstSketch}
const part002 = startSketchOn('XY') const part002 = startSketchOn('${plane}')
|> startProfileAt(${startAt2}, %)`.replace(/\s/g, '') |> startProfileAt(${startAt2}, %)`.replace(/\s/g, '')
) )
await page.waitForTimeout(100) await page.waitForTimeout(100)
@ -1106,12 +1136,12 @@ const part002 = startSketchOn('XY')
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)
const num2 = 0.94 const num2 = process.platform === 'darwin' ? 9.84 : 0.94
await expect( await expect(
(await page.locator('.cm-content').innerText()).replace(/\s/g, '') (await page.locator('.cm-content').innerText()).replace(/\s/g, '')
).toBe( ).toBe(
`${finalCodeFirstSketch} `${finalCodeFirstSketch}
const part002 = startSketchOn('XY') const part002 = startSketchOn('${plane}')
|> startProfileAt(${startAt2}, %) |> startProfileAt(${startAt2}, %)
|> line([${num2}, 0], %)`.replace(/\s/g, '') |> line([${num2}, 0], %)`.replace(/\s/g, '')
) )
@ -1121,21 +1151,29 @@ const part002 = startSketchOn('XY')
(await page.locator('.cm-content').innerText()).replace(/\s/g, '') (await page.locator('.cm-content').innerText()).replace(/\s/g, '')
).toBe( ).toBe(
`${finalCodeFirstSketch} `${finalCodeFirstSketch}
const part002 = startSketchOn('XY') const part002 = startSketchOn('${plane}')
|> startProfileAt(${startAt2}, %) |> startProfileAt(${startAt2}, %)
|> line([${num2}, 0], %) |> line([${num2}, 0], %)
|> line([0, ${roundOff(num2 - 0.01)}], %)`.replace(/\s/g, '') |> line([0, ${roundOff(
num2 + (process.platform === 'darwin' ? 0.01 : -0.01)
)}], %)`.replace(/\s/g, '')
) )
await page.waitForTimeout(100)
await page.mouse.click(startXPx, 500 - PUR * 20) await page.mouse.click(startXPx, 500 - PUR * 20)
await expect( await expect(
(await page.locator('.cm-content').innerText()).replace(/\s/g, '') (await page.locator('.cm-content').innerText()).replace(/\s/g, '')
).toBe( ).toBe(
`${finalCodeFirstSketch} `${finalCodeFirstSketch}
const part002 = startSketchOn('XY') const part002 = startSketchOn('${plane}')
|> startProfileAt(${startAt2}, %) |> startProfileAt(${startAt2}, %)
|> line([${num2}, 0], %) |> line([${num2}, 0], %)
|> line([0, ${roundOff(num2 - 0.01)}], %) |> line([0, ${roundOff(
|> line([-1.87, 0], %)`.replace(/\s/g, '') num2 + (process.platform === 'darwin' ? 0.01 : -0.01)
)}], %)
|> line([-${process.platform === 'darwin' ? 19.59 : 1.87}, 0], %)`.replace(
/\s/g,
''
)
) )
}) })
@ -1339,10 +1377,12 @@ test('Deselecting line tool should mean nothing happens on click', async ({
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent) await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
previousCodeContent = await page.locator('.cm-content').innerText() previousCodeContent = await page.locator('.cm-content').innerText()
await page.waitForTimeout(100)
await page.mouse.click(700, 300) await page.mouse.click(700, 300)
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent) await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
previousCodeContent = await page.locator('.cm-content').innerText() previousCodeContent = await page.locator('.cm-content').innerText()
await page.waitForTimeout(100)
await page.mouse.click(750, 300) await page.mouse.click(750, 300)
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent) await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
previousCodeContent = await page.locator('.cm-content').innerText() previousCodeContent = await page.locator('.cm-content').innerText()
@ -1367,16 +1407,16 @@ test('Can edit segments by dragging their handles', async ({ page }) => {
page.getByRole('button', { name: 'Start Sketch' }) page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled() ).not.toBeDisabled()
const startPX = [652, 418] const startPX = [665, 458]
const lineEndPX = [794, 416] const lineEndPX = [842, 458]
const arcEndPX = [893, 318] const arcEndPX = [971, 342]
const dragPX = 30 const dragPX = 30
await page.getByText('startProfileAt([4.61, -14.01], %)').click() await page.getByText('startProfileAt([4.61, -14.01], %)').click()
await expect(page.getByRole('button', { name: 'Edit Sketch' })).toBeVisible() await expect(page.getByRole('button', { name: 'Edit Sketch' })).toBeVisible()
await page.getByRole('button', { name: 'Edit Sketch' }).click() await page.getByRole('button', { name: 'Edit Sketch' }).click()
await page.waitForTimeout(100) await page.waitForTimeout(400)
let prevContent = await page.locator('.cm-content').innerText() let prevContent = await page.locator('.cm-content').innerText()
const step5 = { steps: 5 } const step5 = { steps: 5 }
@ -1386,7 +1426,7 @@ test('Can edit segments by dragging their handles', async ({ page }) => {
await page.mouse.down() await page.mouse.down()
await page.mouse.move(startPX[0] + dragPX, startPX[1] - dragPX, step5) await page.mouse.move(startPX[0] + dragPX, startPX[1] - dragPX, step5)
await page.mouse.up() await page.mouse.up()
await page.waitForTimeout(100)
await expect(page.locator('.cm-content')).not.toHaveText(prevContent) await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
prevContent = await page.locator('.cm-content').innerText() prevContent = await page.locator('.cm-content').innerText()
@ -1414,9 +1454,9 @@ test('Can edit segments by dragging their handles', async ({ page }) => {
// expect the code to have changed // expect the code to have changed
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ') .toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt([7.01, -11.79], %) |> startProfileAt([6.44, -12.07], %)
|> line([14.69, 2.73], %) |> line([14.04, 2.03], %)
|> tangentialArcTo([27.6, -3.25], %)`) |> tangentialArcTo([27.19, -4.2], %)`)
}) })
const doSnapAtDifferentScales = async ( const doSnapAtDifferentScales = async (
@ -1535,38 +1575,46 @@ test('Sketch on face', async ({ page }) => {
).not.toBeDisabled() ).not.toBeDisabled()
await page.getByRole('button', { name: 'Start Sketch' }).click() await page.getByRole('button', { name: 'Start Sketch' }).click()
await page.waitForTimeout(300)
let previousCodeContent = await page.locator('.cm-content').innerText() let previousCodeContent = await page.locator('.cm-content').innerText()
await page.mouse.click(793, 133) await u.openAndClearDebugPanel()
await u.doAndWaitForCmd(
() => page.mouse.click(793, 133),
'default_camera_get_settings',
true
)
await page.waitForTimeout(150)
const firstClickPosition = [612, 238] const firstClickPosition = [612, 238]
const secondClickPosition = [661, 242] const secondClickPosition = [661, 242]
const thirdClickPosition = [609, 267] const thirdClickPosition = [609, 267]
await page.waitForTimeout(300)
await page.mouse.click(firstClickPosition[0], firstClickPosition[1]) await page.mouse.click(firstClickPosition[0], firstClickPosition[1])
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent) await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
previousCodeContent = await page.locator('.cm-content').innerText() previousCodeContent = await page.locator('.cm-content').innerText()
await page.waitForTimeout(100)
await page.mouse.click(secondClickPosition[0], secondClickPosition[1]) await page.mouse.click(secondClickPosition[0], secondClickPosition[1])
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent) await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
previousCodeContent = await page.locator('.cm-content').innerText() previousCodeContent = await page.locator('.cm-content').innerText()
await page.waitForTimeout(100)
await page.mouse.click(thirdClickPosition[0], thirdClickPosition[1]) await page.mouse.click(thirdClickPosition[0], thirdClickPosition[1])
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent) await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
previousCodeContent = await page.locator('.cm-content').innerText() previousCodeContent = await page.locator('.cm-content').innerText()
await page.waitForTimeout(100)
await page.mouse.click(firstClickPosition[0], firstClickPosition[1]) await page.mouse.click(firstClickPosition[0], firstClickPosition[1])
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent) await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
previousCodeContent = await page.locator('.cm-content').innerText() previousCodeContent = await page.locator('.cm-content').innerText()
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toContainText(`const part002 = startSketchOn(part001, 'seg01') .toContainText(`const part002 = startSketchOn(part001, 'seg01')
|> startProfileAt([1.03, 1.03], %) |> startProfileAt([-12.83, 6.7], %)
|> line([4.18, -0.35], %) |> line([2.87, -0.23], %)
|> line([-4.44, -2.13], %) |> line([-3.05, -1.47], %)
|> close(%)`) |> close(%)`)
await u.openAndClearDebugPanel() await u.openAndClearDebugPanel()
@ -1576,9 +1624,14 @@ test('Sketch on face', async ({ page }) => {
await u.updateCamPosition([1049, 239, 686]) await u.updateCamPosition([1049, 239, 686])
await u.closeDebugPanel() await u.closeDebugPanel()
await page.getByText('startProfileAt([1.03, 1.03], %)').click() await page.getByText('startProfileAt([-12.83, 6.7], %)').click()
await expect(page.getByRole('button', { name: 'Edit Sketch' })).toBeVisible() await expect(page.getByRole('button', { name: 'Edit Sketch' })).toBeVisible()
await page.getByRole('button', { name: 'Edit Sketch' }).click() await u.doAndWaitForCmd(
() => page.getByRole('button', { name: 'Edit Sketch' }).click(),
'default_camera_get_settings',
true
)
await page.waitForTimeout(150)
await page.setViewportSize({ width: 1200, height: 1200 }) await page.setViewportSize({ width: 1200, height: 1200 })
await u.openAndClearDebugPanel() await u.openAndClearDebugPanel()
await u.updateCamPosition([452, -152, 1166]) await u.updateCamPosition([452, -152, 1166])
@ -1598,11 +1651,11 @@ test('Sketch on face', async ({ page }) => {
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toContainText(`const part002 = startSketchOn(part001, 'seg01') .toContainText(`const part002 = startSketchOn(part001, 'seg01')
|> startProfileAt([1.03, 1.03], %) |> startProfileAt([-12.83, 6.7], %)
|> line([${process?.env?.CI ? 2.74 : 2.93}, -${ |> line([${process?.env?.CI ? 2.28 : 2.28}, -${
process?.env?.CI ? 0.24 : 0.2 process?.env?.CI ? 0.07 : 0.07
}], %) }], %)
|> line([-4.44, -2.13], %) |> line([-3.05, -1.47], %)
|> close(%)`) |> close(%)`)
// exit sketch // exit sketch
@ -1610,7 +1663,7 @@ test('Sketch on face', async ({ page }) => {
await page.getByRole('button', { name: 'Exit Sketch' }).click() await page.getByRole('button', { name: 'Exit Sketch' }).click()
await u.expectCmdLog('[data-message-type="execution-done"]') await u.expectCmdLog('[data-message-type="execution-done"]')
await page.getByText('startProfileAt([1.03, 1.03], %)').click() await page.getByText('startProfileAt([-12.83, 6.7], %)').click()
await expect(page.getByRole('button', { name: 'Extrude' })).not.toBeDisabled() await expect(page.getByRole('button', { name: 'Extrude' })).not.toBeDisabled()
await page.getByRole('button', { name: 'Extrude' }).click() await page.getByRole('button', { name: 'Extrude' }).click()
@ -1624,11 +1677,11 @@ test('Sketch on face', async ({ page }) => {
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toContainText(`const part002 = startSketchOn(part001, 'seg01') .toContainText(`const part002 = startSketchOn(part001, 'seg01')
|> startProfileAt([1.03, 1.03], %) |> startProfileAt([-12.83, 6.7], %)
|> line([${process?.env?.CI ? 2.74 : 2.93}, -${ |> line([${process?.env?.CI ? 2.28 : 2.28}, -${
process?.env?.CI ? 0.24 : 0.2 process?.env?.CI ? 0.07 : 0.07
}], %) }], %)
|> line([-4.44, -2.13], %) |> line([-3.05, -1.47], %)
|> close(%) |> close(%)
|> extrude(5 + 7, %)`) |> extrude(5 + 7, %)`)
}) })
@ -1661,11 +1714,11 @@ test('Can code mod a line length', async ({ page }) => {
// enter sketch again // enter sketch again
await page.getByRole('button', { name: 'Edit Sketch' }).click() await page.getByRole('button', { name: 'Edit Sketch' }).click()
await page.waitForTimeout(300) // wait for animation await page.waitForTimeout(350) // wait for animation
const startXPx = 500 const startXPx = 500
await page.mouse.move(startXPx + PUR * 15, 250 - PUR * 10) await page.mouse.move(startXPx + PUR * 15, 250 - PUR * 10)
await page.mouse.click(615, 133) await page.mouse.click(615, 102)
await page.getByRole('button', { name: 'length', exact: true }).click() await page.getByRole('button', { name: 'length', exact: true }).click()
await page.getByText('Add constraining value').click() await page.getByText('Add constraining value').click()
@ -1673,3 +1726,42 @@ test('Can code mod a line length', async ({ page }) => {
`const length001 = 20const part001 = startSketchOn('XY') |> startProfileAt([-10, -10], %) |> line([20, 0], %) |> line([0, 20], %) |> xLine(-length001, %)` `const length001 = 20const part001 = startSketchOn('XY') |> startProfileAt([-10, -10], %) |> line([20, 0], %) |> line([0, 20], %) |> xLine(-length001, %)`
) )
}) })
test('Extrude from command bar selects extrude line after', async ({
page,
}) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`const part001 = startSketchOn('XY')
|> startProfileAt([-10, -10], %)
|> line([20, 0], %)
|> line([0, 20], %)
|> xLine(-20, %)
|> close(%)
`
)
})
const u = getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
// Click the line of code for xLine.
await page.getByText(`close(%)`).click() // TODO remove this and reinstate // await topHorzSegmentClick()
await page.waitForTimeout(100)
await page.getByRole('button', { name: 'Extrude' }).click()
await page.waitForTimeout(100)
await page.keyboard.press('Enter')
await page.waitForTimeout(100)
await page.keyboard.press('Enter')
await page.waitForTimeout(100)
await expect(page.locator('.cm-activeLine')).toHaveText(
` |> extrude(5 + 7, %)`
)
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

View File

@ -6,7 +6,7 @@ import { PNG } from 'pngjs'
async function waitForPageLoad(page: Page) { async function waitForPageLoad(page: Page) {
// wait for 'Loading stream...' spinner // wait for 'Loading stream...' spinner
await page.getByTestId('loading-stream').waitFor() // await page.getByTestId('loading-stream').waitFor()
// wait for all spinners to be gone // wait for all spinners to be gone
await page.getByTestId('loading').waitFor({ state: 'detached' }) await page.getByTestId('loading').waitFor({ state: 'detached' })

View File

@ -57,7 +57,7 @@ echo "New version number without 'v': $new_version_number"
git checkout -b "cut-release-$new_version" git checkout -b "cut-release-$new_version"
echo "$(jq --arg v "$new_version_number" '.version=$v' package.json --indent 2)" > package.json echo "$(jq --arg v "$new_version_number" '.version=$v' package.json --indent 2)" > package.json
echo "$(jq --arg v "$new_version_number" '.package.version=$v' src-tauri/tauri.conf.json --indent 2)" > src-tauri/tauri.conf.json echo "$(jq --arg v "$new_version_number" '.version=$v' src-tauri/tauri.conf.json --indent 2)" > src-tauri/tauri.conf.json
git add package.json src-tauri/tauri.conf.json git add package.json src-tauri/tauri.conf.json
git commit -m "Cut release $new_version" git commit -m "Cut release $new_version"

View File

@ -1,6 +1,6 @@
{ {
"name": "untitled-app", "name": "untitled-app",
"version": "0.17.3", "version": "0.18.1",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@codemirror/autocomplete": "^6.16.0", "@codemirror/autocomplete": "^6.16.0",
@ -8,7 +8,7 @@
"@fortawesome/free-brands-svg-icons": "^6.5.2", "@fortawesome/free-brands-svg-icons": "^6.5.2",
"@fortawesome/free-solid-svg-icons": "^6.4.2", "@fortawesome/free-solid-svg-icons": "^6.4.2",
"@fortawesome/react-fontawesome": "^0.2.0", "@fortawesome/react-fontawesome": "^0.2.0",
"@headlessui/react": "^1.7.18", "@headlessui/react": "^1.7.19",
"@headlessui/tailwindcss": "^0.2.0", "@headlessui/tailwindcss": "^0.2.0",
"@kittycad/lib": "^0.0.58", "@kittycad/lib": "^0.0.58",
"@lezer/javascript": "^1.4.9", "@lezer/javascript": "^1.4.9",
@ -84,8 +84,8 @@
"test:e2e:tauri": "E2E_TAURI_ENABLED=true TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\"}' wdio run wdio.conf.ts", "test:e2e:tauri": "E2E_TAURI_ENABLED=true TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\"}' wdio run wdio.conf.ts",
"simpleserver:ci": "yarn pretest && http-server ./public --cors -p 3000 &", "simpleserver:ci": "yarn pretest && http-server ./public --cors -p 3000 &",
"simpleserver": "yarn pretest && http-server ./public --cors -p 3000", "simpleserver": "yarn pretest && http-server ./public --cors -p 3000",
"fmt": "prettier --write ./src && prettier --write ./e2e", "fmt": "prettier --write ./src *.ts *.json *.js ./e2e",
"fmt-check": "prettier --check ./src && prettier --check ./e2e", "fmt-check": "prettier --check ./src *.ts *.json *.js ./e2e",
"build:wasm-dev": "(cd src/wasm-lib && wasm-pack build --dev --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt", "build:wasm-dev": "(cd src/wasm-lib && wasm-pack build --dev --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt",
"build:wasm": "(cd src/wasm-lib && wasm-pack build --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt", "build:wasm": "(cd src/wasm-lib && wasm-pack build --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt",
"build:wasm-clean": "yarn wasm-prep && yarn build:wasm", "build:wasm-clean": "yarn wasm-prep && yarn build:wasm",
@ -132,6 +132,7 @@
"@types/wicg-file-system-access": "^2023.10.5", "@types/wicg-file-system-access": "^2023.10.5",
"@types/ws": "^8.5.10", "@types/ws": "^8.5.10",
"@vitejs/plugin-react": "^4.2.1", "@vitejs/plugin-react": "^4.2.1",
"@vitest/web-worker": "^1.5.0",
"@wdio/cli": "^8.24.3", "@wdio/cli": "^8.24.3",
"@wdio/globals": "^8.36.0", "@wdio/globals": "^8.36.0",
"@wdio/local-runner": "^8.36.0", "@wdio/local-runner": "^8.36.0",

View File

@ -49,8 +49,6 @@ export default defineConfig({
// use: { ...devices['Desktop Chrome'] }, // use: { ...devices['Desktop Chrome'] },
// }, // },
/* Test against mobile viewports. */ /* Test against mobile viewports. */
// { // {
// name: 'Mobile Chrome', // name: 'Mobile Chrome',
@ -78,4 +76,4 @@ export default defineConfig({
// url: 'http://127.0.0.1:3000', // url: 'http://127.0.0.1:3000',
reuseExistingServer: !process.env.CI, reuseExistingServer: !process.env.CI,
}, },
}) })

8
src-tauri/Cargo.lock generated
View File

@ -2199,9 +2199,9 @@ dependencies = [
[[package]] [[package]]
name = "kittycad" name = "kittycad"
version = "0.2.67" version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc460442c165c8e707b1154551cefd08938d10bb80c78940e10cd9869487c325" checksum = "ddc922f0da3abc22661bf49423c9bfcc02ce6ae92dae007ede6990874789545b"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -4641,9 +4641,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-fs" name = "tauri-plugin-fs"
version = "2.0.0-beta.5" version = "2.0.0-beta.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c138126392c350aa68554e3461529b02680062c9146ab7b41d3ef97a2deaf93b" checksum = "609f53d90f08808679ecdd81455d9a4d0053291b92780695569f7400fdba27d5"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"glob", "glob",

View File

@ -16,13 +16,13 @@ tauri-build = { version = "2.0.0-beta.12", features = [] }
[dependencies] [dependencies]
anyhow = "1" anyhow = "1"
kittycad = "0.2.67" kittycad = "0.3.0"
oauth2 = "4.4.2" oauth2 = "4.4.2"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
tauri = { version = "2.0.0-beta.15", features = [ "devtools", "unstable"] } tauri = { version = "2.0.0-beta.15", features = [ "devtools", "unstable"] }
tauri-plugin-dialog = { version = "2.0.0-beta.5" } tauri-plugin-dialog = { version = "2.0.0-beta.5" }
tauri-plugin-fs = { version = "2.0.0-beta.5" } tauri-plugin-fs = { version = "2.0.0-beta.6" }
tauri-plugin-http = { version = "2.0.0-beta.5" } tauri-plugin-http = { version = "2.0.0-beta.5" }
tauri-plugin-os = { version = "2.0.0-beta.2" } tauri-plugin-os = { version = "2.0.0-beta.2" }
tauri-plugin-process = { version = "2.0.0-beta.2" } tauri-plugin-process = { version = "2.0.0-beta.2" }

View File

@ -55,5 +55,5 @@
} }
}, },
"productName": "Zoo Modeling App", "productName": "Zoo Modeling App",
"version": "0.17.3" "version": "0.18.1"
} }

View File

@ -193,6 +193,35 @@ export const Toolbar = () => {
Rectangle Rectangle
</ActionButton> </ActionButton>
</li> </li>
<li className="contents" key="circle-button">
<ActionButton
className={buttonClassName}
Element="button"
onClick={() =>
state.matches('Sketch.Circle tool')
? send('CancelSketch')
: send('Equip circle tool')
}
aria-pressed={state.matches('Sketch.Circle tool')}
icon={{
icon: 'circle',
iconClassName,
bgClassName,
}}
disabled={
(!state.can('Equip circle tool') &&
!state.matches('Sketch.Circle tool')) ||
disableAllButtons
}
title={
state.can('Equip circle tool')
? 'Circle'
: 'Can only be used when a sketch is empty currently'
}
>
Circle
</ActionButton>
</li>
</> </>
)} )}
{state.matches('Sketch.SketchIdle') && {state.matches('Sketch.SketchIdle') &&

View File

@ -246,13 +246,31 @@ export class CameraControls {
camSettings.center.y, camSettings.center.y,
camSettings.center.z camSettings.center.z
) )
this.camera.up.set(camSettings.up.x, camSettings.up.y, camSettings.up.z)
if (this.camera instanceof PerspectiveCamera && camSettings.ortho) {
this.useOrthographicCamera()
}
if (this.camera instanceof OrthographicCamera && !camSettings.ortho) {
this.usePerspectiveCamera()
}
if (this.camera instanceof PerspectiveCamera && camSettings.fov_y) { if (this.camera instanceof PerspectiveCamera && camSettings.fov_y) {
this.camera.fov = camSettings.fov_y this.camera.fov = camSettings.fov_y
} else if ( } else if (
this.camera instanceof OrthographicCamera && this.camera instanceof OrthographicCamera &&
camSettings.ortho_scale camSettings.ortho_scale
) { ) {
this.camera.zoom = camSettings.ortho_scale const distanceToTarget = new Vector3(
camSettings.pos.x,
camSettings.pos.y,
camSettings.pos.z
).distanceTo(
new Vector3(
camSettings.center.x,
camSettings.center.y,
camSettings.center.z
)
)
this.camera.zoom = (camSettings.ortho_scale * 40) / distanceToTarget
} }
this.onCameraChange() this.onCameraChange()
} }
@ -965,10 +983,10 @@ export class CameraControls {
// Pure function helpers // Pure function helpers
function calculateNearFarFromFOV(fov: number) { function calculateNearFarFromFOV(fov: number) {
const nearFarRatio = (fov - 3) / (45 - 3) // const nearFarRatio = (fov - 3) / (45 - 3)
// const z_near = 0.1 + nearFarRatio * (5 - 0.1) // const z_near = 0.1 + nearFarRatio * (5 - 0.1)
const z_far = 1000 + nearFarRatio * (100000 - 1000) // const z_far = 1000 + nearFarRatio * (100000 - 1000)
return { z_near: 0.1, z_far } return { z_near: 0.1, z_far: 1000 }
} }
function convertThreeCamValuesToEngineCam({ function convertThreeCamValuesToEngineCam({
@ -1043,3 +1061,62 @@ function _getInteractionType(
if (enableZoom && interactionGuards.zoom.dragCallback(event)) return 'zoom' if (enableZoom && interactionGuards.zoom.dragCallback(event)) return 'zoom'
return state return state
} }
/**
* Tells the engine to fire it's animation waits for it to finish and then requests camera settings
* to ensure the client-side camera is synchronized with the engine's camera state.
*
* @param engineCommandManager Our websocket singleton
* @param entityId - The ID of face or sketchPlane.
*/
export async function letEngineAnimateAndSyncCamAfter(
engineCommandManager: EngineCommandManager,
entityId: string
) {
await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'enable_sketch_mode',
adjust_camera: true,
animated: !isReducedMotion(),
ortho: false,
entity_id: entityId,
},
})
// wait 600ms (animation takes 500, + 100 for safety)
await new Promise((resolve) =>
setTimeout(resolve, isReducedMotion() ? 100 : 600)
)
await engineCommandManager.sendSceneCommand({
// CameraControls subscribes to default_camera_get_settings response events
// firing this at connection ensure the camera's are synced initially
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
})
await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'enable_sketch_mode',
adjust_camera: true,
animated: false,
ortho: true,
entity_id: entityId,
},
})
await new Promise((resolve) => setTimeout(resolve, 50))
await engineCommandManager.sendSceneCommand({
// CameraControls subscribes to default_camera_get_settings response events
// firing this at connection ensure the camera's are synced initially
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
})
}

View File

@ -3,7 +3,6 @@ import { useModelingContext } from 'hooks/useModelingContext'
import { cameraMouseDragGuards } from 'lib/cameraControls' import { cameraMouseDragGuards } from 'lib/cameraControls'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { useStore } from 'useStore'
import { ARROWHEAD, DEBUG_SHOW_BOTH_SCENES } from './sceneInfra' import { ARROWHEAD, DEBUG_SHOW_BOTH_SCENES } from './sceneInfra'
import { ReactCameraProperties } from './CameraControls' import { ReactCameraProperties } from './CameraControls'
import { throttle } from 'lib/utils' import { throttle } from 'lib/utils'
@ -47,10 +46,6 @@ export const ClientSideScene = ({
const canvasRef = useRef<HTMLDivElement>(null) const canvasRef = useRef<HTMLDivElement>(null)
const { state, send, context } = useModelingContext() const { state, send, context } = useModelingContext()
const { hideClient, hideServer } = useShouldHideScene() const { hideClient, hideServer } = useShouldHideScene()
const { setHighlightRange } = useStore((s) => ({
setHighlightRange: s.setHighlightRange,
highlightRange: s.highlightRange,
}))
// Listen for changes to the camera controls setting // Listen for changes to the camera controls setting
// and update the client-side scene's controls accordingly. // and update the client-side scene's controls accordingly.
@ -69,7 +64,6 @@ export const ClientSideScene = ({
const canvas = canvasRef.current const canvas = canvasRef.current
canvas.appendChild(sceneInfra.renderer.domElement) canvas.appendChild(sceneInfra.renderer.domElement)
sceneInfra.animate() sceneInfra.animate()
sceneInfra.setHighlightCallback(setHighlightRange)
canvas.addEventListener('mousemove', sceneInfra.onMouseMove, false) canvas.addEventListener('mousemove', sceneInfra.onMouseMove, false)
canvas.addEventListener('mousedown', sceneInfra.onMouseDown, false) canvas.addEventListener('mousedown', sceneInfra.onMouseDown, false)
canvas.addEventListener('mouseup', sceneInfra.onMouseUp, false) canvas.addEventListener('mouseup', sceneInfra.onMouseUp, false)

View File

@ -57,6 +57,7 @@ import {
kclManager, kclManager,
sceneInfra, sceneInfra,
codeManager, codeManager,
editorManager,
} from 'lib/singletons' } from 'lib/singletons'
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst' import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
import { executeAst, useStore } from 'useStore' import { executeAst, useStore } from 'useStore'
@ -96,6 +97,7 @@ import {
getRectangleCallExpressions, getRectangleCallExpressions,
updateRectangleSketch, updateRectangleSketch,
} from 'lib/rectangleTool' } from 'lib/rectangleTool'
import { circleAsCallExpressions, updateCircleSketch } from 'lib/circleTool'
type DraftSegment = 'line' | 'tangentialArcTo' type DraftSegment = 'line' | 'tangentialArcTo'
@ -214,8 +216,9 @@ export class SceneEntities {
const orthoFactor = orthoScale(sceneInfra.camControls.camera) const orthoFactor = orthoScale(sceneInfra.camControls.camera)
const baseXColor = 0x000055 const baseXColor = 0x000055
const baseYColor = 0x550000 const baseYColor = 0x550000
const xAxisGeometry = new BoxGeometry(100000, 0.3, 0.01) const axisPixelWidth = 1.6
const yAxisGeometry = new BoxGeometry(0.3, 100000, 0.01) const xAxisGeometry = new BoxGeometry(100000, axisPixelWidth, 0.01)
const yAxisGeometry = new BoxGeometry(axisPixelWidth, 100000, 0.01)
const xAxisMaterial = new MeshBasicMaterial({ const xAxisMaterial = new MeshBasicMaterial({
color: baseXColor, color: baseXColor,
depthTest: false, depthTest: false,
@ -578,7 +581,7 @@ export class SceneEntities {
...this.mouseEnterLeaveCallbacks(), ...this.mouseEnterLeaveCallbacks(),
}) })
} }
setupRectangleOriginListener = () => { setupOriginListener = (type: 'circle' | 'rectangle') => {
sceneInfra.setCallbacks({ sceneInfra.setCallbacks({
onClick: (args) => { onClick: (args) => {
const twoD = args.intersectionPoint?.twoD const twoD = args.intersectionPoint?.twoD
@ -587,7 +590,7 @@ export class SceneEntities {
return return
} }
sceneInfra.modelingSend({ sceneInfra.modelingSend({
type: 'Add rectangle origin', type: `Add ${type} origin`,
data: [twoD.x, twoD.y], data: [twoD.x, twoD.y],
}) })
}, },
@ -745,6 +748,154 @@ export class SceneEntities {
}, },
}) })
} }
setupDraftCircle = async (
sketchPathToNode: PathToNode,
forward: [number, number, number],
up: [number, number, number],
sketchOrigin: [number, number, number],
circleOrigin: [x: number, y: number]
) => {
let _ast = JSON.parse(JSON.stringify(kclManager.ast))
const variableDeclarationName =
getNodeFromPath<VariableDeclaration>(
_ast,
sketchPathToNode || [],
'VariableDeclaration'
)?.node?.declarations?.[0]?.id?.name || ''
const tags: [string] = [findUniqueName(_ast, 'circle')]
const startSketchOn = getNodeFromPath<VariableDeclaration>(
_ast,
sketchPathToNode || [],
'VariableDeclaration'
)?.node?.declarations
const startSketchOnInit = startSketchOn?.[0]?.init
startSketchOn[0].init = createPipeExpression([
startSketchOnInit,
...circleAsCallExpressions(circleOrigin, tags),
])
_ast = parse(recast(_ast))
const { programMemoryOverride, truncatedAst } = await this.setupSketch({
sketchPathToNode,
forward,
up,
position: sketchOrigin,
maybeModdedAst: _ast,
draftExpressionsIndices: { start: 0, end: 1 },
})
sceneInfra.setCallbacks({
onMove: async (args) => {
// Update the radius of the draft rectangle
const pathToNodeTwo = JSON.parse(JSON.stringify(sketchPathToNode))
pathToNodeTwo[1][0] = 0
const sketchInit = getNodeFromPath<VariableDeclaration>(
truncatedAst,
pathToNodeTwo || [],
'VariableDeclaration'
)?.node?.declarations?.[0]?.init
const x = (args.intersectionPoint.twoD.x || 0) - circleOrigin[0]
const y = (args.intersectionPoint.twoD.y || 0) - circleOrigin[1]
if (sketchInit.type === 'PipeExpression') {
updateCircleSketch(sketchInit, x, y, tags[0])
}
const { programMemory } = await executeAst({
ast: truncatedAst,
useFakeExecutor: true,
engineCommandManager: this.engineCommandManager,
programMemoryOverride,
})
this.sceneProgramMemory = programMemory
const sketchGroup = programMemory.root[
variableDeclarationName
] as SketchGroup
const sgPaths = sketchGroup.value
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
this.updateSegment(
sketchGroup.start,
0,
0,
_ast,
orthoFactor,
sketchGroup
)
sgPaths.forEach((seg, index) =>
this.updateSegment(seg, index, 0, _ast, orthoFactor, sketchGroup)
)
},
onClick: async (args) => {
// Commit the circle to the full AST/code and return to sketch.idle
const radiusPoint = args.intersectionPoint?.twoD
if (!radiusPoint || args.mouseEvent.button !== 0) return
const x = roundOff((radiusPoint.x || 0) - circleOrigin[0])
const y = roundOff((radiusPoint.y || 0) - circleOrigin[1])
const sketchInit = getNodeFromPath<VariableDeclaration>(
_ast,
sketchPathToNode || [],
'VariableDeclaration'
)?.node?.declarations?.[0]?.init
if (sketchInit.type === 'PipeExpression') {
updateCircleSketch(sketchInit, x, y, tags[0])
_ast = parse(recast(_ast))
console.log('onClick', {
sketchInit: sketchInit,
_ast,
x,
y,
truncatedAst,
})
// Update the primary AST and unequip the rectangle tool
await kclManager.executeAstMock(_ast)
sceneInfra.modelingSend({ type: 'CancelSketch' })
const { programMemory } = await executeAst({
ast: _ast,
useFakeExecutor: true,
engineCommandManager: this.engineCommandManager,
programMemoryOverride,
})
// Prepare to update the THREEjs scene
this.sceneProgramMemory = programMemory
const sketchGroup = programMemory.root[
variableDeclarationName
] as SketchGroup
const sgPaths = sketchGroup.value
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
// Update the starting segment of the THREEjs scene
this.updateSegment(
sketchGroup.start,
0,
0,
_ast,
orthoFactor,
sketchGroup
)
// Update the rest of the segments of the THREEjs scene
sgPaths.forEach((seg, index) =>
this.updateSegment(seg, index, 0, _ast, orthoFactor, sketchGroup)
)
}
},
})
}
setupSketchIdleCallbacks = ({ setupSketchIdleCallbacks = ({
pathToNode, pathToNode,
up, up,
@ -1323,30 +1474,31 @@ export class SceneEntities {
selected.material.color = defaultPlaneColor(type) selected.material.color = defaultPlaneColor(type)
}, },
onClick: async (args) => { onClick: async (args) => {
const checkExtrudeFaceClick = async (): Promise<boolean> => { const checkExtrudeFaceClick = async (): Promise<
['face' | 'plane' | 'other', string]
> => {
const { streamDimensions } = useStore.getState() const { streamDimensions } = useStore.getState()
const { entity_id } = await sendSelectEventToEngine( const { entity_id } = await sendSelectEventToEngine(
args?.mouseEvent, args?.mouseEvent,
document.getElementById('video-stream') as HTMLVideoElement, document.getElementById('video-stream') as HTMLVideoElement,
streamDimensions streamDimensions
) )
if (!entity_id) return false if (!entity_id) return ['other', '']
if (
engineCommandManager.defaultPlanes?.xy === entity_id ||
engineCommandManager.defaultPlanes?.xz === entity_id ||
engineCommandManager.defaultPlanes?.yz === entity_id
) {
return ['plane', entity_id]
}
const artifact = this.engineCommandManager.artifactMap[entity_id] const artifact = this.engineCommandManager.artifactMap[entity_id]
if (artifact?.commandType !== 'solid3d_get_extrusion_face_info') if (artifact?.commandType !== 'solid3d_get_extrusion_face_info')
return false return ['other', entity_id]
const faceInfo: Models['FaceIsPlanar_type'] = (
await this.engineCommandManager.sendSceneCommand({ const faceInfo = await getFaceDetails(entity_id)
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) if (!faceInfo?.origin || !faceInfo?.z_axis || !faceInfo?.y_axis)
return false return ['other', entity_id]
const { z_axis, origin, y_axis } = faceInfo const { z_axis, y_axis, origin } = faceInfo
const pathToNode = getNodePathFromSourceRange( const pathToNode = getNodePathFromSourceRange(
kclManager.ast, kclManager.ast,
artifact.range artifact.range
@ -1366,12 +1518,15 @@ export class SceneEntities {
artifact?.additionalData?.type === 'cap' artifact?.additionalData?.type === 'cap'
? artifact.additionalData.info ? artifact.additionalData.info
: 'none', : 'none',
faceId: entity_id,
}, },
}) })
return true return ['face', entity_id]
} }
if (await checkExtrudeFaceClick()) return const faceResult = await checkExtrudeFaceClick()
console.log('faceResult', faceResult)
if (faceResult[0] === 'face') 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
@ -1397,6 +1552,7 @@ export class SceneEntities {
plane: planeString, plane: planeString,
zAxis, zAxis,
yAxis, yAxis,
planeId: faceResult[1],
}, },
}) })
}, },
@ -1423,7 +1579,7 @@ export class SceneEntities {
parent.userData.pathToNode, parent.userData.pathToNode,
'CallExpression' 'CallExpression'
).node ).node
sceneInfra.highlightCallback([node.start, node.end]) editorManager.setHighlightRange([node.start, node.end])
const yellow = 0xffff00 const yellow = 0xffff00
colorSegment(selected, yellow) colorSegment(selected, yellow)
const extraSegmentGroup = parent.getObjectByName(EXTRA_SEGMENT_HANDLE) const extraSegmentGroup = parent.getObjectByName(EXTRA_SEGMENT_HANDLE)
@ -1459,10 +1615,10 @@ export class SceneEntities {
} }
return return
} }
sceneInfra.highlightCallback([0, 0]) editorManager.setHighlightRange([0, 0])
}, },
onMouseLeave: ({ selected, ...rest }: OnMouseEnterLeaveArgs) => { onMouseLeave: ({ selected, ...rest }: OnMouseEnterLeaveArgs) => {
sceneInfra.highlightCallback([0, 0]) editorManager.setHighlightRange([0, 0])
const parent = getParentGroup(selected, [ const parent = getParentGroup(selected, [
STRAIGHT_SEGMENT, STRAIGHT_SEGMENT,
TANGENTIAL_ARC_TO_SEGMENT, TANGENTIAL_ARC_TO_SEGMENT,
@ -1680,7 +1836,7 @@ export async function getSketchOrientationDetails(
sketchPathToNode: PathToNode sketchPathToNode: PathToNode
): Promise<{ ): Promise<{
quat: Quaternion quat: Quaternion
sketchDetails: SketchDetails sketchDetails: SketchDetails & { faceId?: string }
}> { }> {
const sketchGroup = sketchGroupFromPathToNode({ const sketchGroup = sketchGroupFromPathToNode({
pathToNode: sketchPathToNode, pathToNode: sketchPathToNode,
@ -1696,20 +1852,13 @@ export async function getSketchOrientationDetails(
zAxis: [zAxis.x, zAxis.y, zAxis.z], zAxis: [zAxis.x, zAxis.y, zAxis.z],
yAxis: [sketchGroup.yAxis.x, sketchGroup.yAxis.y, sketchGroup.yAxis.z], yAxis: [sketchGroup.yAxis.x, sketchGroup.yAxis.y, sketchGroup.yAxis.z],
origin: [0, 0, 0], origin: [0, 0, 0],
faceId: sketchGroup.on.id,
}, },
} }
} }
if (sketchGroup.on.type === 'face') { if (sketchGroup.on.type === 'face') {
const faceInfo: Models['FaceIsPlanar_type'] = ( const faceInfo = await getFaceDetails(sketchGroup.on.faceId)
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) if (!faceInfo?.origin || !faceInfo?.z_axis || !faceInfo?.y_axis)
throw new Error('faceInfo') throw new Error('faceInfo')
const { z_axis, y_axis, origin } = faceInfo const { z_axis, y_axis, origin } = faceInfo
@ -1724,6 +1873,7 @@ export async function getSketchOrientationDetails(
zAxis: [z_axis.x, z_axis.y, z_axis.z], zAxis: [z_axis.x, z_axis.y, z_axis.z],
yAxis: [y_axis.x, y_axis.y, y_axis.z], yAxis: [y_axis.x, y_axis.y, y_axis.z],
origin: [origin.x, origin.y, origin.z], origin: [origin.x, origin.y, origin.z],
faceId: sketchGroup.on.faceId,
}, },
} }
} }
@ -1732,6 +1882,46 @@ export async function getSketchOrientationDetails(
) )
} }
/**
* Retrieves orientation details for a given entity representing a face (brep face or default plane).
* This function asynchronously fetches and returns the origin, x-axis, y-axis, and z-axis details
* for a specified entity ID. It is primarily used to obtain the orientation of a face in the scene,
* which is essential for calculating the correct positioning and alignment of the client side sketch.
*
* @param entityId - The ID of the entity for which orientation details are being fetched.
* @returns A promise that resolves with the orientation details of the face.
*/
async function getFaceDetails(
entityId: string
): Promise<Models['FaceIsPlanar_type']> {
// TODO mode engine connection to allow batching returns and batch the following
await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'enable_sketch_mode',
adjust_camera: false,
animated: false,
ortho: false,
entity_id: entityId,
},
})
// TODO change typing to get_sketch_mode_plane once lib is updated
const faceInfo: Models['FaceIsPlanar_type'] = (
await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: { type: 'get_sketch_mode_plane' },
})
)?.data?.data
await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: { type: 'sketch_mode_disable' },
})
return faceInfo
}
export function getQuaternionFromZAxis(zAxis: Vector3): Quaternion { 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)

View File

@ -24,7 +24,6 @@ import {
import { compareVec2Epsilon2 } from 'lang/std/sketch' import { compareVec2Epsilon2 } from 'lang/std/sketch'
import { useModelingContext } from 'hooks/useModelingContext' import { useModelingContext } from 'hooks/useModelingContext'
import * as TWEEN from '@tweenjs/tween.js' import * as TWEEN from '@tweenjs/tween.js'
import { SourceRange } from 'lang/wasm'
import { Axis } from 'lib/selections' import { Axis } from 'lib/selections'
import { type BaseUnit } from 'lib/settings/settingsTypes' import { type BaseUnit } from 'lib/settings/settingsTypes'
import { CameraControls } from './CameraControls' import { CameraControls } from './CameraControls'
@ -149,10 +148,6 @@ export class SceneInfra {
onMouseLeave: () => {}, onMouseLeave: () => {},
}) })
} }
highlightCallback: (a: SourceRange) => void = () => {}
setHighlightCallback(cb: (a: SourceRange) => void) {
this.highlightCallback = cb
}
modelingSend: SendType = (() => {}) as any modelingSend: SendType = (() => {}) as any
setSend(send: SendType) { setSend(send: SendType) {

View File

@ -1,11 +1,9 @@
import { useModelingContext } from 'hooks/useModelingContext' import { useModelingContext } from 'hooks/useModelingContext'
import { kclManager } from 'lib/singletons' import { editorManager, kclManager } from 'lib/singletons'
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst' import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { useStore } from 'useStore'
export function AstExplorer() { export function AstExplorer() {
const setHighlightRange = useStore((s) => s.setHighlightRange)
const { context } = useModelingContext() const { context } = useModelingContext()
const pathToNode = getNodePathFromSourceRange( const pathToNode = getNodePathFromSourceRange(
// TODO maybe need to have callback to make sure it stays in sync // TODO maybe need to have callback to make sure it stays in sync
@ -42,7 +40,7 @@ export function AstExplorer() {
<div <div
className="h-full relative" className="h-full relative"
onMouseLeave={(e) => { onMouseLeave={(e) => {
setHighlightRange([0, 0]) editorManager.setHighlightRange([0, 0])
}} }}
> >
<pre className="text-xs"> <pre className="text-xs">
@ -88,7 +86,6 @@ function DisplayObj({
filterKeys: string[] filterKeys: string[]
node: any node: any
}) { }) {
const setHighlightRange = useStore((s) => s.setHighlightRange)
const { send } = useModelingContext() const { send } = useModelingContext()
const ref = useRef<HTMLPreElement>(null) const ref = useRef<HTMLPreElement>(null)
const [hasCursor, setHasCursor] = useState(false) const [hasCursor, setHasCursor] = useState(false)
@ -112,12 +109,12 @@ function DisplayObj({
hasCursor ? 'bg-violet-100/80 dark:bg-violet-100/25' : '' hasCursor ? 'bg-violet-100/80 dark:bg-violet-100/25' : ''
}`} }`}
onMouseEnter={(e) => { onMouseEnter={(e) => {
setHighlightRange([obj?.start || 0, obj.end]) editorManager.setHighlightRange([obj?.start || 0, obj.end])
e.stopPropagation() e.stopPropagation()
}} }}
onMouseMove={(e) => { onMouseMove={(e) => {
e.stopPropagation() e.stopPropagation()
setHighlightRange([obj?.start || 0, obj.end]) editorManager.setHighlightRange([obj?.start || 0, obj.end])
}} }}
onClick={(e) => { onClick={(e) => {
send({ send({

View File

@ -137,34 +137,33 @@ export function useCalc({
setAvailableVarInfo(varInfo) setAvailableVarInfo(varInfo)
}, [kclManager.ast, kclManager.programMemory, selectionRange]) }, [kclManager.ast, kclManager.programMemory, selectionRange])
useEffect(async () => { useEffect(() => {
try { try {
const code = `const __result__ = ${value}` const code = `const __result__ = ${value}`
parse(code).then((ast) => { const ast = parse(code)
const _programMem: any = { root: {}, return: null } const _programMem: any = { root: {}, return: null }
availableVarInfo.variables.forEach(({ key, value }) => { availableVarInfo.variables.forEach(({ key, value }) => {
_programMem.root[key] = { type: 'userVal', value, __meta: [] } _programMem.root[key] = { type: 'userVal', value, __meta: [] }
}) })
executeAst({ executeAst({
ast, ast,
engineCommandManager, engineCommandManager,
useFakeExecutor: true, useFakeExecutor: true,
programMemoryOverride: JSON.parse( programMemoryOverride: JSON.parse(
JSON.stringify(kclManager.programMemory) JSON.stringify(kclManager.programMemory)
), ),
}).then(({ programMemory }) => { }).then(({ programMemory }) => {
const resultDeclaration = ast.body.find( const resultDeclaration = ast.body.find(
(a) => (a) =>
a.type === 'VariableDeclaration' && a.type === 'VariableDeclaration' &&
a.declarations?.[0]?.id?.name === '__result__' a.declarations?.[0]?.id?.name === '__result__'
) )
const init = const init =
resultDeclaration?.type === 'VariableDeclaration' && resultDeclaration?.type === 'VariableDeclaration' &&
resultDeclaration?.declarations?.[0]?.init resultDeclaration?.declarations?.[0]?.init
const result = programMemory?.root?.__result__?.value const result = programMemory?.root?.__result__?.value
setCalcResult(typeof result === 'number' ? String(result) : 'NAN') setCalcResult(typeof result === 'number' ? String(result) : 'NAN')
init && setValueNode(init) init && setValueNode(init)
})
}) })
} catch (e) { } catch (e) {
setCalcResult('NAN') setCalcResult('NAN')

View File

@ -1,6 +1,7 @@
import { useMachine } from '@xstate/react' import { useMachine } from '@xstate/react'
import { editorManager } from 'lib/singletons'
import { commandBarMachine } from 'machines/commandBarMachine' import { commandBarMachine } from 'machines/commandBarMachine'
import { createContext } from 'react' import { createContext, useEffect } from 'react'
import { EventFrom, StateFrom } from 'xstate' import { EventFrom, StateFrom } from 'xstate'
type CommandsContextType = { type CommandsContextType = {
@ -30,6 +31,10 @@ export const CommandBarProvider = ({
}, },
}) })
useEffect(() => {
editorManager.setCommandBarSend(commandBarSend)
})
return ( return (
<CommandsContext.Provider <CommandsContext.Provider
value={{ value={{

View File

@ -61,6 +61,16 @@ const CustomIconMap = {
/> />
</svg> </svg>
), ),
circle: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10 2.5C9.01509 2.5 8.03982 2.69399 7.12988 3.0709C6.21994 3.44781 5.39314 4.00026 4.6967 4.6967C4.00026 5.39314 3.44782 6.21993 3.07091 7.12987C2.694 8.03981 2.5 9.01508 2.5 10C2.5 10.9849 2.69399 11.9602 3.0709 12.8701C3.44781 13.7801 4.00026 14.6069 4.6967 15.3033C5.39314 15.9997 6.21993 16.5522 7.12987 16.9291C8.03982 17.306 9.01509 17.5 10 17.5C10.9849 17.5 11.9602 17.306 12.8701 16.9291C13.7801 16.5522 14.6069 15.9997 15.3033 15.3033C15.9997 14.6069 16.5522 13.7801 16.9291 12.8701C17.306 11.9602 17.5 10.9849 17.5 10C17.5 9.01509 17.306 8.03982 16.9291 7.12988C16.5522 6.21993 15.9997 5.39314 15.3033 4.6967C14.6069 4.00026 13.7801 3.44781 12.8701 3.0709C11.9602 2.69399 10.9849 2.5 10 2.5ZM6.7472 2.14702C7.77847 1.71986 8.88377 1.5 10 1.5C11.1162 1.5 12.2215 1.71986 13.2528 2.14702C14.2841 2.57419 15.2211 3.20029 16.0104 3.98959C16.7997 4.77889 17.4258 5.71592 17.853 6.74719C18.2801 7.77846 18.5 8.88377 18.5 10C18.5 11.1162 18.2801 12.2215 17.853 13.2528C17.4258 14.2841 16.7997 15.2211 16.0104 16.0104C15.2211 16.7997 14.2841 17.4258 13.2528 17.853C12.2215 18.2801 11.1162 18.5 10 18.5C8.88376 18.5 7.77846 18.2801 6.74719 17.853C5.71592 17.4258 4.77889 16.7997 3.98959 16.0104C3.20029 15.2211 2.57419 14.2841 2.14702 13.2528C1.71986 12.2215 1.5 11.1162 1.5 10C1.5 8.88376 1.71986 7.77845 2.14703 6.74719C2.57419 5.71592 3.2003 4.77889 3.9896 3.98959C4.7789 3.20029 5.71593 2.57419 6.7472 2.14702Z"
fill="currentColor"
/>
</svg>
),
clipboardCheckmark: ( clipboardCheckmark: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path <path

View File

@ -17,6 +17,7 @@ import {
sceneInfra, sceneInfra,
engineCommandManager, engineCommandManager,
codeManager, codeManager,
editorManager,
} from 'lib/singletons' } from 'lib/singletons'
import { applyConstraintHorzVertDistance } from './Toolbar/SetHorzVertDistance' import { applyConstraintHorzVertDistance } from './Toolbar/SetHorzVertDistance'
import { import {
@ -53,10 +54,9 @@ 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 { EditorSelection } from '@uiw/react-codemirror'
import { Vector3 } from 'three'
import { quaternionFromUpNForward } from 'clientSideScene/helpers'
import { CoreDumpManager } from 'lib/coredump' import { CoreDumpManager } from 'lib/coredump'
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'
import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls'
type MachineContext<T extends AnyStateMachine> = { type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T> state: StateFrom<T>
@ -98,17 +98,6 @@ export const ModelingMachineProvider = ({
) )
useHotkeys('meta + shift + .', () => coreDump(coreDumpManager, true)) useHotkeys('meta + shift + .', () => coreDump(coreDumpManager, true))
const {
isShiftDown,
editorView,
setLastCodeMirrorSelectionUpdatedFromScene,
} = useStore((s) => ({
isShiftDown: s.isShiftDown,
editorView: s.editorView,
setLastCodeMirrorSelectionUpdatedFromScene:
s.setLastCodeMirrorSelectionUpdatedFromScene,
}))
// Settings machine setup // Settings machine setup
// const retrievedSettings = useRef( // const retrievedSettings = useRef(
// localStorage?.getItem(MODELING_PERSIST_KEY) || '{}' // localStorage?.getItem(MODELING_PERSIST_KEY) || '{}'
@ -135,29 +124,33 @@ export const ModelingMachineProvider = ({
'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 (!editorManager.editorView) return {}
const dispatchSelection = (selection?: EditorSelection) => { const dispatchSelection = (selection?: EditorSelection) => {
if (!selection) return // TODO less of hack for the below please if (!selection) return // TODO less of hack for the below please
setLastCodeMirrorSelectionUpdatedFromScene(Date.now()) editorManager.lastSelectionEvent = Date.now()
setTimeout(() => editorView.dispatch({ selection })) setTimeout(() => {
if (editorManager.editorView) {
editorManager.editorView.dispatch({ selection })
}
})
} }
let selections: Selections = { let selections: Selections = {
codeBasedSelections: [], codeBasedSelections: [],
otherSelections: [], otherSelections: [],
} }
if (setSelections.selectionType === 'singleCodeCursor') { if (setSelections.selectionType === 'singleCodeCursor') {
if (!setSelections.selection && isShiftDown) { if (!setSelections.selection && editorManager.isShiftDown) {
} else if (!setSelections.selection && !isShiftDown) { } else if (!setSelections.selection && !editorManager.isShiftDown) {
selections = { selections = {
codeBasedSelections: [], codeBasedSelections: [],
otherSelections: [], otherSelections: [],
} }
} else if (setSelections.selection && !isShiftDown) { } else if (setSelections.selection && !editorManager.isShiftDown) {
selections = { selections = {
codeBasedSelections: [setSelections.selection], codeBasedSelections: [setSelections.selection],
otherSelections: [], otherSelections: [],
} }
} else if (setSelections.selection && isShiftDown) { } else if (setSelections.selection && editorManager.isShiftDown) {
selections = { selections = {
codeBasedSelections: [ codeBasedSelections: [
...selectionRanges.codeBasedSelections, ...selectionRanges.codeBasedSelections,
@ -180,6 +173,7 @@ export const ModelingMachineProvider = ({
engineCommandManager.sendSceneCommand(event) engineCommandManager.sendSceneCommand(event)
) )
updateSceneObjectColors() updateSceneObjectColors()
return { return {
selectionRanges: selections, selectionRanges: selections,
} }
@ -192,7 +186,7 @@ export const ModelingMachineProvider = ({
} }
if (setSelections.selectionType === 'otherSelection') { if (setSelections.selectionType === 'otherSelection') {
if (isShiftDown) { if (editorManager.isShiftDown) {
selections = { selections = {
codeBasedSelections: selectionRanges.codeBasedSelections, codeBasedSelections: selectionRanges.codeBasedSelections,
otherSelections: [setSelections.selection], otherSelections: [setSelections.selection],
@ -324,16 +318,9 @@ export const ModelingMachineProvider = ({
) )
await kclManager.executeAstMock(modifiedAst) await kclManager.executeAstMock(modifiedAst)
const forward = new Vector3(...data.zAxis) await letEngineAnimateAndSyncCamAfter(
const up = new Vector3(...data.yAxis) engineCommandManager,
data.faceId
let target = new Vector3(...data.position).multiplyScalar(
sceneInfra._baseUnitMultiplier
)
const quaternion = quaternionFromUpNForward(up, forward)
await sceneInfra.camControls.tweenCameraToQuaternion(
quaternion,
target
) )
return { return {
@ -348,6 +335,7 @@ export const ModelingMachineProvider = ({
data.plane data.plane
) )
await kclManager.updateAst(modifiedAst, false) await kclManager.updateAst(modifiedAst, false)
sceneInfra.camControls.syncDirection = 'clientToEngine'
const quat = await getSketchQuaternion(pathToNode, data.zAxis) const quat = await getSketchQuaternion(pathToNode, data.zAxis)
await sceneInfra.camControls.tweenCameraToQuaternion(quat) await sceneInfra.camControls.tweenCameraToQuaternion(quat)
return { return {
@ -364,9 +352,9 @@ export const ModelingMachineProvider = ({
sourceRange sourceRange
) )
const info = await getSketchOrientationDetails(sketchPathToNode || []) const info = await getSketchOrientationDetails(sketchPathToNode || [])
await sceneInfra.camControls.tweenCameraToQuaternion( await letEngineAnimateAndSyncCamAfter(
info.quat, engineCommandManager,
new Vector3(...info.sketchDetails.origin) info?.sketchDetails?.faceId || ''
) )
return { return {
sketchPathToNode: sketchPathToNode || [], sketchPathToNode: sketchPathToNode || [],
@ -516,6 +504,19 @@ export const ModelingMachineProvider = ({
}) })
}, [modelingSend]) }, [modelingSend])
// Give the state back to the editorManager.
useEffect(() => {
editorManager.modelingSend = modelingSend
}, [modelingSend])
useEffect(() => {
editorManager.modelingEvent = modelingState.event
}, [modelingState.event])
useEffect(() => {
editorManager.selectionRanges = modelingState.context.selectionRanges
}, [modelingState.context.selectionRanges])
useStateMachineCommands({ useStateMachineCommands({
machineId: 'modeling', machineId: 'modeling',
state: modelingState, state: modelingState,

View File

@ -1,13 +1,8 @@
import { undo, redo } from '@codemirror/commands'
import ReactCodeMirror from '@uiw/react-codemirror' import ReactCodeMirror from '@uiw/react-codemirror'
import { TEST } from 'env' import { TEST } from 'env'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { useConvertToVariable } from 'hooks/useToolbarGuards'
import { Themes, getSystemTheme } from 'lib/theme' import { Themes, getSystemTheme } from 'lib/theme'
import { useEffect, useMemo, useRef } from 'react' import { useEffect, useMemo } from 'react'
import { useStore } from 'useStore'
import { processCodeMirrorRanges } from 'lib/selections'
import { highlightSelectionMatches, searchKeymap } from '@codemirror/search' import { highlightSelectionMatches, searchKeymap } from '@codemirror/search'
import { lineHighlightField } from 'editor/highlightextension' import { lineHighlightField } from 'editor/highlightextension'
import { roundOff } from 'lib/utils' import { roundOff } from 'lib/utils'
@ -21,7 +16,6 @@ import {
EditorView, EditorView,
dropCursor, dropCursor,
drawSelection, drawSelection,
ViewUpdate,
} from '@codemirror/view' } from '@codemirror/view'
import { import {
indentWithTab, indentWithTab,
@ -29,7 +23,7 @@ import {
historyKeymap, historyKeymap,
history, history,
} from '@codemirror/commands' } from '@codemirror/commands'
import { lintGutter, lintKeymap, linter } from '@codemirror/lint' import { lintGutter, lintKeymap } from '@codemirror/lint'
import { import {
foldGutter, foldGutter,
foldKeymap, foldKeymap,
@ -39,25 +33,20 @@ import {
syntaxHighlighting, syntaxHighlighting,
defaultHighlightStyle, defaultHighlightStyle,
} from '@codemirror/language' } from '@codemirror/language'
import { useModelingContext } from 'hooks/useModelingContext'
import interact from '@replit/codemirror-interact' import interact from '@replit/codemirror-interact'
import { engineCommandManager, sceneInfra, kclManager } from 'lib/singletons' import { kclManager, editorManager, codeManager } from 'lib/singletons'
import { useKclContext } from 'lang/KclProvider'
import { ModelingMachineEvent } from 'machines/modelingMachine'
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'
import { isTauri } from 'lib/isTauri' import { isTauri } from 'lib/isTauri'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { paths } from 'lib/paths' import { paths } from 'lib/paths'
import makeUrlPathRelative from 'lib/makeUrlPathRelative' import makeUrlPathRelative from 'lib/makeUrlPathRelative'
import { useLspContext } from 'components/LspProvider' import { useLspContext } from 'components/LspProvider'
import { Prec, EditorState, Extension, SelectionRange } from '@codemirror/state' import { Prec, EditorState, Extension } from '@codemirror/state'
import { import {
closeBrackets, closeBrackets,
closeBracketsKeymap, closeBracketsKeymap,
completionKeymap, completionKeymap,
hasNextSnippetField,
} from '@codemirror/autocomplete' } from '@codemirror/autocomplete'
import { kclErrorsToDiagnostics } from 'lang/errors'
export const editorShortcutMeta = { export const editorShortcutMeta = {
formatCode: { formatCode: {
@ -77,13 +66,6 @@ export const KclEditorPane = () => {
context.app.theme.current === Themes.System context.app.theme.current === Themes.System
? getSystemTheme() ? getSystemTheme()
: context.app.theme.current : context.app.theme.current
const { editorView, setEditorView, isShiftDown } = useStore((s) => ({
editorView: s.editorView,
setEditorView: s.setEditorView,
isShiftDown: s.isShiftDown,
}))
const { editorCode, errors } = useKclContext()
const lastEvent = useRef({ event: '', time: Date.now() })
const { copilotLSP, kclLSP } = useLspContext() const { copilotLSP, kclLSP } = useLspContext()
const navigate = useNavigate() const navigate = useNavigate()
@ -96,90 +78,15 @@ export const KclEditorPane = () => {
useHotkeys('mod+z', (e) => { useHotkeys('mod+z', (e) => {
e.preventDefault() e.preventDefault()
if (editorView) { editorManager.undo()
undo(editorView)
}
}) })
useHotkeys('mod+shift+z', (e) => { useHotkeys('mod+shift+z', (e) => {
e.preventDefault() e.preventDefault()
if (editorView) { editorManager.redo()
redo(editorView)
}
}) })
const { const textWrapping = context.textEditor.textWrapping
context: { selectionRanges }, const cursorBlinking = context.textEditor.blinkingCursor
send,
state,
} = useModelingContext()
const { settings } = useSettingsAuthContext()
const textWrapping = settings.context.textEditor.textWrapping
const cursorBlinking = settings.context.textEditor.blinkingCursor
const { commandBarSend } = useCommandsContext()
const { enable: convertEnabled, handleClick: convertCallback } =
useConvertToVariable()
const lastSelection = useRef('')
const onUpdate = (viewUpdate: ViewUpdate) => {
// If we are just fucking around in a snippet, return early and don't
// trigger stuff below that might cause the component to re-render.
// Otherwise we will not be able to tab thru the snippet portions.
// We explicitly dont check HasPrevSnippetField because we always add
// a ${} to the end of the function so that's fine.
if (hasNextSnippetField(viewUpdate.view.state)) {
return
}
if (!editorView) {
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
const ignoreEvents: ModelingMachineEvent['type'][] = [
'Equip Line tool',
'Equip tangential arc to',
]
if (ignoreEvents.includes(state.event.type)) return
const eventInfo = processCodeMirrorRanges({
codeMirrorRanges: viewUpdate.state.selection.ranges,
selectionRanges,
isShiftDown,
})
if (!eventInfo) return
const deterministicEventInfo = {
...eventInfo,
engineEvents: eventInfo.engineEvents.map((e) => ({
...e,
cmd_id: 'static',
})),
}
const stringEvent = JSON.stringify(deterministicEventInfo)
if (
stringEvent === lastEvent.current.event &&
Date.now() - lastEvent.current.time < 500
)
return // don't repeat events
lastEvent.current = { event: stringEvent, time: Date.now() }
send(eventInfo.modelingEvent)
eventInfo.engineEvents.forEach((event) =>
engineCommandManager.sendSceneCommand(event)
)
}
const editorExtensions = useMemo(() => { const editorExtensions = useMemo(() => {
const extensions = [ const extensions = [
@ -202,7 +109,7 @@ export const KclEditorPane = () => {
{ {
key: 'Meta-k', key: 'Meta-k',
run: () => { run: () => {
commandBarSend({ type: 'Open' }) editorManager.commandBarSend({ type: 'Open' })
return false return false
}, },
}, },
@ -216,11 +123,7 @@ export const KclEditorPane = () => {
{ {
key: editorShortcutMeta.convertToVariable.codeMirror, key: editorShortcutMeta.convertToVariable.codeMirror,
run: () => { run: () => {
if (convertEnabled) { return editorManager.convertToVariable()
convertCallback()
return true
}
return false
}, },
}, },
]), ]),
@ -233,9 +136,6 @@ export const KclEditorPane = () => {
if (!TEST) { if (!TEST) {
extensions.push( extensions.push(
lintGutter(), lintGutter(),
linter((_view: EditorView) => {
return kclErrorsToDiagnostics(errors)
}),
lineNumbers(), lineNumbers(),
highlightActiveLineGutter(), highlightActiveLineGutter(),
highlightSpecialChars(), highlightSpecialChars(),
@ -288,13 +188,7 @@ export const KclEditorPane = () => {
} }
return extensions return extensions
}, [ }, [kclLSP, copilotLSP, textWrapping.current, cursorBlinking.current])
kclLSP,
copilotLSP,
textWrapping.current,
cursorBlinking.current,
convertCallback,
])
return ( return (
<div <div
@ -302,18 +196,15 @@ export const KclEditorPane = () => {
className={'absolute inset-0 ' + (cursorBlinking.current ? 'blink' : '')} className={'absolute inset-0 ' + (cursorBlinking.current ? 'blink' : '')}
> >
<ReactCodeMirror <ReactCodeMirror
value={editorCode} value={codeManager.code}
extensions={editorExtensions} extensions={editorExtensions}
onUpdate={onUpdate}
theme={theme} theme={theme}
onCreateEditor={(_editorView) => setEditorView(_editorView)} onCreateEditor={(_editorView) =>
editorManager.setEditorView(_editorView)
}
indentWithTab={false} indentWithTab={false}
basicSetup={false} basicSetup={false}
/> />
</div> </div>
) )
} }
function stringifyRanges(ranges: readonly SelectionRange[]): string {
return ranges.map(({ to, from }) => `${to}->${from}`).join('&')
}

View File

@ -2,7 +2,9 @@ import { processMemory } from './MemoryPane'
import { enginelessExecutor } from '../../../lib/testHelpers' import { enginelessExecutor } from '../../../lib/testHelpers'
import { initPromise, parse } from '../../../lang/wasm' import { initPromise, parse } from '../../../lang/wasm'
beforeAll(() => initPromise) beforeAll(async () => {
await initPromise
})
describe('processMemory', () => { describe('processMemory', () => {
it('should grab the values and remove and geo data', async () => { it('should grab the values and remove and geo data', async () => {
@ -26,7 +28,7 @@ describe('processMemory', () => {
|> lineTo([0.98, 5.16], %) |> lineTo([0.98, 5.16], %)
|> lineTo([2.15, 4.32], %) |> lineTo([2.15, 4.32], %)
// |> rx(90, %)` // |> rx(90, %)`
const ast = await parse(code) const ast = parse(code)
const programMemory = await enginelessExecutor(ast, { const programMemory = await enginelessExecutor(ast, {
root: {}, root: {},
return: null, return: null,

238
src/editor/manager.ts Normal file
View File

@ -0,0 +1,238 @@
import { hasNextSnippetField } from '@codemirror/autocomplete'
import { EditorView, ViewUpdate } from '@codemirror/view'
import { EditorSelection, SelectionRange } from '@codemirror/state'
import { engineCommandManager, sceneInfra } from 'lib/singletons'
import { ModelingMachineEvent } from 'machines/modelingMachine'
import { Selections, processCodeMirrorRanges, Selection } from 'lib/selections'
import { undo, redo } from '@codemirror/commands'
import { CommandBarMachineEvent } from 'machines/commandBarMachine'
import { addLineHighlight } from './highlightextension'
import { setDiagnostics, Diagnostic } from '@codemirror/lint'
export default class EditorManager {
private _editorView: EditorView | null = null
private _isShiftDown: boolean = false
private _selectionRanges: Selections = {
otherSelections: [],
codeBasedSelections: [],
}
private _lastSelectionEvent: number | null = null
private _lastSelection: string = ''
private _lastEvent: { event: string; time: number } | null = null
private _modelingSend: (eventInfo: ModelingMachineEvent) => void = () => {}
private _modelingEvent: ModelingMachineEvent | null = null
private _commandBarSend: (eventInfo: CommandBarMachineEvent) => void =
() => {}
private _convertToVariableEnabled: boolean = false
private _convertToVariableCallback: () => void = () => {}
private _highlightRange: [number, number] = [0, 0]
setEditorView(editorView: EditorView) {
this._editorView = editorView
}
get editorView(): EditorView | null {
return this._editorView
}
get isShiftDown(): boolean {
return this._isShiftDown
}
setIsShiftDown(isShiftDown: boolean) {
this._isShiftDown = isShiftDown
}
set selectionRanges(selectionRanges: Selections) {
this._selectionRanges = selectionRanges
}
set lastSelectionEvent(time: number) {
this._lastSelectionEvent = time
}
set modelingSend(send: (eventInfo: ModelingMachineEvent) => void) {
this._modelingSend = send
}
set modelingEvent(event: ModelingMachineEvent) {
this._modelingEvent = event
}
setCommandBarSend(send: (eventInfo: CommandBarMachineEvent) => void) {
this._commandBarSend = send
}
commandBarSend(eventInfo: CommandBarMachineEvent): void {
return this._commandBarSend(eventInfo)
}
get highlightRange(): [number, number] {
return this._highlightRange
}
setHighlightRange(selection: Selection['range']): void {
this._highlightRange = selection
const editorView = this.editorView
const safeEnd = Math.min(
selection[1],
editorView?.state.doc.length || selection[1]
)
if (editorView) {
editorView.dispatch({
effects: addLineHighlight.of([selection[0], safeEnd]),
})
}
}
setDiagnostics(diagnostics: Diagnostic[]): void {
if (!this.editorView) return
this.editorView.dispatch(setDiagnostics(this.editorView.state, diagnostics))
}
undo() {
if (this._editorView) {
undo(this._editorView)
}
}
redo() {
if (this._editorView) {
redo(this._editorView)
}
}
set convertToVariableEnabled(enabled: boolean) {
this._convertToVariableEnabled = enabled
}
set convertToVariableCallback(callback: () => void) {
this._convertToVariableCallback = callback
}
convertToVariable() {
if (this._convertToVariableEnabled) {
this._convertToVariableCallback()
return true
}
return false
}
selectRange(selections: Selections) {
if (selections.codeBasedSelections.length === 0) {
return
}
if (!this.editorView) {
return
}
let codeBasedSelections = []
for (const selection of selections.codeBasedSelections) {
codeBasedSelections.push(
EditorSelection.range(selection.range[0], selection.range[1])
)
}
codeBasedSelections.push(
EditorSelection.cursor(
selections.codeBasedSelections[
selections.codeBasedSelections.length - 1
].range[1]
)
)
this.editorView.dispatch({
selection: EditorSelection.create(codeBasedSelections, 1),
})
}
handleOnViewUpdate(viewUpdate: ViewUpdate): void {
// If we are just fucking around in a snippet, return early and don't
// trigger stuff below that might cause the component to re-render.
// Otherwise we will not be able to tab thru the snippet portions.
// We explicitly dont check HasPrevSnippetField because we always add
// a ${} to the end of the function so that's fine.
if (hasNextSnippetField(viewUpdate.view.state)) {
return
}
if (this.editorView === null) {
this.setEditorView(viewUpdate.view)
}
const selString = stringifyRanges(
viewUpdate?.state?.selection?.ranges || []
)
if (selString === this._lastSelection) {
// 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
}
this._lastSelection = selString
if (
this._lastSelectionEvent &&
Date.now() - this._lastSelectionEvent < 150
) {
return // update triggered by scene selection
}
if (sceneInfra.selected) {
return // mid drag
}
const ignoreEvents: ModelingMachineEvent['type'][] = [
'Equip Line tool',
'Equip tangential arc to',
]
if (!this._modelingEvent) {
return
}
if (ignoreEvents.includes(this._modelingEvent.type)) {
return
}
const eventInfo = processCodeMirrorRanges({
codeMirrorRanges: viewUpdate.state.selection.ranges,
selectionRanges: this._selectionRanges,
isShiftDown: this._isShiftDown,
})
if (!eventInfo) {
return
}
const deterministicEventInfo = {
...eventInfo,
engineEvents: eventInfo.engineEvents.map((e) => ({
...e,
cmd_id: 'static',
})),
}
const stringEvent = JSON.stringify(deterministicEventInfo)
if (
this._lastEvent &&
stringEvent === this._lastEvent.event &&
Date.now() - this._lastEvent.time < 500
) {
return // don't repeat events
}
this._lastEvent = { event: stringEvent, time: Date.now() }
this._modelingSend(eventInfo.modelingEvent)
eventInfo.engineEvents.forEach((event) =>
engineCommandManager.sendSceneCommand(event)
)
}
}
function stringifyRanges(ranges: readonly SelectionRange[]): string {
return ranges.map(({ to, from }) => `${to}->${from}`).join('&')
}

View File

@ -21,7 +21,7 @@ import { LanguageServerClient } from 'editor/plugins/lsp'
import { Marked } from '@ts-stack/markdown' import { Marked } from '@ts-stack/markdown'
import { posToOffset } from 'editor/plugins/lsp/util' import { posToOffset } from 'editor/plugins/lsp/util'
import { Program, ProgramMemory } from 'lang/wasm' import { Program, ProgramMemory } from 'lang/wasm'
import { codeManager, kclManager } from 'lib/singletons' import { codeManager, editorManager, kclManager } from 'lib/singletons'
import type { UnitLength } from 'wasm-lib/kcl/bindings/UnitLength' import type { UnitLength } from 'wasm-lib/kcl/bindings/UnitLength'
import { UpdateUnitsResponse } from 'wasm-lib/kcl/bindings/UpdateUnitsResponse' import { UpdateUnitsResponse } from 'wasm-lib/kcl/bindings/UpdateUnitsResponse'
import { UpdateCanExecuteResponse } from 'wasm-lib/kcl/bindings/UpdateCanExecuteResponse' import { UpdateCanExecuteResponse } from 'wasm-lib/kcl/bindings/UpdateCanExecuteResponse'
@ -39,6 +39,8 @@ const CompletionItemKindMap = Object.fromEntries(
) as Record<CompletionItemKind, string> ) as Record<CompletionItemKind, string>
const changesDelay = 600 const changesDelay = 600
let debounceTimer: ReturnType<typeof setTimeout> | null = null
const updateDelay = 100
export class LanguageServerPlugin implements PluginValue { export class LanguageServerPlugin implements PluginValue {
public client: LanguageServerClient public client: LanguageServerClient
@ -47,6 +49,7 @@ export class LanguageServerPlugin implements PluginValue {
public workspaceFolders: LSP.WorkspaceFolder[] public workspaceFolders: LSP.WorkspaceFolder[]
private documentVersion: number private documentVersion: number
private foldingRanges: LSP.FoldingRange[] | null = null private foldingRanges: LSP.FoldingRange[] | null = null
private viewUpdate: ViewUpdate | null = null
private _defferer = deferExecution((code: string) => { private _defferer = deferExecution((code: string) => {
try { try {
// Update the state (not the editor) with the new code. // Update the state (not the editor) with the new code.
@ -57,6 +60,10 @@ export class LanguageServerPlugin implements PluginValue {
}, },
contentChanges: [{ text: code }], contentChanges: [{ text: code }],
}) })
if (this.viewUpdate) {
editorManager.handleOnViewUpdate(this.viewUpdate)
}
} catch (e) { } catch (e) {
console.error(e) console.error(e)
} }
@ -80,14 +87,27 @@ export class LanguageServerPlugin implements PluginValue {
}) })
} }
update({ docChanged }: ViewUpdate) { update(viewUpdate: ViewUpdate) {
if (!docChanged) return this.viewUpdate = viewUpdate
if (!viewUpdate.docChanged) {
// debounce the view update.
// otherwise it is laggy for typing.
if (debounceTimer) {
clearTimeout(debounceTimer)
}
debounceTimer = setTimeout(() => {
editorManager.handleOnViewUpdate(viewUpdate)
}, updateDelay)
return
}
const newCode = this.view.state.doc.toString() const newCode = this.view.state.doc.toString()
codeManager.code = newCode codeManager.code = newCode
codeManager.writeToFile() codeManager.writeToFile()
kclManager.executeCode() kclManager.executeCode()
this.sendChange({ this.sendChange({
documentText: newCode, documentText: newCode,
}) })
@ -357,15 +377,9 @@ export class LanguageServerPlugin implements PluginValue {
try { try {
switch (notification.method) { switch (notification.method) {
case 'textDocument/publishDiagnostics': case 'textDocument/publishDiagnostics':
const params = notification.params as PublishDiagnosticsParams //const params = notification.params as PublishDiagnosticsParams
this.processDiagnostics(params) // this is sometimes slower than our actual typing.
// Update the kcl errors pane. //this.processDiagnostics(params)
/*if (!kclManager.isExecuting) {
kclManager.kclErrors = lspDiagnosticsToKclErrors(
this.view.state.doc,
params.diagnostics
)
}*/
break break
case 'window/logMessage': case 'window/logMessage':
console.log( console.log(
@ -385,17 +399,6 @@ export class LanguageServerPlugin implements PluginValue {
// The server has updated the AST, we should update elsewhere. // The server has updated the AST, we should update elsewhere.
let updatedAst = notification.params as Program let updatedAst = notification.params as Program
console.log('[lsp]: Updated AST', updatedAst) console.log('[lsp]: Updated AST', updatedAst)
// Update the ast when we are not already executing.
/* if (!kclManager.isExecuting) {
kclManager.ast = updatedAst
// Execute the ast.
console.log('[lsp]: executing ast')
await kclManager.executeAst(updatedAst)
console.log('[lsp]: executed ast', kclManager.kclErrors)
let diagnostics = kclErrorsToDiagnostics(kclManager.kclErrors)
this.view.dispatch(setDiagnostics(this.view.state, diagnostics))
console.log('[lsp]: updated diagnostics')
}*/
// Update the folding ranges, since the AST has changed. // Update the folding ranges, since the AST has changed.
// This is a hack since codemirror does not support async foldService. // This is a hack since codemirror does not support async foldService.

View File

@ -1,14 +1,9 @@
import { useEffect } from 'react' import { useEffect } from 'react'
import { useStore } from 'useStore' import { editorManager, engineCommandManager } from 'lib/singletons'
import { engineCommandManager } from 'lib/singletons'
import { useModelingContext } from './useModelingContext' import { useModelingContext } from './useModelingContext'
import { getEventForSelectWithPoint } from 'lib/selections' import { getEventForSelectWithPoint } from 'lib/selections'
export function useEngineConnectionSubscriptions() { export function useEngineConnectionSubscriptions() {
const { setHighlightRange, highlightRange } = useStore((s) => ({
setHighlightRange: s.setHighlightRange,
highlightRange: s.highlightRange,
}))
const { send, context } = useModelingContext() const { send, context } = useModelingContext()
useEffect(() => { useEffect(() => {
@ -21,12 +16,13 @@ export function useEngineConnectionSubscriptions() {
if (data?.entity_id) { if (data?.entity_id) {
const sourceRange = const sourceRange =
engineCommandManager.artifactMap?.[data.entity_id]?.range engineCommandManager.artifactMap?.[data.entity_id]?.range
setHighlightRange(sourceRange) editorManager.setHighlightRange(sourceRange)
} else if ( } else if (
!highlightRange || !editorManager.highlightRange ||
(highlightRange[0] !== 0 && highlightRange[1] !== 0) (editorManager.highlightRange[0] !== 0 &&
editorManager.highlightRange[1] !== 0)
) { ) {
setHighlightRange([0, 0]) editorManager.setHighlightRange([0, 0])
} }
}, },
}) })
@ -43,10 +39,5 @@ export function useEngineConnectionSubscriptions() {
unSubHover() unSubHover()
unSubClick() unSubClick()
} }
}, [ }, [engineCommandManager, context?.sketchEnginePathId])
engineCommandManager,
setHighlightRange,
highlightRange,
context?.sketchEnginePathId,
])
} }

View File

@ -1,4 +1,4 @@
import { useStore } from '../useStore' import { editorManager } from 'lib/singletons'
import { useEffect } from 'react' import { useEffect } from 'react'
// Kurt's note: codeMirror styling overrides were needed to make this work // Kurt's note: codeMirror styling overrides were needed to make this work
@ -6,20 +6,17 @@ import { useEffect } from 'react'
// search for code-mirror-override in the repo to find the relevant styles // search for code-mirror-override in the repo to find the relevant styles
export function useHotKeyListener() { export function useHotKeyListener() {
const { setIsShiftDown } = useStore((s) => ({
setIsShiftDown: s.setIsShiftDown,
}))
const keyName = 'Shift' const keyName = 'Shift'
useEffect(() => { useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => const handleKeyDown = (event: KeyboardEvent) =>
event.key === keyName && setIsShiftDown(true) event.key === keyName && editorManager.setIsShiftDown(true)
const handleKeyUp = (event: KeyboardEvent) => const handleKeyUp = (event: KeyboardEvent) =>
event.key === keyName && setIsShiftDown(false) event.key === keyName && editorManager.setIsShiftDown(false)
window.addEventListener('keydown', handleKeyDown) window.addEventListener('keydown', handleKeyDown)
window.addEventListener('keyup', handleKeyUp) window.addEventListener('keyup', handleKeyUp)
return () => { return () => {
window.removeEventListener('keydown', handleKeyDown) window.removeEventListener('keydown', handleKeyDown)
window.removeEventListener('keyup', handleKeyUp) window.removeEventListener('keyup', handleKeyUp)
} }
}, [setIsShiftDown]) })
} }

View File

@ -2,7 +2,7 @@ import {
SetVarNameModal, SetVarNameModal,
createSetVarNameModal, createSetVarNameModal,
} from 'components/SetVarNameModal' } from 'components/SetVarNameModal'
import { kclManager } from 'lib/singletons' import { editorManager, kclManager } from 'lib/singletons'
import { moveValueIntoNewVariable } from 'lang/modifyAst' import { moveValueIntoNewVariable } from 'lang/modifyAst'
import { isNodeSafeToReplace } from 'lang/queryAst' import { isNodeSafeToReplace } from 'lang/queryAst'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
@ -13,6 +13,11 @@ const getModalInfo = createSetVarNameModal(SetVarNameModal)
export function useConvertToVariable() { export function useConvertToVariable() {
const { context } = useModelingContext() const { context } = useModelingContext()
const [enable, setEnabled] = useState(false) const [enable, setEnabled] = useState(false)
useEffect(() => {
editorManager.convertToVariableEnabled = enable
}, [enable])
useEffect(() => { useEffect(() => {
const { isSafe, value } = isNodeSafeToReplace( const { isSafe, value } = isNodeSafeToReplace(
kclManager.ast, kclManager.ast,
@ -45,5 +50,7 @@ export function useConvertToVariable() {
} }
} }
editorManager.convertToVariableCallback = handleClick
return { enable, handleClick } return { enable, handleClick }
} }

View File

@ -2,12 +2,10 @@ import { KCLError } from './errors'
import { createContext, useContext, useEffect, useState } from 'react' import { createContext, useContext, useEffect, useState } from 'react'
import { type IndexLoaderData } from 'lib/types' import { type IndexLoaderData } from 'lib/types'
import { useLoaderData } from 'react-router-dom' import { useLoaderData } from 'react-router-dom'
import { useParams } from 'react-router-dom'
import { codeManager, kclManager } from 'lib/singletons' import { codeManager, kclManager } from 'lib/singletons'
const KclContext = createContext({ const KclContext = createContext({
code: codeManager?.code || '', code: codeManager?.code || '',
editorCode: codeManager?.code || '',
programMemory: kclManager?.programMemory, programMemory: kclManager?.programMemory,
ast: kclManager?.ast, ast: kclManager?.ast,
isExecuting: kclManager?.isExecuting, isExecuting: kclManager?.isExecuting,
@ -30,7 +28,6 @@ export function KclContextProvider({
const { code: loadedCode } = useLoaderData() as IndexLoaderData const { code: loadedCode } = useLoaderData() as IndexLoaderData
// Both the code state and the editor state start off with the same code. // Both the code state and the editor state start off with the same code.
const [code, setCode] = useState(loadedCode || codeManager.code) const [code, setCode] = useState(loadedCode || codeManager.code)
const [editorCode, setEditorCode] = useState(code)
const [programMemory, setProgramMemory] = useState(kclManager.programMemory) const [programMemory, setProgramMemory] = useState(kclManager.programMemory)
const [ast, setAst] = useState(kclManager.ast) const [ast, setAst] = useState(kclManager.ast)
@ -42,7 +39,6 @@ export function KclContextProvider({
useEffect(() => { useEffect(() => {
codeManager.registerCallBacks({ codeManager.registerCallBacks({
setCode, setCode,
setEditorCode,
}) })
kclManager.registerCallBacks({ kclManager.registerCallBacks({
setProgramMemory, setProgramMemory,
@ -54,15 +50,10 @@ export function KclContextProvider({
}) })
}, []) }, [])
const params = useParams()
useEffect(() => {
codeManager.setParams(params)
}, [params])
return ( return (
<KclContext.Provider <KclContext.Provider
value={{ value={{
code, code,
editorCode,
programMemory, programMemory,
ast, ast,
isExecuting, isExecuting,

View File

@ -1,6 +1,6 @@
import { executeAst } from 'useStore' import { executeAst } from 'useStore'
import { Selections } from 'lib/selections' import { Selections } from 'lib/selections'
import { KCLError } from './errors' import { KCLError, kclErrorsToDiagnostics } from './errors'
import { uuidv4 } from 'lib/utils' import { uuidv4 } from 'lib/utils'
import { EngineCommandManager } from './std/engineConnection' import { EngineCommandManager } from './std/engineConnection'
@ -17,7 +17,7 @@ import {
ExtrudeGroup, ExtrudeGroup,
} from 'lang/wasm' } from 'lang/wasm'
import { getNodeFromPath } from './queryAst' import { getNodeFromPath } from './queryAst'
import { codeManager } from 'lib/singletons' import { codeManager, editorManager } from 'lib/singletons'
export class KclManager { export class KclManager {
private _ast: Program = { private _ast: Program = {
@ -43,16 +43,14 @@ export class KclManager {
const ast = this.safeParse(code) const ast = this.safeParse(code)
if (!ast) return if (!ast) return
try { try {
parse(recast(ast)).then((newAst) => {
const fmtAndStringify = (ast: Program) => const fmtAndStringify = (ast: Program) =>
JSON.stringify(newAst) JSON.stringify(parse(recast(ast)))
const isAstTheSame = fmtAndStringify(ast) === fmtAndStringify(this._ast) const isAstTheSame = fmtAndStringify(ast) === fmtAndStringify(this._ast)
if (isAstTheSame) return if (isAstTheSame) return
this.executeAst(ast)
})
} catch (e) { } catch (e) {
console.error(e) console.error(e)
} }
this.executeAst(ast)
}, 600) }, 600)
private _isExecutingCallback: (arg: boolean) => void = () => {} private _isExecutingCallback: (arg: boolean) => void = () => {}
@ -92,6 +90,8 @@ export class KclManager {
} }
set kclErrors(kclErrors) { set kclErrors(kclErrors) {
this._kclErrors = kclErrors this._kclErrors = kclErrors
let diagnostics = kclErrorsToDiagnostics(kclErrors)
editorManager.setDiagnostics(diagnostics)
this._kclErrorsCallBack(kclErrors) this._kclErrorsCallBack(kclErrors)
} }
@ -145,9 +145,9 @@ export class KclManager {
this._executeCallback = callback this._executeCallback = callback
} }
async safeParse(code: string): Promise<Program | null> { safeParse(code: string): Program | null {
try { try {
const ast = await parse(code) const ast = parse(code)
this.kclErrors = [] this.kclErrors = []
return ast return ast
} catch (e) { } catch (e) {
@ -347,6 +347,16 @@ export class KclManager {
void this.engineCommandManager.setPlaneHidden(this.defaultPlanes.yz, true) void this.engineCommandManager.setPlaneHidden(this.defaultPlanes.yz, true)
void this.engineCommandManager.setPlaneHidden(this.defaultPlanes.xz, true) void this.engineCommandManager.setPlaneHidden(this.defaultPlanes.xz, true)
} }
enterEditMode() {
enterEditMode(this.programMemory, this.engineCommandManager)
}
exitEditMode() {
this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: { type: 'edit_mode_exit' },
})
}
} }
function enterEditMode( function enterEditMode(

View File

@ -1,11 +1,13 @@
import { KCLError } from './errors' import { KCLError } from './errors'
import { initPromise, parse } from './wasm' import { initPromise, parse } from './wasm'
beforeAll(() => initPromise) beforeAll(async () => {
await initPromise
})
describe('testing AST', () => { describe('testing AST', () => {
test('5 + 6', async () => { test('5 + 6', () => {
const result = await parse('5 +6') const result = parse('5 +6')
delete (result as any).nonCodeMeta delete (result as any).nonCodeMeta
expect(result.body).toEqual([ expect(result.body).toEqual([
{ {
@ -35,8 +37,8 @@ describe('testing AST', () => {
}, },
]) ])
}) })
test('const myVar = 5', async () => { test('const myVar = 5', () => {
const { body } = await parse('const myVar = 5') const { body } = parse('const myVar = 5')
expect(body).toEqual([ expect(body).toEqual([
{ {
type: 'VariableDeclaration', type: 'VariableDeclaration',
@ -66,11 +68,11 @@ describe('testing AST', () => {
}, },
]) ])
}) })
test('multi-line', async () => { test('multi-line', () => {
const code = `const myVar = 5 const code = `const myVar = 5
const newVar = myVar + 1 const newVar = myVar + 1
` `
const { body } = await parse(code) const { body } = parse(code)
expect(body).toEqual([ expect(body).toEqual([
{ {
type: 'VariableDeclaration', type: 'VariableDeclaration',
@ -141,8 +143,8 @@ const newVar = myVar + 1
}) })
describe('testing function declaration', () => { describe('testing function declaration', () => {
test('fn funcN = (a, b) => {return a + b}', async () => { test('fn funcN = (a, b) => {return a + b}', () => {
const { body } = await parse( const { body } = parse(
['fn funcN = (a, b) => {', ' return a + b', '}'].join('\n') ['fn funcN = (a, b) => {', ' return a + b', '}'].join('\n')
) )
delete (body[0] as any).declarations[0].init.body.nonCodeMeta delete (body[0] as any).declarations[0].init.body.nonCodeMeta
@ -224,10 +226,10 @@ describe('testing function declaration', () => {
}, },
]) ])
}) })
test('call expression assignment', async () => { test('call expression assignment', () => {
const code = `fn funcN = (a, b) => { return a + b } const code = `fn funcN = (a, b) => { return a + b }
const myVar = funcN(1, 2)` const myVar = funcN(1, 2)`
const { body } = await parse(code) const { body } = parse(code)
delete (body[0] as any).declarations[0].init.body.nonCodeMeta delete (body[0] as any).declarations[0].init.body.nonCodeMeta
expect(body).toEqual([ expect(body).toEqual([
{ {
@ -357,14 +359,14 @@ const myVar = funcN(1, 2)`
}) })
describe('testing pipe operator special', () => { describe('testing pipe operator special', () => {
test('pipe operator with sketch', async () => { test('pipe operator with sketch', () => {
let code = `const mySketch = startSketchAt([0, 0]) let code = `const mySketch = startSketchAt([0, 0])
|> lineTo([2, 3], %) |> lineTo([2, 3], %)
|> lineTo([0, 1], %, "myPath") |> lineTo([0, 1], %, "myPath")
|> lineTo([1, 1], %) |> lineTo([1, 1], %)
|> rx(45, %) |> rx(45, %)
` `
const { body } = await parse(code) const { body } = parse(code)
delete (body[0] as any).declarations[0].init.nonCodeMeta delete (body[0] as any).declarations[0].init.nonCodeMeta
expect(body).toEqual([ expect(body).toEqual([
{ {
@ -562,9 +564,9 @@ describe('testing pipe operator special', () => {
}, },
]) ])
}) })
test('pipe operator with binary expression', async () => { test('pipe operator with binary expression', () => {
let code = `const myVar = 5 + 6 |> myFunc(45, %)` let code = `const myVar = 5 + 6 |> myFunc(45, %)`
const { body } = await parse(code) const { body } = parse(code)
delete (body as any)[0].declarations[0].init.nonCodeMeta delete (body as any)[0].declarations[0].init.nonCodeMeta
expect(body).toEqual([ expect(body).toEqual([
{ {
@ -641,9 +643,9 @@ describe('testing pipe operator special', () => {
}, },
]) ])
}) })
test('array expression', async () => { test('array expression', () => {
let code = `const yo = [1, '2', three, 4 + 5]` let code = `const yo = [1, '2', three, 4 + 5]`
const { body } = await parse(code) const { body } = parse(code)
expect(body).toEqual([ expect(body).toEqual([
{ {
type: 'VariableDeclaration', type: 'VariableDeclaration',
@ -713,12 +715,12 @@ describe('testing pipe operator special', () => {
}, },
]) ])
}) })
test('object expression ast', async () => { test('object expression ast', () => {
const code = [ const code = [
'const three = 3', 'const three = 3',
"const yo = {aStr: 'str', anum: 2, identifier: three, binExp: 4 + 5}", "const yo = {aStr: 'str', anum: 2, identifier: three, binExp: 4 + 5}",
].join('\n') ].join('\n')
const { body } = await parse(code) const { body } = parse(code)
expect(body).toEqual([ expect(body).toEqual([
{ {
type: 'VariableDeclaration', type: 'VariableDeclaration',
@ -858,11 +860,11 @@ describe('testing pipe operator special', () => {
}, },
]) ])
}) })
test('nested object expression ast', async () => { test('nested object expression ast', () => {
const code = `const yo = {key: { const code = `const yo = {key: {
key2: 'value' key2: 'value'
}}` }}`
const { body } = await parse(code) const { body } = parse(code)
expect(body).toEqual([ expect(body).toEqual([
{ {
type: 'VariableDeclaration', type: 'VariableDeclaration',
@ -928,9 +930,9 @@ describe('testing pipe operator special', () => {
}, },
]) ])
}) })
test('object expression with array ast', async () => { test('object expression with array ast', () => {
const code = `const yo = {key: [1, '2']}` const code = `const yo = {key: [1, '2']}`
const { body } = await parse(code) const { body } = parse(code)
expect(body).toEqual([ expect(body).toEqual([
{ {
type: 'VariableDeclaration', type: 'VariableDeclaration',
@ -992,9 +994,9 @@ describe('testing pipe operator special', () => {
}, },
]) ])
}) })
test('object memberExpression simple', async () => { test('object memberExpression simple', () => {
const code = `const prop = yo.one.two` const code = `const prop = yo.one.two`
const { body } = await parse(code) const { body } = parse(code)
expect(body).toEqual([ expect(body).toEqual([
{ {
type: 'VariableDeclaration', type: 'VariableDeclaration',
@ -1047,9 +1049,9 @@ describe('testing pipe operator special', () => {
}, },
]) ])
}) })
test('object memberExpression with square braces', async () => { test('object memberExpression with square braces', () => {
const code = `const prop = yo.one["two"]` const code = `const prop = yo.one["two"]`
const { body } = await parse(code) const { body } = parse(code)
expect(body).toEqual([ expect(body).toEqual([
{ {
type: 'VariableDeclaration', type: 'VariableDeclaration',
@ -1103,9 +1105,9 @@ describe('testing pipe operator special', () => {
}, },
]) ])
}) })
test('object memberExpression with two square braces literal and identifier', async () => { test('object memberExpression with two square braces literal and identifier', () => {
const code = `const prop = yo["one"][two]` const code = `const prop = yo["one"][two]`
const { body } = await parse(code) const { body } = parse(code)
expect(body).toEqual([ expect(body).toEqual([
{ {
type: 'VariableDeclaration', type: 'VariableDeclaration',
@ -1162,9 +1164,9 @@ describe('testing pipe operator special', () => {
}) })
describe('nests binary expressions correctly', () => { describe('nests binary expressions correctly', () => {
it('works with the simple case', async () => { it('works with the simple case', () => {
const code = `const yo = 1 + 2` const code = `const yo = 1 + 2`
const { body } = await parse(code) const { body } = parse(code)
expect(body[0]).toEqual({ expect(body[0]).toEqual({
type: 'VariableDeclaration', type: 'VariableDeclaration',
start: 0, start: 0,
@ -1205,10 +1207,10 @@ describe('nests binary expressions correctly', () => {
], ],
}) })
}) })
it('should nest according to precedence with multiply first', async () => { it('should nest according to precedence with multiply first', () => {
// should be binExp { binExp { lit-1 * lit-2 } + lit} // should be binExp { binExp { lit-1 * lit-2 } + lit}
const code = `const yo = 1 * 2 + 3` const code = `const yo = 1 * 2 + 3`
const { body } = await parse(code) const { body } = parse(code)
expect(body[0]).toEqual({ expect(body[0]).toEqual({
type: 'VariableDeclaration', type: 'VariableDeclaration',
start: 0, start: 0,
@ -1262,10 +1264,10 @@ describe('nests binary expressions correctly', () => {
], ],
}) })
}) })
it('should nest according to precedence with sum first', async () => { it('should nest according to precedence with sum first', () => {
// should be binExp { lit-1 + binExp { lit-2 * lit-3 } } // should be binExp { lit-1 + binExp { lit-2 * lit-3 } }
const code = `const yo = 1 + 2 * 3` const code = `const yo = 1 + 2 * 3`
const { body } = await parse(code) const { body } = parse(code)
expect(body[0]).toEqual({ expect(body[0]).toEqual({
type: 'VariableDeclaration', type: 'VariableDeclaration',
start: 0, start: 0,
@ -1319,9 +1321,9 @@ describe('nests binary expressions correctly', () => {
], ],
}) })
}) })
it('should nest properly with two operators of equal precedence', async () => { it('should nest properly with two operators of equal precedence', () => {
const code = `const yo = 1 + 2 - 3` const code = `const yo = 1 + 2 - 3`
const { body } = await parse(code) const { body } = parse(code)
expect((body[0] as any).declarations[0].init).toEqual({ expect((body[0] as any).declarations[0].init).toEqual({
type: 'BinaryExpression', type: 'BinaryExpression',
start: 11, start: 11,
@ -1356,9 +1358,9 @@ describe('nests binary expressions correctly', () => {
}, },
}) })
}) })
it('should nest properly with two operators of equal (but higher) precedence', async () => { it('should nest properly with two operators of equal (but higher) precedence', () => {
const code = `const yo = 1 * 2 / 3` const code = `const yo = 1 * 2 / 3`
const { body } = await parse(code) const { body } = parse(code)
expect((body[0] as any).declarations[0].init).toEqual({ expect((body[0] as any).declarations[0].init).toEqual({
type: 'BinaryExpression', type: 'BinaryExpression',
start: 11, start: 11,
@ -1393,9 +1395,9 @@ describe('nests binary expressions correctly', () => {
}, },
}) })
}) })
it('should nest properly with longer example', async () => { it('should nest properly with longer example', () => {
const code = `const yo = 1 + 2 * (3 - 4) / 5 + 6` const code = `const yo = 1 + 2 * (3 - 4) / 5 + 6`
const { body } = await parse(code) const { body } = parse(code)
const init = (body[0] as any).declarations[0].init const init = (body[0] as any).declarations[0].init
expect(init).toEqual({ expect(init).toEqual({
type: 'BinaryExpression', type: 'BinaryExpression',
@ -1443,7 +1445,7 @@ describe('nests binary expressions correctly', () => {
}) })
describe('check nonCodeMeta data is attached to the AST correctly', () => { describe('check nonCodeMeta data is attached to the AST correctly', () => {
it('comments between expressions', async () => { it('comments between expressions', () => {
const code = ` const code = `
const yo = { a: { b: { c: '123' } } } const yo = { a: { b: { c: '123' } } }
// this is a comment // this is a comment
@ -1458,14 +1460,12 @@ const key = 'c'`
value: 'this is a comment', value: 'this is a comment',
}, },
} }
const { nonCodeMeta } = await parse(code) const { nonCodeMeta } = parse(code)
expect(nonCodeMeta.nonCodeNodes[0][0]).toEqual(nonCodeMetaInstance) expect(nonCodeMeta.nonCodeNodes[0][0]).toEqual(nonCodeMetaInstance)
// extra whitespace won't change it's position (0) or value (NB the start end would have changed though) // extra whitespace won't change it's position (0) or value (NB the start end would have changed though)
const codeWithExtraStartWhitespace = '\n\n\n' + code const codeWithExtraStartWhitespace = '\n\n\n' + code
const { nonCodeMeta: nonCodeMeta2 } = await parse( const { nonCodeMeta: nonCodeMeta2 } = parse(codeWithExtraStartWhitespace)
codeWithExtraStartWhitespace
)
expect(nonCodeMeta2.nonCodeNodes[0][0].value).toStrictEqual( expect(nonCodeMeta2.nonCodeNodes[0][0].value).toStrictEqual(
nonCodeMetaInstance.value nonCodeMetaInstance.value
) )
@ -1473,7 +1473,7 @@ const key = 'c'`
nonCodeMetaInstance.start nonCodeMetaInstance.start
) )
}) })
it('comments nested within a block statement', async () => { it('comments nested within a block statement', () => {
const code = `const mySketch = startSketchAt([0,0]) const code = `const mySketch = startSketchAt([0,0])
|> lineTo([0, 1], %, 'myPath') |> lineTo([0, 1], %, 'myPath')
|> lineTo([1, 1], %) /* this is |> lineTo([1, 1], %) /* this is
@ -1483,7 +1483,7 @@ const key = 'c'`
|> close(%) |> close(%)
` `
const { body } = await parse(code) const { body } = parse(code)
const indexOfSecondLineToExpression = 2 const indexOfSecondLineToExpression = 2
const sketchNonCodeMeta = (body as any)[0].declarations[0].init.nonCodeMeta const sketchNonCodeMeta = (body as any)[0].declarations[0].init.nonCodeMeta
.nonCodeNodes .nonCodeNodes
@ -1498,7 +1498,7 @@ const key = 'c'`
}, },
}) })
}) })
it('comments in a pipe expression', async () => { it('comments in a pipe expression', () => {
const code = [ const code = [
'const mySk1 = startSketchAt([0, 0])', 'const mySk1 = startSketchAt([0, 0])',
' |> lineTo([1, 1], %)', ' |> lineTo([1, 1], %)',
@ -1508,7 +1508,7 @@ const key = 'c'`
' |> rx(90, %)', ' |> rx(90, %)',
].join('\n') ].join('\n')
const { body } = await parse(code) const { body } = parse(code)
const sketchNonCodeMeta = (body[0] as any).declarations[0].init.nonCodeMeta const sketchNonCodeMeta = (body[0] as any).declarations[0].init.nonCodeMeta
.nonCodeNodes[3][0] .nonCodeNodes[3][0]
expect(sketchNonCodeMeta).toEqual({ expect(sketchNonCodeMeta).toEqual({
@ -1525,9 +1525,9 @@ const key = 'c'`
}) })
describe('test UnaryExpression', () => { describe('test UnaryExpression', () => {
it('should parse a unary expression in simple var dec situation', async () => { it('should parse a unary expression in simple var dec situation', () => {
const code = `const myVar = -min(4, 100)` const code = `const myVar = -min(4, 100)`
const { body } = await parse(code) const { body } = parse(code)
const myVarInit = (body?.[0] as any).declarations[0]?.init const myVarInit = (body?.[0] as any).declarations[0]?.init
expect(myVarInit).toEqual({ expect(myVarInit).toEqual({
type: 'UnaryExpression', type: 'UnaryExpression',
@ -1550,9 +1550,9 @@ describe('test UnaryExpression', () => {
}) })
describe('testing nested call expressions', () => { describe('testing nested call expressions', () => {
it('callExp in a binExp in a callExp', async () => { it('callExp in a binExp in a callExp', () => {
const code = 'const myVar = min(100, 1 + legLen(5, 3))' const code = 'const myVar = min(100, 1 + legLen(5, 3))'
const { body } = await parse(code) const { body } = parse(code)
const myVarInit = (body?.[0] as any).declarations[0]?.init const myVarInit = (body?.[0] as any).declarations[0]?.init
expect(myVarInit).toEqual({ expect(myVarInit).toEqual({
type: 'CallExpression', type: 'CallExpression',
@ -1587,8 +1587,8 @@ describe('testing nested call expressions', () => {
describe('should recognise callExpresions in binaryExpressions', () => { describe('should recognise callExpresions in binaryExpressions', () => {
const code = "xLineTo(segEndX('seg02', %) + 1, %)" const code = "xLineTo(segEndX('seg02', %) + 1, %)"
it('should recognise the callExp', async () => { it('should recognise the callExp', () => {
const { body } = await parse(code) const { body } = parse(code)
const callExpArgs = (body?.[0] as any).expression?.arguments const callExpArgs = (body?.[0] as any).expression?.arguments
expect(callExpArgs).toEqual([ expect(callExpArgs).toEqual([
{ {

View File

@ -1,7 +1,9 @@
import { parse, initPromise } from './wasm' import { parse, initPromise } from './wasm'
import { enginelessExecutor } from '../lib/testHelpers' import { enginelessExecutor } from '../lib/testHelpers'
beforeAll(() => initPromise) beforeAll(async () => {
await initPromise
})
describe('testing artifacts', () => { describe('testing artifacts', () => {
// Enable rotations #152 // Enable rotations #152
@ -12,7 +14,7 @@ const mySketch001 = startSketchOn('XY')
|> lineTo([-1.59, -1.54], %) |> lineTo([-1.59, -1.54], %)
|> lineTo([0.46, -5.82], %) |> lineTo([0.46, -5.82], %)
// |> rx(45, %)` // |> rx(45, %)`
const programMemory = await enginelessExecutor(await parse(code)) const programMemory = await enginelessExecutor(parse(code))
// @ts-ignore // @ts-ignore
const sketch001 = programMemory?.root?.mySketch001 const sketch001 = programMemory?.root?.mySketch001
expect(sketch001).toEqual({ expect(sketch001).toEqual({
@ -68,7 +70,7 @@ const mySketch001 = startSketchOn('XY')
|> lineTo([0.46, -5.82], %) |> lineTo([0.46, -5.82], %)
// |> rx(45, %) // |> rx(45, %)
|> extrude(2, %)` |> extrude(2, %)`
const programMemory = await enginelessExecutor(await parse(code)) const programMemory = await enginelessExecutor(parse(code))
// @ts-ignore // @ts-ignore
const sketch001 = programMemory?.root?.mySketch001 const sketch001 = programMemory?.root?.mySketch001
expect(sketch001).toEqual({ expect(sketch001).toEqual({
@ -150,7 +152,7 @@ const sk2 = startSketchOn('XY')
|> extrude(2, %) |> extrude(2, %)
` `
const programMemory = await enginelessExecutor(await parse(code)) const programMemory = await enginelessExecutor(parse(code))
// @ts-ignore // @ts-ignore
const geos = [programMemory?.root?.theExtrude, programMemory?.root?.sk2] const geos = [programMemory?.root?.theExtrude, programMemory?.root?.sk2]
expect(geos).toEqual([ expect(geos).toEqual([

View File

@ -5,7 +5,7 @@ import { bracket } from 'lib/exampleKcl'
import { isTauri } from 'lib/isTauri' import { isTauri } from 'lib/isTauri'
import { writeTextFile } from '@tauri-apps/plugin-fs' import { writeTextFile } from '@tauri-apps/plugin-fs'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { Params } from 'react-router-dom' import { editorManager } from 'lib/singletons'
const PERSIST_CODE_TOKEN = 'persistCode' const PERSIST_CODE_TOKEN = 'persistCode'
@ -13,7 +13,7 @@ export default class CodeManager {
private _code: string = bracket private _code: string = bracket
private _updateState: (arg: string) => void = () => {} private _updateState: (arg: string) => void = () => {}
private _updateEditor: (arg: string) => void = () => {} private _updateEditor: (arg: string) => void = () => {}
private _params: Params<string> = {} private _currentFilePath: string | null = null
constructor() { constructor() {
if (isTauri()) { if (isTauri()) {
@ -45,19 +45,12 @@ export default class CodeManager {
return this._code return this._code
} }
registerCallBacks({ registerCallBacks({ setCode }: { setCode: (arg: string) => void }) {
setCode,
setEditorCode,
}: {
setCode: (arg: string) => void
setEditorCode: (arg: string) => void
}) {
this._updateState = setCode this._updateState = setCode
this._updateEditor = setEditorCode
} }
setParams(params: Params<string>) { updateCurrentFilePath(path: string) {
this._params = params this._currentFilePath = path
} }
// This updates the code state and calls the updateState function. // This updates the code state and calls the updateState function.
@ -70,11 +63,14 @@ export default class CodeManager {
// Update the code in the editor. // Update the code in the editor.
updateCodeEditor(code: string): void { updateCodeEditor(code: string): void {
if (this._code !== code) { const lastCode = this._code
this.code = code this.code = code
this._updateEditor(code)
}
this._updateEditor(code) this._updateEditor(code)
if (editorManager.editorView) {
editorManager.editorView.dispatch({
changes: { from: 0, to: lastCode.length, insert: code },
})
}
} }
// Update the code, state, and the code the code mirror editor sees. // Update the code, state, and the code the code mirror editor sees.
@ -91,8 +87,8 @@ export default class CodeManager {
setTimeout(() => { setTimeout(() => {
// Wait one event loop to give a chance for params to be set // Wait one event loop to give a chance for params to be set
// Save the file to disk // Save the file to disk
this._params.id && this._currentFilePath &&
writeTextFile(this._params.id, this.code).catch((err) => { writeTextFile(this._currentFilePath, this.code).catch((err) => {
// TODO: add tracing per GH issue #254 (https://github.com/KittyCAD/modeling-app/issues/254) // TODO: add tracing per GH issue #254 (https://github.com/KittyCAD/modeling-app/issues/254)
console.error('error saving file', err) console.error('error saving file', err)
toast.error('Error saving file, please check file permissions') toast.error('Error saving file, please check file permissions')

View File

@ -4,7 +4,9 @@ import { parse, ProgramMemory, SketchGroup, initPromise } from './wasm'
import { enginelessExecutor } from '../lib/testHelpers' import { enginelessExecutor } from '../lib/testHelpers'
import { KCLError } from './errors' import { KCLError } from './errors'
beforeAll(() => initPromise) beforeAll(async () => {
await initPromise
})
describe('test executor', () => { describe('test executor', () => {
it('test assigning two variables, the second summing with the first', async () => { it('test assigning two variables, the second summing with the first', async () => {
@ -398,7 +400,7 @@ async function exe(
code: string, code: string,
programMemory: ProgramMemory = { root: {}, return: null } programMemory: ProgramMemory = { root: {}, return: null }
) { ) {
const ast = await parse(code) const ast = parse(code)
const result = await enginelessExecutor(ast, programMemory) const result = await enginelessExecutor(ast, programMemory)
return result return result

View File

@ -1,10 +1,12 @@
import { getNodePathFromSourceRange, getNodeFromPath } from './queryAst' import { getNodePathFromSourceRange, getNodeFromPath } from './queryAst'
import { Identifier, parse, initPromise, Parameter } from './wasm' import { Identifier, parse, initPromise, Parameter } from './wasm'
beforeAll(() => initPromise) beforeAll(async () => {
await initPromise
})
describe('testing getNodePathFromSourceRange', () => { describe('testing getNodePathFromSourceRange', () => {
it('test it gets the right path for a `lineTo` CallExpression within a SketchExpression', async () => { it('test it gets the right path for a `lineTo` CallExpression within a SketchExpression', () => {
const code = ` const code = `
const myVar = 5 const myVar = 5
const sk3 = startSketchAt([0, 0]) const sk3 = startSketchAt([0, 0])
@ -19,14 +21,14 @@ const sk3 = startSketchAt([0, 0])
lineToSubstringIndex + subStr.length, lineToSubstringIndex + subStr.length,
] ]
const ast = await parse(code) const ast = parse(code)
const nodePath = getNodePathFromSourceRange(ast, sourceRange) const nodePath = getNodePathFromSourceRange(ast, sourceRange)
const { node } = getNodeFromPath<any>(ast, nodePath) const { node } = getNodeFromPath<any>(ast, nodePath)
expect([node.start, node.end]).toEqual(sourceRange) expect([node.start, node.end]).toEqual(sourceRange)
expect(node.type).toBe('CallExpression') expect(node.type).toBe('CallExpression')
}) })
it('gets path right for function definition params', async () => { it('gets path right for function definition params', () => {
const code = `fn cube = (pos, scale) => { const code = `fn cube = (pos, scale) => {
const sg = startSketchAt(pos) const sg = startSketchAt(pos)
|> line([0, scale], %) |> line([0, scale], %)
@ -44,7 +46,7 @@ const b1 = cube([0,0], 10)`
subStrIndex + 'pos'.length, subStrIndex + 'pos'.length,
] ]
const ast = await parse(code) const ast = parse(code)
const nodePath = getNodePathFromSourceRange(ast, sourceRange) const nodePath = getNodePathFromSourceRange(ast, sourceRange)
const node = getNodeFromPath<Parameter>(ast, nodePath).node const node = getNodeFromPath<Parameter>(ast, nodePath).node
@ -60,7 +62,7 @@ const b1 = cube([0,0], 10)`
expect(node.type).toBe('Parameter') expect(node.type).toBe('Parameter')
expect(node.identifier.name).toBe('pos') expect(node.identifier.name).toBe('pos')
}) })
it('gets path right for deep within function definition body', async () => { it('gets path right for deep within function definition body', () => {
const code = `fn cube = (pos, scale) => { const code = `fn cube = (pos, scale) => {
const sg = startSketchAt(pos) const sg = startSketchAt(pos)
|> line([0, scale], %) |> line([0, scale], %)
@ -78,7 +80,7 @@ const b1 = cube([0,0], 10)`
subStrIndex + 'scale'.length, subStrIndex + 'scale'.length,
] ]
const ast = await parse(code) const ast = parse(code)
const nodePath = getNodePathFromSourceRange(ast, sourceRange) const nodePath = getNodePathFromSourceRange(ast, sourceRange)
const node = getNodeFromPath<Identifier>(ast, nodePath).node const node = getNodeFromPath<Identifier>(ast, nodePath).node
expect(nodePath).toEqual([ expect(nodePath).toEqual([

View File

@ -17,7 +17,9 @@ import {
import { enginelessExecutor } from '../lib/testHelpers' import { enginelessExecutor } from '../lib/testHelpers'
import { getNodePathFromSourceRange } from './queryAst' import { getNodePathFromSourceRange } from './queryAst'
beforeAll(() => initPromise) beforeAll(async () => {
await initPromise
})
describe('Testing createLiteral', () => { describe('Testing createLiteral', () => {
it('should create a literal', () => { it('should create a literal', () => {

View File

@ -1,81 +0,0 @@
import {
ParserWorkerResponse,
WasmWorker,
WasmWorkerEventType,
ParserWorkerCallResponse,
} from 'lang/workers/types'
import Worker from 'lang/workers/parser?worker'
import { Program, wasmUrl } from 'lang/wasm'
import { KCLError } from 'lang/errors'
import { v4 as uuidv4 } from 'uuid'
export default class Parser {
worker: any = new Worker({ name: 'parse' })
intitalized: boolean = false
mappings: Map<string, Program | KCLError> = new Map()
async parse(code: string): Promise<Program> {
this.ensureWorker()
const uuid = uuidv4()
this.worker.postMessage({
worker: WasmWorker.Parser,
eventType: WasmWorkerEventType.Call,
eventData: {
uuid,
code,
},
})
let result = await this.waitForResult(uuid)
if (result instanceof KCLError) {
throw result
}
return result
}
waitForResult(uuid: string): Promise<Program | KCLError> {
return new Promise((resolve) => {
const result = this.mappings.get(uuid)
if (result) {
this.mappings.delete(uuid)
resolve(result)
} else {
setTimeout(() => {
resolve(this.waitForResult(uuid))
}, 100)
}
})
}
ensureWorker() {
if (!this.intitalized) {
this.start()
}
}
// Start the worker.
start() {
if (this.intitalized) {
console.log('Worker already initialized')
return
}
this.worker.postMessage({
worker: WasmWorker.Parser,
eventType: WasmWorkerEventType.Init,
eventData: {
wasmUrl: wasmUrl(),
},
})
this.worker.onmessage = function (r: ParserWorkerResponse) {
switch (r.eventType) {
case WasmWorkerEventType.Init:
this.intitalized = true
break
case WasmWorkerEventType.Call:
const c = r.response as ParserWorkerCallResponse
this.mappings.set(c.uuid, c.response)
break
}
}
}
}

View File

@ -15,7 +15,9 @@ import {
createPipeSubstitution, createPipeSubstitution,
} from './modifyAst' } from './modifyAst'
beforeAll(() => initPromise) beforeAll(async () => {
await initPromise
})
describe('findAllPreviousVariables', () => { describe('findAllPreviousVariables', () => {
it('should find all previous variables', async () => { it('should find all previous variables', async () => {

View File

@ -1,56 +1,58 @@
import { parse, Program, recast, initPromise } from './wasm' import { parse, Program, recast, initPromise } from './wasm'
import fs from 'node:fs' import fs from 'node:fs'
beforeAll(() => initPromise) beforeAll(async () => {
await initPromise
})
describe('recast', () => { describe('recast', () => {
it('recasts a simple program', async () => { it('recasts a simple program', () => {
const code = '1 + 2' const code = '1 + 2'
const { ast } = await code2ast(code) const { ast } = code2ast(code)
const recasted = recast(ast) const recasted = recast(ast)
expect(recasted.trim()).toBe(code) expect(recasted.trim()).toBe(code)
}) })
it('variable declaration', async () => { it('variable declaration', () => {
const code = 'const myVar = 5' const code = 'const myVar = 5'
const { ast } = await code2ast(code) const { ast } = code2ast(code)
const recasted = recast(ast) const recasted = recast(ast)
expect(recasted.trim()).toBe(code) expect(recasted.trim()).toBe(code)
}) })
it("variable declaration that's binary with string", async () => { it("variable declaration that's binary with string", () => {
const code = "const myVar = 5 + 'yo'" const code = "const myVar = 5 + 'yo'"
const { ast } = await code2ast(code) const { ast } = code2ast(code)
const recasted = recast(ast) const recasted = recast(ast)
expect(recasted.trim()).toBe(code) expect(recasted.trim()).toBe(code)
const codeWithOtherQuotes = 'const myVar = 5 + "yo"' const codeWithOtherQuotes = 'const myVar = 5 + "yo"'
const { ast: ast2 } = await code2ast(codeWithOtherQuotes) const { ast: ast2 } = code2ast(codeWithOtherQuotes)
expect(recast(ast2).trim()).toBe(codeWithOtherQuotes) expect(recast(ast2).trim()).toBe(codeWithOtherQuotes)
}) })
it('test assigning two variables, the second summing with the first', async () => { it('test assigning two variables, the second summing with the first', () => {
const code = `const myVar = 5 const code = `const myVar = 5
const newVar = myVar + 1 const newVar = myVar + 1
` `
const { ast } = await code2ast(code) const { ast } = code2ast(code)
const recasted = recast(ast) const recasted = recast(ast)
expect(recasted).toBe(code) expect(recasted).toBe(code)
}) })
it('test assigning a var by cont concatenating two strings string', async () => { it('test assigning a var by cont concatenating two strings string', () => {
const code = fs.readFileSync( const code = fs.readFileSync(
'./src/lang/testExamples/variableDeclaration.cado', './src/lang/testExamples/variableDeclaration.cado',
'utf-8' 'utf-8'
) )
const { ast } = await code2ast(code) const { ast } = code2ast(code)
const recasted = recast(ast) const recasted = recast(ast)
expect(recasted.trim()).toBe(code.trim()) expect(recasted.trim()).toBe(code.trim())
}) })
it('test with function call', async () => { it('test with function call', () => {
const code = `const myVar = "hello" const code = `const myVar = "hello"
log(5, myVar) log(5, myVar)
` `
const { ast } = await code2ast(code) const { ast } = code2ast(code)
const recasted = recast(ast) const recasted = recast(ast)
expect(recasted).toBe(code) expect(recasted).toBe(code)
}) })
it('function declaration with call', async () => { it('function declaration with call', () => {
const code = [ const code = [
'fn funcN = (a, b) => {', 'fn funcN = (a, b) => {',
' return a + b', ' return a + b',
@ -58,22 +60,22 @@ log(5, myVar)
'const theVar = 60', 'const theVar = 60',
'const magicNum = funcN(9, theVar)', 'const magicNum = funcN(9, theVar)',
].join('\n') ].join('\n')
const { ast } = await code2ast(code) const { ast } = code2ast(code)
const recasted = recast(ast) const recasted = recast(ast)
expect(recasted.trim()).toBe(code) expect(recasted.trim()).toBe(code)
}) })
it('recast sketch declaration', async () => { it('recast sketch declaration', () => {
let code = `const mySketch = startSketchAt([0, 0]) let code = `const mySketch = startSketchAt([0, 0])
|> lineTo([0, 1], %, "myPath") |> lineTo([0, 1], %, "myPath")
|> lineTo([1, 1], %) |> lineTo([1, 1], %)
|> lineTo([1, 0], %, "rightPath") |> lineTo([1, 0], %, "rightPath")
|> close(%) |> close(%)
` `
const { ast } = await code2ast(code) const { ast } = code2ast(code)
const recasted = recast(ast) const recasted = recast(ast)
expect(recasted).toBe(code) expect(recasted).toBe(code)
}) })
it('sketch piped into callExpression', async () => { it('sketch piped into callExpression', () => {
const code = [ const code = [
'const mySk1 = startSketchAt([0, 0])', 'const mySk1 = startSketchAt([0, 0])',
' |> lineTo([1, 1], %)', ' |> lineTo([1, 1], %)',
@ -81,11 +83,11 @@ log(5, myVar)
' |> lineTo([1, 1], %)', ' |> lineTo([1, 1], %)',
' |> rx(90, %)', ' |> rx(90, %)',
].join('\n') ].join('\n')
const { ast } = await code2ast(code) const { ast } = code2ast(code)
const recasted = recast(ast) const recasted = recast(ast)
expect(recasted.trim()).toBe(code.trim()) expect(recasted.trim()).toBe(code.trim())
}) })
it('recast BinaryExpression piped into CallExpression', async () => { it('recast BinaryExpression piped into CallExpression', () => {
const code = [ const code = [
'fn myFn = (a) => {', 'fn myFn = (a) => {',
' return a + 1', ' return a + 1',
@ -93,49 +95,49 @@ log(5, myVar)
'const myVar = 5 + 1', 'const myVar = 5 + 1',
' |> myFn(%)', ' |> myFn(%)',
].join('\n') ].join('\n')
const { ast } = await code2ast(code) const { ast } = code2ast(code)
const recasted = recast(ast) const recasted = recast(ast)
expect(recasted.trim()).toBe(code) expect(recasted.trim()).toBe(code)
}) })
it('recast nested binary expression', async () => { it('recast nested binary expression', () => {
const code = ['const myVar = 1 + 2 * 5'].join('\n') const code = ['const myVar = 1 + 2 * 5'].join('\n')
const { ast } = await code2ast(code) const { ast } = code2ast(code)
const recasted = recast(ast) const recasted = recast(ast)
expect(recasted.trim()).toBe(code.trim()) expect(recasted.trim()).toBe(code.trim())
}) })
it('recast nested binary expression with parans', async () => { it('recast nested binary expression with parans', () => {
const code = ['const myVar = 1 + (1 + 2) * 5'].join('\n') const code = ['const myVar = 1 + (1 + 2) * 5'].join('\n')
const { ast } = await code2ast(code) const { ast } = code2ast(code)
const recasted = recast(ast) const recasted = recast(ast)
expect(recasted.trim()).toBe(code.trim()) expect(recasted.trim()).toBe(code.trim())
}) })
it('unnecessary paran wrap will be remove', async () => { it('unnecessary paran wrap will be remove', () => {
const code = ['const myVar = 1 + (2 * 5)'].join('\n') const code = ['const myVar = 1 + (2 * 5)'].join('\n')
const { ast } = await code2ast(code) const { ast } = code2ast(code)
const recasted = recast(ast) const recasted = recast(ast)
expect(recasted.trim()).toBe(code.replace('(', '').replace(')', '')) expect(recasted.trim()).toBe(code.replace('(', '').replace(')', ''))
}) })
it('complex nested binary expression', async () => { it('complex nested binary expression', () => {
const code = ['1 * ((2 + 3) / 4 + 5)'].join('\n') const code = ['1 * ((2 + 3) / 4 + 5)'].join('\n')
const { ast } = await code2ast(code) const { ast } = code2ast(code)
const recasted = recast(ast) const recasted = recast(ast)
expect(recasted.trim()).toBe(code.trim()) expect(recasted.trim()).toBe(code.trim())
}) })
it('multiplied paren expressions', async () => { it('multiplied paren expressions', () => {
const code = ['3 + (1 + 2) * (3 + 4)'].join('\n') const code = ['3 + (1 + 2) * (3 + 4)'].join('\n')
const { ast } = await code2ast(code) const { ast } = code2ast(code)
const recasted = recast(ast) const recasted = recast(ast)
expect(recasted.trim()).toBe(code.trim()) expect(recasted.trim()).toBe(code.trim())
}) })
it('recast array declaration', async () => { it('recast array declaration', () => {
const code = ['const three = 3', "const yo = [1, '2', three, 4 + 5]"].join( const code = ['const three = 3', "const yo = [1, '2', three, 4 + 5]"].join(
'\n' '\n'
) )
const { ast } = await code2ast(code) const { ast } = code2ast(code)
const recasted = recast(ast) const recasted = recast(ast)
expect(recasted.trim()).toBe(code.trim()) expect(recasted.trim()).toBe(code.trim())
}) })
it('recast long array declaration', async () => { it('recast long array declaration', () => {
const code = [ const code = [
'const three = 3', 'const three = 3',
'const yo = [', 'const yo = [',
@ -146,11 +148,11 @@ log(5, myVar)
" 'hey oooooo really long long long'", " 'hey oooooo really long long long'",
']', ']',
].join('\n') ].join('\n')
const { ast } = await code2ast(code) const { ast } = code2ast(code)
const recasted = recast(ast) const recasted = recast(ast)
expect(recasted.trim()).toBe(code.trim()) expect(recasted.trim()).toBe(code.trim())
}) })
it('recast long object execution', async () => { it('recast long object execution', () => {
const code = `const three = 3 const code = `const three = 3
const yo = { const yo = {
aStr: 'str', aStr: 'str',
@ -159,43 +161,43 @@ const yo = {
binExp: 4 + 5 binExp: 4 + 5
} }
` `
const { ast } = await code2ast(code) const { ast } = code2ast(code)
const recasted = recast(ast) const recasted = recast(ast)
expect(recasted).toBe(code) expect(recasted).toBe(code)
}) })
it('recast short object execution', async () => { it('recast short object execution', () => {
const code = `const yo = { key: 'val' } const code = `const yo = { key: 'val' }
` `
const { ast } = await code2ast(code) const { ast } = code2ast(code)
const recasted = recast(ast) const recasted = recast(ast)
expect(recasted).toBe(code) expect(recasted).toBe(code)
}) })
it('recast object execution with member expression', async () => { it('recast object execution with member expression', () => {
const code = `const yo = { a: { b: { c: '123' } } } const code = `const yo = { a: { b: { c: '123' } } }
const key = 'c' const key = 'c'
const myVar = yo.a['b'][key] const myVar = yo.a['b'][key]
const key2 = 'b' const key2 = 'b'
const myVar2 = yo['a'][key2].c const myVar2 = yo['a'][key2].c
` `
const { ast } = await code2ast(code) const { ast } = code2ast(code)
const recasted = recast(ast) const recasted = recast(ast)
expect(recasted).toBe(code) expect(recasted).toBe(code)
}) })
}) })
describe('testing recasting with comments and whitespace', () => { describe('testing recasting with comments and whitespace', () => {
it('code with comments', async () => { it('code with comments', () => {
const code = `const yo = { a: { b: { c: '123' } } } const code = `const yo = { a: { b: { c: '123' } } }
// this is a comment // this is a comment
const key = 'c' const key = 'c'
` `
const { ast } = await code2ast(code) const { ast } = code2ast(code)
const recasted = recast(ast) const recasted = recast(ast)
expect(recasted).toBe(code) expect(recasted).toBe(code)
}) })
it('code with comment and extra lines', async () => { it('code with comment and extra lines', () => {
const code = `const yo = 'c' const code = `const yo = 'c'
/* this is /* this is
@ -203,23 +205,23 @@ a
comment */ comment */
const yo = 'bing' const yo = 'bing'
` `
const { ast } = await code2ast(code) const { ast } = code2ast(code)
const recasted = recast(ast) const recasted = recast(ast)
expect(recasted).toBe(code) expect(recasted).toBe(code)
}) })
it('comments at the start and end', async () => { it('comments at the start and end', () => {
const code = `// this is a comment const code = `// this is a comment
const yo = { a: { b: { c: '123' } } } const yo = { a: { b: { c: '123' } } }
const key = 'c' const key = 'c'
// this is also a comment // this is also a comment
` `
const { ast } = await code2ast(code) const { ast } = code2ast(code)
const recasted = recast(ast) const recasted = recast(ast)
expect(recasted).toBe(code) expect(recasted).toBe(code)
}) })
it('comments in a fn block', async () => { it('comments in a fn block', () => {
const code = `fn myFn = async () => { const code = `fn myFn = () => {
// this is a comment // this is a comment
const yo = { a: { b: { c: '123' } } } const yo = { a: { b: { c: '123' } } }
@ -229,11 +231,11 @@ const key = 'c'
// this is also a comment // this is also a comment
} }
` `
const { ast } = await code2ast(code) const { ast } = code2ast(code)
const recasted = recast(ast) const recasted = recast(ast)
expect(recasted).toBe(code) expect(recasted).toBe(code)
}) })
it('comments in a pipe expression', async () => { it('comments in a pipe expression', () => {
const code = [ const code = [
'const mySk1 = startSketchAt([0, 0])', 'const mySk1 = startSketchAt([0, 0])',
' |> lineTo([1, 1], %)', ' |> lineTo([1, 1], %)',
@ -242,11 +244,11 @@ const key = 'c'
' // a comment', ' // a comment',
' |> rx(90, %)', ' |> rx(90, %)',
].join('\n') ].join('\n')
const { ast } = await code2ast(code) const { ast } = code2ast(code)
const recasted = recast(ast) const recasted = recast(ast)
expect(recasted.trim()).toBe(code) expect(recasted.trim()).toBe(code)
}) })
it('comments sprinkled in all over the place', async () => { it('comments sprinkled in all over the place', () => {
const code = ` const code = `
/* comment at start */ /* comment at start */
@ -264,11 +266,11 @@ const mySk1 = startSketchAt([0, 0])
|> rx(45, %) |> rx(45, %)
/* /*
one more for good measure one more for good measure
*/ */
` `
const { ast } = await code2ast(code) const { ast } = code2ast(code)
const recasted = recast(ast) const recasted = recast(ast)
expect(recasted).toBe(`/* comment at start */ expect(recasted).toBe(`/* comment at start */
@ -283,43 +285,43 @@ const mySk1 = startSketchAt([0, 0])
// and another with just white space between others below // and another with just white space between others below
|> ry(45, %) |> ry(45, %)
|> rx(45, %) |> rx(45, %)
/* one more for good measure */ /* one more for good measure */
`) `)
}) })
}) })
describe('testing call Expressions in BinaryExpressions and UnaryExpressions', () => { describe('testing call Expressions in BinaryExpressions and UnaryExpressions', () => {
it('nested callExpression in binaryExpression', async () => { it('nested callExpression in binaryExpression', () => {
const code = 'const myVar = 2 + min(100, legLen(5, 3))' const code = 'const myVar = 2 + min(100, legLen(5, 3))'
const { ast } = await code2ast(code) const { ast } = code2ast(code)
const recasted = recast(ast) const recasted = recast(ast)
expect(recasted.trim()).toBe(code) expect(recasted.trim()).toBe(code)
}) })
it('nested callExpression in unaryExpression', async () => { it('nested callExpression in unaryExpression', () => {
const code = 'const myVar = -min(100, legLen(5, 3))' const code = 'const myVar = -min(100, legLen(5, 3))'
const { ast } = await code2ast(code) const { ast } = code2ast(code)
const recasted = recast(ast) const recasted = recast(ast)
expect(recasted.trim()).toBe(code) expect(recasted.trim()).toBe(code)
}) })
it('with unaryExpression in callExpression', async () => { it('with unaryExpression in callExpression', () => {
const code = 'const myVar = min(5, -legLen(5, 4))' const code = 'const myVar = min(5, -legLen(5, 4))'
const { ast } = await code2ast(code) const { ast } = code2ast(code)
const recasted = recast(ast) const recasted = recast(ast)
expect(recasted.trim()).toBe(code) expect(recasted.trim()).toBe(code)
}) })
it('with unaryExpression in sketch situation', async () => { it('with unaryExpression in sketch situation', () => {
const code = [ const code = [
'const part001 = startSketchAt([0, 0])', 'const part001 = startSketchAt([0, 0])',
' |> line([-2.21, -legLen(5, min(3, 999))], %)', ' |> line([-2.21, -legLen(5, min(3, 999))], %)',
].join('\n') ].join('\n')
const { ast } = await code2ast(code) const { ast } = code2ast(code)
const recasted = recast(ast) const recasted = recast(ast)
expect(recasted.trim()).toBe(code) expect(recasted.trim()).toBe(code)
}) })
}) })
describe('it recasts wrapped object expressions in pipe bodies with correct indentation', () => { describe('it recasts wrapped object expressions in pipe bodies with correct indentation', () => {
it('with a single line', async () => { it('with a single line', () => {
const code = `const part001 = startSketchAt([-0.01, -0.08]) const code = `const part001 = startSketchAt([-0.01, -0.08])
|> line([0.62, 4.15], %, 'seg01') |> line([0.62, 4.15], %, 'seg01')
|> line([2.77, -1.24], %) |> line([2.77, -1.24], %)
@ -330,35 +332,35 @@ describe('it recasts wrapped object expressions in pipe bodies with correct inde
}, %) }, %)
|> line([-0.42, -1.72], %) |> line([-0.42, -1.72], %)
` `
const { ast } = await code2ast(code) const { ast } = code2ast(code)
const recasted = recast(ast) const recasted = recast(ast)
expect(recasted).toBe(code) expect(recasted).toBe(code)
}) })
it('recasts wrapped object expressions NOT in pipe body correctly', async () => { it('recasts wrapped object expressions NOT in pipe body correctly', () => {
const code = `angledLineThatIntersects({ const code = `angledLineThatIntersects({
angle: 201, angle: 201,
offset: -1.35, offset: -1.35,
intersectTag: 'seg01' intersectTag: 'seg01'
}, %) }, %)
` `
const { ast } = await code2ast(code) const { ast } = code2ast(code)
const recasted = recast(ast) const recasted = recast(ast)
expect(recasted).toBe(code) expect(recasted).toBe(code)
}) })
}) })
describe('it recasts binary expression using brackets where needed', () => { describe('it recasts binary expression using brackets where needed', () => {
it('when there are two minus in a row', async () => { it('when there are two minus in a row', () => {
const code = `const part001 = 1 - (def - abc) const code = `const part001 = 1 - (def - abc)
` `
const recasted = recast((await code2ast(code)).ast) const recasted = recast(code2ast(code).ast)
expect(recasted).toBe(code) expect(recasted).toBe(code)
}) })
}) })
// helpers // helpers
async function code2ast(code: string): Promise<{ ast: Program }> { function code2ast(code: string): { ast: Program } {
const ast = await parse(code) const ast = parse(code)
return { ast } return { ast }
} }

View File

@ -1171,7 +1171,10 @@ export class EngineCommandManager {
type: 'receive-reliable', type: 'receive-reliable',
data: message, data: message,
id, id,
cmd_type: command?.commandType || this.lastArtifactMap[id]?.commandType, cmd_type:
command?.commandType ||
this.lastArtifactMap[id]?.commandType ||
sceneCommand?.commandType,
}) })
Object.values(this.subscriptions[modelingResponse.type] || {}).forEach( Object.values(this.subscriptions[modelingResponse.type] || {}).forEach(
(callback) => callback(modelingResponse) (callback) => callback(modelingResponse)

View File

@ -25,7 +25,9 @@ const eachQuad: [number, [number, number]][] = [
[675, [1, -1]], [675, [1, -1]],
] ]
beforeAll(() => initPromise) beforeAll(async () => {
await initPromise
})
describe('testing getYComponent', () => { describe('testing getYComponent', () => {
it('should return the vertical component of a vector correctly when given angles in each quadrant (and with angles < 0, or > 360)', () => { it('should return the vertical component of a vector correctly when given angles in each quadrant (and with angles < 0, or > 360)', () => {
@ -100,11 +102,11 @@ describe('testing changeSketchArguments', () => {
|> startProfileAt([0, 0], %) |> startProfileAt([0, 0], %)
|> ${line} |> ${line}
|> lineTo([0.46, -5.82], %) |> lineTo([0.46, -5.82], %)
// |> rx(45, %) // |> rx(45, %)
` `
const code = genCode(lineToChange) const code = genCode(lineToChange)
const expectedCode = genCode(lineAfterChange) const expectedCode = genCode(lineAfterChange)
const ast = await parse(code) const ast = parse(code)
const programMemory = await enginelessExecutor(ast) const programMemory = await enginelessExecutor(ast)
const sourceStart = code.indexOf(lineToChange) const sourceStart = code.indexOf(lineToChange)
const { modifiedAst } = changeSketchArguments( const { modifiedAst } = changeSketchArguments(
@ -128,7 +130,7 @@ const mySketch001 = startSketchOn('XY')
// |> rx(45, %) // |> rx(45, %)
|> lineTo([-1.59, -1.54], %) |> lineTo([-1.59, -1.54], %)
|> lineTo([0.46, -5.82], %)` |> lineTo([0.46, -5.82], %)`
const ast = await parse(code) const ast = parse(code)
const programMemory = await enginelessExecutor(ast) const programMemory = await enginelessExecutor(ast)
const sourceStart = code.indexOf(lineToChange) const sourceStart = code.indexOf(lineToChange)
expect(sourceStart).toBe(95) expect(sourceStart).toBe(95)
@ -190,7 +192,7 @@ describe('testing addTagForSketchOnFace', () => {
|> lineTo([0.46, -5.82], %) |> lineTo([0.46, -5.82], %)
` `
const code = genCode(originalLine) const code = genCode(originalLine)
const ast = await parse(code) const ast = parse(code)
const programMemory = await enginelessExecutor(ast) const programMemory = await enginelessExecutor(ast)
const sourceStart = code.indexOf(originalLine) const sourceStart = code.indexOf(originalLine)
const sourceRange: [number, number] = [ const sourceRange: [number, number] = [

View File

@ -8,7 +8,9 @@ import { getSketchSegmentFromSourceRange } from './sketchConstraints'
import { Selection } from 'lib/selections' import { Selection } from 'lib/selections'
import { enginelessExecutor } from '../../lib/testHelpers' import { enginelessExecutor } from '../../lib/testHelpers'
beforeAll(() => initPromise) beforeAll(async () => {
await initPromise
})
// testing helper function // testing helper function
async function testingSwapSketchFnCall({ async function testingSwapSketchFnCall({
@ -28,7 +30,7 @@ async function testingSwapSketchFnCall({
type: 'default', type: 'default',
range: [startIndex, startIndex + callToSwap.length], range: [startIndex, startIndex + callToSwap.length],
} }
const ast = await parse(inputCode) const ast = parse(inputCode)
const programMemory = await enginelessExecutor(ast) const programMemory = await enginelessExecutor(ast)
const selections = { const selections = {
codeBasedSelections: [range], codeBasedSelections: [range],
@ -351,7 +353,7 @@ const part001 = startSketchOn('XY')
|> line([2.14, 1.35], %) // normal-segment |> line([2.14, 1.35], %) // normal-segment
|> xLine(3.54, %)` |> xLine(3.54, %)`
it('normal case works', async () => { it('normal case works', async () => {
const programMemory = await enginelessExecutor(await parse(code)) const programMemory = await enginelessExecutor(parse(code))
const index = code.indexOf('// normal-segment') - 7 const index = code.indexOf('// normal-segment') - 7
const { __geoMeta, ...segment } = getSketchSegmentFromSourceRange( const { __geoMeta, ...segment } = getSketchSegmentFromSourceRange(
programMemory.root['part001'] as SketchGroup, programMemory.root['part001'] as SketchGroup,
@ -365,7 +367,7 @@ const part001 = startSketchOn('XY')
}) })
}) })
it('verify it works when the segment is in the `start` property', async () => { it('verify it works when the segment is in the `start` property', async () => {
const programMemory = await enginelessExecutor(await parse(code)) const programMemory = await enginelessExecutor(parse(code))
const index = code.indexOf('// segment-in-start') - 7 const index = code.indexOf('// segment-in-start') - 7
const { __geoMeta, ...segment } = getSketchSegmentFromSourceRange( const { __geoMeta, ...segment } = getSketchSegmentFromSourceRange(
programMemory.root['part001'] as SketchGroup, programMemory.root['part001'] as SketchGroup,

View File

@ -11,57 +11,59 @@ import { ToolTip } from '../../useStore'
import { Selections } from 'lib/selections' import { Selections } from 'lib/selections'
import { enginelessExecutor } from '../../lib/testHelpers' import { enginelessExecutor } from '../../lib/testHelpers'
beforeAll(() => initPromise) beforeAll(async () => {
await initPromise
})
describe('testing getConstraintType', () => { describe('testing getConstraintType', () => {
const helper = getConstraintTypeFromSourceHelper const helper = getConstraintTypeFromSourceHelper
it('testing line', async () => { it('testing line', () => {
expect(await helper(`line([5, myVar], %)`)).toBe('yRelative') expect(helper(`line([5, myVar], %)`)).toBe('yRelative')
expect(await helper(`line([myVar, 5], %)`)).toBe('xRelative') expect(helper(`line([myVar, 5], %)`)).toBe('xRelative')
}) })
it('testing lineTo', async () => { it('testing lineTo', () => {
expect(await helper(`lineTo([5, myVar], %)`)).toBe('yAbsolute') expect(helper(`lineTo([5, myVar], %)`)).toBe('yAbsolute')
expect(await helper(`lineTo([myVar, 5], %)`)).toBe('xAbsolute') expect(helper(`lineTo([myVar, 5], %)`)).toBe('xAbsolute')
}) })
it('testing angledLine', async () => { it('testing angledLine', () => {
expect(await helper(`angledLine([5, myVar], %)`)).toBe('length') expect(helper(`angledLine([5, myVar], %)`)).toBe('length')
expect(await helper(`angledLine([myVar, 5], %)`)).toBe('angle') expect(helper(`angledLine([myVar, 5], %)`)).toBe('angle')
}) })
it('testing angledLineOfXLength', async () => { it('testing angledLineOfXLength', () => {
expect(await helper(`angledLineOfXLength([5, myVar], %)`)).toBe('xRelative') expect(helper(`angledLineOfXLength([5, myVar], %)`)).toBe('xRelative')
expect(await helper(`angledLineOfXLength([myVar, 5], %)`)).toBe('angle') expect(helper(`angledLineOfXLength([myVar, 5], %)`)).toBe('angle')
}) })
it('testing angledLineToX', async () => { it('testing angledLineToX', () => {
expect(await helper(`angledLineToX([5, myVar], %)`)).toBe('xAbsolute') expect(helper(`angledLineToX([5, myVar], %)`)).toBe('xAbsolute')
expect(await helper(`angledLineToX([myVar, 5], %)`)).toBe('angle') expect(helper(`angledLineToX([myVar, 5], %)`)).toBe('angle')
}) })
it('testing angledLineOfYLength', async () => { it('testing angledLineOfYLength', () => {
expect(await helper(`angledLineOfYLength([5, myVar], %)`)).toBe('yRelative') expect(helper(`angledLineOfYLength([5, myVar], %)`)).toBe('yRelative')
expect(await helper(`angledLineOfYLength([myVar, 5], %)`)).toBe('angle') expect(helper(`angledLineOfYLength([myVar, 5], %)`)).toBe('angle')
}) })
it('testing angledLineToY', async () => { it('testing angledLineToY', () => {
expect(await helper(`angledLineToY([5, myVar], %)`)).toBe('yAbsolute') expect(helper(`angledLineToY([5, myVar], %)`)).toBe('yAbsolute')
expect(await helper(`angledLineToY([myVar, 5], %)`)).toBe('angle') expect(helper(`angledLineToY([myVar, 5], %)`)).toBe('angle')
}) })
const helper2 = getConstraintTypeFromSourceHelper2 const helper2 = getConstraintTypeFromSourceHelper2
it('testing xLine', async () => { it('testing xLine', () => {
expect(await helper2(`xLine(5, %)`)).toBe('yRelative') expect(helper2(`xLine(5, %)`)).toBe('yRelative')
}) })
it('testing yLine', async () => { it('testing yLine', () => {
expect(await helper2(`yLine(5, %)`)).toBe('xRelative') expect(helper2(`yLine(5, %)`)).toBe('xRelative')
}) })
it('testing xLineTo', async () => { it('testing xLineTo', () => {
expect(await helper2(`xLineTo(5, %)`)).toBe('yAbsolute') expect(helper2(`xLineTo(5, %)`)).toBe('yAbsolute')
}) })
it('testing yLineTo', async () => { it('testing yLineTo', () => {
expect(await helper2(`yLineTo(5, %)`)).toBe('xAbsolute') expect(helper2(`yLineTo(5, %)`)).toBe('xAbsolute')
}) })
}) })
async function getConstraintTypeFromSourceHelper( function getConstraintTypeFromSourceHelper(
code: string code: string
): Promise<ReturnType<typeof getConstraintType>> { ): ReturnType<typeof getConstraintType> {
const ast = await parse(code) const ast = parse(code)
const args = (ast.body[0] as any).expression.arguments[0].elements as [ const args = (ast.body[0] as any).expression.arguments[0].elements as [
Value, Value,
Value Value
@ -69,10 +71,10 @@ async function getConstraintTypeFromSourceHelper(
const fnName = (ast.body[0] as any).expression.callee.name as ToolTip const fnName = (ast.body[0] as any).expression.callee.name as ToolTip
return getConstraintType(args, fnName) return getConstraintType(args, fnName)
} }
async function getConstraintTypeFromSourceHelper2( function getConstraintTypeFromSourceHelper2(
code: string code: string
): Promise<ReturnType<typeof getConstraintType>> { ): ReturnType<typeof getConstraintType> {
const ast = await parse(code) const ast = parse(code)
const arg = (ast.body[0] as any).expression.arguments[0] as Value const arg = (ast.body[0] as any).expression.arguments[0] as Value
const fnName = (ast.body[0] as any).expression.callee.name as ToolTip const fnName = (ast.body[0] as any).expression.callee.name as ToolTip
return getConstraintType(arg, fnName) return getConstraintType(arg, fnName)
@ -197,7 +199,7 @@ const part001 = startSketchOn('XY')
|> yLine(segLen('seg01', %), %) // ln-yLineTo-free should convert to yLine |> yLine(segLen('seg01', %), %) // ln-yLineTo-free should convert to yLine
` `
it('should transform the ast', async () => { it('should transform the ast', async () => {
const ast = await parse(inputScript) const ast = parse(inputScript)
const selectionRanges: Selections['codeBasedSelections'] = inputScript const selectionRanges: Selections['codeBasedSelections'] = inputScript
.split('\n') .split('\n')
.filter((ln) => ln.includes('//')) .filter((ln) => ln.includes('//'))
@ -284,7 +286,7 @@ const part001 = startSketchOn('XY')
|> xLineTo(myVar3, %) // select for horizontal constraint 10 |> xLineTo(myVar3, %) // select for horizontal constraint 10
|> angledLineToY([301, myVar], %) // select for vertical constraint 10 |> angledLineToY([301, myVar], %) // select for vertical constraint 10
` `
const ast = await parse(inputScript) const ast = parse(inputScript)
const selectionRanges: Selections['codeBasedSelections'] = inputScript const selectionRanges: Selections['codeBasedSelections'] = inputScript
.split('\n') .split('\n')
.filter((ln) => ln.includes('// select for horizontal constraint')) .filter((ln) => ln.includes('// select for horizontal constraint'))
@ -342,7 +344,7 @@ const part001 = startSketchOn('XY')
|> angledLineToX([333, myVar3], %) // select for horizontal constraint 10 |> angledLineToX([333, myVar3], %) // select for horizontal constraint 10
|> yLineTo(myVar, %) // select for vertical constraint 10 |> yLineTo(myVar, %) // select for vertical constraint 10
` `
const ast = await parse(inputScript) const ast = parse(inputScript)
const selectionRanges: Selections['codeBasedSelections'] = inputScript const selectionRanges: Selections['codeBasedSelections'] = inputScript
.split('\n') .split('\n')
.filter((ln) => ln.includes('// select for vertical constraint')) .filter((ln) => ln.includes('// select for vertical constraint'))
@ -433,7 +435,7 @@ async function helperThing(
linesOfInterest: string[], linesOfInterest: string[],
constraint: ConstraintType constraint: ConstraintType
): Promise<string> { ): Promise<string> {
const ast = await parse(inputScript) const ast = parse(inputScript)
const selectionRanges: Selections['codeBasedSelections'] = inputScript const selectionRanges: Selections['codeBasedSelections'] = inputScript
.split('\n') .split('\n')
.filter((ln) => .filter((ln) =>
@ -465,7 +467,7 @@ async function helperThing(
} }
describe('testing getConstraintLevelFromSourceRange', () => { describe('testing getConstraintLevelFromSourceRange', () => {
it('should divide up lines into free, partial and fully contrained', async () => { it('should divide up lines into free, partial and fully contrained', () => {
const code = `const baseLength = 3 const code = `const baseLength = 3
const baseThick = 1 const baseThick = 1
const armThick = 0.5 const armThick = 0.5
@ -495,7 +497,7 @@ const part001 = startSketchOn('XY')
|> line([-1.49, 1.06], %) // free |> line([-1.49, 1.06], %) // free
|> xLine(-3.43 + 0, %) // full |> xLine(-3.43 + 0, %) // full
|> angledLineOfXLength([243 + 0, 1.2 + 0], %) // full` |> angledLineOfXLength([243 + 0, 1.2 + 0], %) // full`
const ast = await parse(code) const ast = parse(code)
const constraintLevels: ReturnType< const constraintLevels: ReturnType<
typeof getConstraintLevelFromSourceRange typeof getConstraintLevelFromSourceRange
>['level'][] = ['full', 'partial', 'free'] >['level'][] = ['full', 'partial', 'free']

View File

@ -1,7 +1,9 @@
import { parse, initPromise } from '../wasm' import { parse, initPromise } from '../wasm'
import { enginelessExecutor } from '../../lib/testHelpers' import { enginelessExecutor } from '../../lib/testHelpers'
beforeAll(() => initPromise) beforeAll(async () => {
await initPromise
})
describe('testing angledLineThatIntersects', () => { describe('testing angledLineThatIntersects', () => {
it('angledLineThatIntersects should intersect with another line', async () => { it('angledLineThatIntersects should intersect with another line', async () => {
@ -15,9 +17,9 @@ describe('testing angledLineThatIntersects', () => {
offset: ${offset}, offset: ${offset},
}, %, "yo2") }, %, "yo2")
const intersect = segEndX('yo2', part001)` const intersect = segEndX('yo2', part001)`
const { root } = await enginelessExecutor(await parse(code('-1'))) const { root } = await enginelessExecutor(parse(code('-1')))
expect(root.intersect.value).toBe(1 + Math.sqrt(2)) expect(root.intersect.value).toBe(1 + Math.sqrt(2))
const { root: noOffset } = await enginelessExecutor(await parse(code('0'))) const { root: noOffset } = await enginelessExecutor(parse(code('0')))
expect(noOffset.intersect.value).toBeCloseTo(1) expect(noOffset.intersect.value).toBeCloseTo(1)
}) })
}) })

View File

@ -1,6 +1,8 @@
import { lexer, initPromise } from './wasm' import { lexer, initPromise } from './wasm'
beforeAll(() => initPromise) beforeAll(async () => {
await initPromise
})
describe('testing lexer', () => { describe('testing lexer', () => {
it('async lexer works too', async () => { it('async lexer works too', async () => {

View File

@ -25,8 +25,7 @@ import { AppInfo } from 'wasm-lib/kcl/bindings/AppInfo'
import { CoreDumpManager } from 'lib/coredump' import { CoreDumpManager } from 'lib/coredump'
import openWindow from 'lib/openWindow' import openWindow from 'lib/openWindow'
import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes' import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
import { rangeTypeFix } from './workers/types' import { TEST } from 'env'
import { parser } from 'lib/singletons'
export type { Program } from '../wasm-lib/kcl/bindings/Program' export type { Program } from '../wasm-lib/kcl/bindings/Program'
export type { Value } from '../wasm-lib/kcl/bindings/Value' export type { Value } from '../wasm-lib/kcl/bindings/Value'
@ -97,26 +96,23 @@ export const wasmUrl = () => {
// Initialise the wasm module. // Initialise the wasm module.
const initialise = async () => { const initialise = async () => {
const fullUrl = wasmUrl() try {
const input = await fetch(fullUrl) const fullUrl = wasmUrl()
const buffer = await input.arrayBuffer() const input = await fetch(fullUrl)
return init(buffer) const buffer = await input.arrayBuffer()
return await init(buffer)
} catch (e) {
console.log('Error initialising WASM', e)
throw e
}
} }
export const initPromise = initialise() export const initPromise = initialise()
export type PathToNode = [string | number, string][] export const rangeTypeFix = (ranges: number[][]): [number, number][] =>
ranges.map(([start, end]) => [start, end])
interface Memory { export const parse = (code: string): Program => {
[key: string]: MemoryItem
}
export interface ProgramMemory {
root: Memory
return: ProgramReturn | null
}
/*export const parse = (code: string): Program => {
try { try {
const program: Program = parse_wasm(code) const program: Program = parse_wasm(code)
return program return program
@ -131,10 +127,17 @@ export interface ProgramMemory {
console.log(kclError) console.log(kclError)
throw kclError throw kclError
} }
}*/ }
export const parse = async (code: string): Promise<Program> => { export type PathToNode = [string | number, string][]
return parser.parse(code)
interface Memory {
[key: string]: MemoryItem
}
export interface ProgramMemory {
root: Memory
return: ProgramReturn | null
} }
export const executor = async ( export const executor = async (
@ -156,10 +159,6 @@ export const executor = async (
return _programMemory return _programMemory
} }
const getSettingsState = import('components/SettingsAuthProvider').then(
(module) => module.getSettingsState
)
export const _executor = async ( export const _executor = async (
node: Program, node: Program,
programMemory: ProgramMemory = { root: {}, return: null }, programMemory: ProgramMemory = { root: {}, return: null },
@ -167,8 +166,14 @@ export const _executor = async (
isMock: boolean isMock: boolean
): Promise<ProgramMemory> => { ): Promise<ProgramMemory> => {
try { try {
const baseUnit = let baseUnit = 'mm'
(await getSettingsState)()?.modeling.defaultUnit.current || 'mm' if (!TEST) {
const getSettingsState = import('components/SettingsAuthProvider').then(
(module) => module.getSettingsState
)
baseUnit =
(await getSettingsState)()?.modeling.defaultUnit.current || 'mm'
}
const memory: ProgramMemory = await execute_wasm( const memory: ProgramMemory = await execute_wasm(
JSON.stringify(node), JSON.stringify(node),
JSON.stringify(programMemory), JSON.stringify(programMemory),

View File

@ -1,60 +0,0 @@
import init, { parse_wasm } from 'wasm-lib/pkg/wasm_lib'
import { Program } from 'lang/wasm'
import { KclError as RustKclError } from 'wasm-lib/kcl/bindings/KclError'
import { KCLError } from 'lang/errors'
import {
WasmWorkerEventType,
WasmWorkerEvent,
WasmWorkerOptions,
ParserWorkerCall,
rangeTypeFix,
} from 'lang/workers/types'
// Initialise the wasm module.
const initialise = async (wasmUrl: string) => {
const input = await fetch(wasmUrl)
const buffer = await input.arrayBuffer()
return init(buffer)
}
onmessage = function (event) {
const { worker, eventType, eventData }: WasmWorkerEvent = event.data
switch (eventType) {
case WasmWorkerEventType.Init:
let { wasmUrl }: WasmWorkerOptions = eventData as WasmWorkerOptions
initialise(wasmUrl)
.then((instantiatedModule) => {
console.log('Worker: WASM module loaded', worker, instantiatedModule)
postMessage({
eventType: WasmWorkerEventType.Init,
response: { worker: worker, initialized: true },
})
})
.catch((error) => {
console.error('Worker: Error loading wasm module', worker, error)
})
break
case WasmWorkerEventType.Call:
const data = eventData as ParserWorkerCall
try {
const program: Program = parse_wasm(data.code)
postMessage({ uuid: data.uuid, response: program })
} catch (e: any) {
const parsed: RustKclError = JSON.parse(e.toString())
const kclError = new KCLError(
parsed.kind,
parsed.msg,
rangeTypeFix(parsed.sourceRanges)
)
postMessage({
eventType: WasmWorkerEventType.Call,
response: { uuid: data.uuid, response: kclError },
})
}
break
default:
console.error('Worker: Unknown message type', worker, eventType)
}
}

View File

@ -1,43 +0,0 @@
import { KCLError } from 'lang/errors'
import { Program } from 'lang/wasm'
export enum WasmWorker {
Parser = 'parser',
}
export enum WasmWorkerEventType {
Init = 'init',
Call = 'call',
}
export interface WasmWorkerOptions {
wasmUrl: string
}
export interface ParserWorkerCall {
uuid: string
code: string
}
export interface ParserWorkerInitResponse {
worker: WasmWorker
initialized: boolean
}
export interface ParserWorkerCallResponse {
uuid: string
response: Program | KCLError
}
export interface ParserWorkerResponse {
eventType: WasmWorkerEventType
response: ParserWorkerInitResponse | ParserWorkerCallResponse
}
export interface WasmWorkerEvent {
eventType: WasmWorkerEventType
eventData: Uint8Array | WasmWorkerOptions | ParserWorkerCall
worker: WasmWorker
}
export const rangeTypeFix = (ranges: number[][]): [number, number][] =>
ranges.map(([start, end]) => [start, end])

59
src/lib/circleTool.ts Normal file
View File

@ -0,0 +1,59 @@
import {
createArrayExpression,
createBinaryExpression,
createCallExpressionStdLib,
createLiteral,
createPipeSubstitution,
} from 'lang/modifyAst'
import { roundOff } from './utils'
import {
ArrayExpression,
CallExpression,
Literal,
PipeExpression,
} from 'lang/wasm'
/**
* Returns AST expressions for this KCL code:
* const yo = startSketchOn('XY')
* |> circle([0, 0], 0, %)
*/
export const circleAsCallExpressions = (
circleOrigin: [number, number],
tags: [string]
) => [
createCallExpressionStdLib('circle', [
createArrayExpression([
createLiteral(roundOff(circleOrigin[0])),
createLiteral(roundOff(circleOrigin[1])),
]),
createLiteral(10),
createPipeSubstitution(),
createLiteral(tags[0]),
]),
]
/**
* Mutates the pipeExpression to update the circle sketch
* @param pipeExpression
* @param x
* @param y
* @param tag
*/
export function updateCircleSketch(
pipeExpression: PipeExpression,
x: number,
y: number,
tag: string
) {
const circle = pipeExpression.body[1] as CallExpression
const origin = circle.arguments[0] as ArrayExpression
const originX = (origin.elements[0] as Literal).value
const originY = (origin.elements[1] as Literal).value
const radius = roundOff(
Math.sqrt((x - Number(originX)) ** 2 + (y - Number(originY)) ** 2)
)
;(circle.arguments[1] as Literal) = createLiteral(radius)
}

Some files were not shown because too many files have changed in this diff Show More