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
281 changed files with 1714 additions and 964 deletions

2
.nvmrc
View File

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

View File

@ -1,3 +1,3 @@
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}, %)
|> line([${commonPoints.num1}, 0], %)
|> line([0, ${commonPoints.num1}], %)`)
await page.waitForTimeout(100)
await page.mouse.click(startXPx, 500 - PUR * 20)
await expect(page.locator('.cm-content'))
.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()
})
/* 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,
}) => {
const u = getUtils(page)
@ -349,7 +348,7 @@ const sketch001 = startSketchOn(box, "revolveAxis")
|> startProfileAt([5, 10], %)
|> line([0, -10], %)
|> line([2, 0], %)
|> line([0, 10], %)
|> line([0, -10], %)
|> close(%)
|> revolve({
axis: getEdge('revolveAxis', box),
@ -364,7 +363,7 @@ angle: 90
await u.waitForAuthSkipAppStart()
u.openDebugPanel()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
@ -378,7 +377,7 @@ angle: 90
'sketch profile must lie entirely on one side of the revolution axis'
)
).toBeVisible()
})*/
})
test('executes on load', async ({ page }) => {
const u = getUtils(page)
@ -566,7 +565,9 @@ test('Auto complete works', async ({ page }) => {
await page.keyboard.press('Tab')
await page.keyboard.type('12')
await page.waitForTimeout(100)
await page.keyboard.press('Tab')
await page.waitForTimeout(100)
await page.keyboard.press('Tab')
await page.keyboard.press('Tab')
await page.keyboard.press('Enter')
@ -736,7 +737,7 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
await u.openDebugPanel()
const xAxisClick = () =>
page.mouse.click(700, 250).then(() => page.waitForTimeout(100))
page.mouse.click(700, 253).then(() => page.waitForTimeout(100))
const emptySpaceClick = () =>
page.mouse.click(728, 343).then(() => page.waitForTimeout(100))
const topHorzSegmentClick = () =>
@ -761,6 +762,7 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${commonPoints.startAt}, %)`)
await page.waitForTimeout(100)
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await expect(page.locator('.cm-content'))
@ -768,12 +770,14 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|> startProfileAt(${commonPoints.startAt}, %)
|> line([${commonPoints.num1}, 0], %)`)
await page.waitForTimeout(100)
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${commonPoints.startAt}, %)
|> line([${commonPoints.num1}, 0], %)
|> line([0, ${commonPoints.num1}], %)`)
await page.waitForTimeout(100)
await page.mouse.click(startXPx, 500 - PUR * 20)
await expect(page.locator('.cm-content'))
.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 u.closeDebugPanel()
const selectionSequence = async () => {
const selectionSequence = async (isSecondTime = false) => {
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()
// 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
await page.mouse.move(startXPx + PUR * 10, 500 - PUR * 15) // mouse off
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()
// 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')
const absYButton = page.getByRole('button', { name: 'ABS Y' })
await expect(absYButton).toBeDisabled()
await page.waitForTimeout(100)
await xAxisClick()
await page.keyboard.up('Shift')
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
await emptySpaceClick()
await page.waitForTimeout(100)
// same selection but click the axis first
await xAxisClick()
await expect(absYButton).toBeDisabled()
await page.keyboard.down('Shift')
await page.waitForTimeout(100)
await topHorzSegmentClick()
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.keyboard.down('Shift')
await expect(absYButton).toBeDisabled()
await page.waitForTimeout(100)
await xAxisClick()
await page.keyboard.up('Shift')
await expect(absYButton).not.toBeDisabled()
@ -875,11 +890,16 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
await page.waitForTimeout(100)
// 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
// hover again and check it works
await selectionSequence()
await selectionSequence(true)
})
test.describe('Command bar tests', () => {
@ -1065,6 +1085,7 @@ test('Can add multiple sketches', async ({ page }) => {
|> startProfileAt(${commonPoints.startAt}, %)
|> line([${commonPoints.num1}, 0], %)
|> line([0, ${commonPoints.num1}], %)`)
await page.waitForTimeout(100)
await page.mouse.click(startXPx, 500 - PUR * 20)
const finalCodeFirstSketch = `const part001 = startSketchOn('-XZ')
|> startProfileAt(${commonPoints.startAt}, %)
@ -1080,24 +1101,33 @@ test('Can add multiple sketches', async ({ page }) => {
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
await u.clearCommandLogs()
await page.getByRole('button', { name: 'Start Sketch' }).click()
await page.waitForTimeout(100)
await page.mouse.click(673, 384)
await page.waitForTimeout(400)
await page.mouse.click(650, 450)
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
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)
const startAt2 = '[0.93,-1.25]'
const startAt2 =
process.platform === 'darwin' ? '[9.75, -13.16]' : '[0.93, -1.25]'
await expect(
(await page.locator('.cm-content').innerText()).replace(/\s/g, '')
).toBe(
`${finalCodeFirstSketch}
const part002 = startSketchOn('XY')
const part002 = startSketchOn('${plane}')
|> startProfileAt(${startAt2}, %)`.replace(/\s/g, '')
)
await page.waitForTimeout(100)
@ -1106,12 +1136,12 @@ const part002 = startSketchOn('XY')
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await page.waitForTimeout(100)
const num2 = 0.94
const num2 = process.platform === 'darwin' ? 9.84 : 0.94
await expect(
(await page.locator('.cm-content').innerText()).replace(/\s/g, '')
).toBe(
`${finalCodeFirstSketch}
const part002 = startSketchOn('XY')
const part002 = startSketchOn('${plane}')
|> startProfileAt(${startAt2}, %)
|> line([${num2}, 0], %)`.replace(/\s/g, '')
)
@ -1121,21 +1151,29 @@ const part002 = startSketchOn('XY')
(await page.locator('.cm-content').innerText()).replace(/\s/g, '')
).toBe(
`${finalCodeFirstSketch}
const part002 = startSketchOn('XY')
const part002 = startSketchOn('${plane}')
|> startProfileAt(${startAt2}, %)
|> 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 expect(
(await page.locator('.cm-content').innerText()).replace(/\s/g, '')
).toBe(
`${finalCodeFirstSketch}
const part002 = startSketchOn('XY')
const part002 = startSketchOn('${plane}')
|> startProfileAt(${startAt2}, %)
|> line([${num2}, 0], %)
|> line([0, ${roundOff(num2 - 0.01)}], %)
|> line([-1.87, 0], %)`.replace(/\s/g, '')
|> line([0, ${roundOff(
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)
previousCodeContent = await page.locator('.cm-content').innerText()
await page.waitForTimeout(100)
await page.mouse.click(700, 300)
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
previousCodeContent = await page.locator('.cm-content').innerText()
await page.waitForTimeout(100)
await page.mouse.click(750, 300)
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
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' })
).not.toBeDisabled()
const startPX = [652, 418]
const lineEndPX = [794, 416]
const arcEndPX = [893, 318]
const startPX = [665, 458]
const lineEndPX = [842, 458]
const arcEndPX = [971, 342]
const dragPX = 30
await page.getByText('startProfileAt([4.61, -14.01], %)').click()
await expect(page.getByRole('button', { name: 'Edit Sketch' })).toBeVisible()
await page.getByRole('button', { name: 'Edit Sketch' }).click()
await page.waitForTimeout(100)
await page.waitForTimeout(400)
let prevContent = await page.locator('.cm-content').innerText()
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.move(startPX[0] + dragPX, startPX[1] - dragPX, step5)
await page.mouse.up()
await page.waitForTimeout(100)
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
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
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt([7.01, -11.79], %)
|> line([14.69, 2.73], %)
|> tangentialArcTo([27.6, -3.25], %)`)
|> startProfileAt([6.44, -12.07], %)
|> line([14.04, 2.03], %)
|> tangentialArcTo([27.19, -4.2], %)`)
})
const doSnapAtDifferentScales = async (
@ -1535,38 +1575,46 @@ test('Sketch on face', async ({ page }) => {
).not.toBeDisabled()
await page.getByRole('button', { name: 'Start Sketch' }).click()
await page.waitForTimeout(300)
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 secondClickPosition = [661, 242]
const thirdClickPosition = [609, 267]
await page.waitForTimeout(300)
await page.mouse.click(firstClickPosition[0], firstClickPosition[1])
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
previousCodeContent = await page.locator('.cm-content').innerText()
await page.waitForTimeout(100)
await page.mouse.click(secondClickPosition[0], secondClickPosition[1])
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
previousCodeContent = await page.locator('.cm-content').innerText()
await page.waitForTimeout(100)
await page.mouse.click(thirdClickPosition[0], thirdClickPosition[1])
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
previousCodeContent = await page.locator('.cm-content').innerText()
await page.waitForTimeout(100)
await page.mouse.click(firstClickPosition[0], firstClickPosition[1])
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
previousCodeContent = await page.locator('.cm-content').innerText()
await expect(page.locator('.cm-content'))
.toContainText(`const part002 = startSketchOn(part001, 'seg01')
|> startProfileAt([1.03, 1.03], %)
|> line([4.18, -0.35], %)
|> line([-4.44, -2.13], %)
|> startProfileAt([-12.83, 6.7], %)
|> line([2.87, -0.23], %)
|> line([-3.05, -1.47], %)
|> close(%)`)
await u.openAndClearDebugPanel()
@ -1576,9 +1624,14 @@ test('Sketch on face', async ({ page }) => {
await u.updateCamPosition([1049, 239, 686])
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 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 u.openAndClearDebugPanel()
await u.updateCamPosition([452, -152, 1166])
@ -1598,11 +1651,11 @@ test('Sketch on face', async ({ page }) => {
await expect(page.locator('.cm-content'))
.toContainText(`const part002 = startSketchOn(part001, 'seg01')
|> startProfileAt([1.03, 1.03], %)
|> line([${process?.env?.CI ? 2.74 : 2.93}, -${
process?.env?.CI ? 0.24 : 0.2
|> startProfileAt([-12.83, 6.7], %)
|> line([${process?.env?.CI ? 2.28 : 2.28}, -${
process?.env?.CI ? 0.07 : 0.07
}], %)
|> line([-4.44, -2.13], %)
|> line([-3.05, -1.47], %)
|> close(%)`)
// exit sketch
@ -1610,7 +1663,7 @@ test('Sketch on face', async ({ page }) => {
await page.getByRole('button', { name: 'Exit Sketch' }).click()
await u.expectCmdLog('[data-message-type="execution-done"]')
await page.getByText('startProfileAt([1.03, 1.03], %)').click()
await page.getByText('startProfileAt([-12.83, 6.7], %)').click()
await expect(page.getByRole('button', { name: 'Extrude' })).not.toBeDisabled()
await page.getByRole('button', { name: 'Extrude' }).click()
@ -1624,11 +1677,11 @@ test('Sketch on face', async ({ page }) => {
await expect(page.locator('.cm-content'))
.toContainText(`const part002 = startSketchOn(part001, 'seg01')
|> startProfileAt([1.03, 1.03], %)
|> line([${process?.env?.CI ? 2.74 : 2.93}, -${
process?.env?.CI ? 0.24 : 0.2
|> startProfileAt([-12.83, 6.7], %)
|> line([${process?.env?.CI ? 2.28 : 2.28}, -${
process?.env?.CI ? 0.07 : 0.07
}], %)
|> line([-4.44, -2.13], %)
|> line([-3.05, -1.47], %)
|> close(%)
|> extrude(5 + 7, %)`)
})
@ -1661,11 +1714,11 @@ test('Can code mod a line length', async ({ page }) => {
// enter sketch again
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
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.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, %)`
)
})
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) {
// wait for 'Loading stream...' spinner
await page.getByTestId('loading-stream').waitFor()
// await page.getByTestId('loading-stream').waitFor()
// wait for all spinners to be gone
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"
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 commit -m "Cut release $new_version"

View File

@ -1,6 +1,6 @@
{
"name": "untitled-app",
"version": "0.17.3",
"version": "0.18.1",
"private": true,
"dependencies": {
"@codemirror/autocomplete": "^6.16.0",
@ -8,7 +8,7 @@
"@fortawesome/free-brands-svg-icons": "^6.5.2",
"@fortawesome/free-solid-svg-icons": "^6.4.2",
"@fortawesome/react-fontawesome": "^0.2.0",
"@headlessui/react": "^1.7.18",
"@headlessui/react": "^1.7.19",
"@headlessui/tailwindcss": "^0.2.0",
"@kittycad/lib": "^0.0.58",
"@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",
"simpleserver:ci": "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-check": "prettier --check ./src && prettier --check ./e2e",
"fmt": "prettier --write ./src *.ts *.json *.js ./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": "(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",
@ -132,6 +132,7 @@
"@types/wicg-file-system-access": "^2023.10.5",
"@types/ws": "^8.5.10",
"@vitejs/plugin-react": "^4.2.1",
"@vitest/web-worker": "^1.5.0",
"@wdio/cli": "^8.24.3",
"@wdio/globals": "^8.36.0",
"@wdio/local-runner": "^8.36.0",

View File

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

8
src-tauri/Cargo.lock generated
View File

@ -2199,9 +2199,9 @@ dependencies = [
[[package]]
name = "kittycad"
version = "0.2.67"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc460442c165c8e707b1154551cefd08938d10bb80c78940e10cd9869487c325"
checksum = "ddc922f0da3abc22661bf49423c9bfcc02ce6ae92dae007ede6990874789545b"
dependencies = [
"anyhow",
"async-trait",
@ -4641,9 +4641,9 @@ dependencies = [
[[package]]
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"
checksum = "c138126392c350aa68554e3461529b02680062c9146ab7b41d3ef97a2deaf93b"
checksum = "609f53d90f08808679ecdd81455d9a4d0053291b92780695569f7400fdba27d5"
dependencies = [
"anyhow",
"glob",

View File

@ -16,13 +16,13 @@ tauri-build = { version = "2.0.0-beta.12", features = [] }
[dependencies]
anyhow = "1"
kittycad = "0.2.67"
kittycad = "0.3.0"
oauth2 = "4.4.2"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tauri = { version = "2.0.0-beta.15", features = [ "devtools", "unstable"] }
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-os = { 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",
"version": "0.17.3"
"version": "0.18.1"
}

View File

@ -193,6 +193,35 @@ export const Toolbar = () => {
Rectangle
</ActionButton>
</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') &&

View File

@ -246,13 +246,31 @@ export class CameraControls {
camSettings.center.y,
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) {
this.camera.fov = camSettings.fov_y
} else if (
this.camera instanceof OrthographicCamera &&
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()
}
@ -965,10 +983,10 @@ export class CameraControls {
// Pure function helpers
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_far = 1000 + nearFarRatio * (100000 - 1000)
return { z_near: 0.1, z_far }
// const z_far = 1000 + nearFarRatio * (100000 - 1000)
return { z_near: 0.1, z_far: 1000 }
}
function convertThreeCamValuesToEngineCam({
@ -1043,3 +1061,62 @@ function _getInteractionType(
if (enableZoom && interactionGuards.zoom.dragCallback(event)) return 'zoom'
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 { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { useStore } from 'useStore'
import { ARROWHEAD, DEBUG_SHOW_BOTH_SCENES } from './sceneInfra'
import { ReactCameraProperties } from './CameraControls'
import { throttle } from 'lib/utils'
@ -47,10 +46,6 @@ export const ClientSideScene = ({
const canvasRef = useRef<HTMLDivElement>(null)
const { state, send, context } = useModelingContext()
const { hideClient, hideServer } = useShouldHideScene()
const { setHighlightRange } = useStore((s) => ({
setHighlightRange: s.setHighlightRange,
highlightRange: s.highlightRange,
}))
// Listen for changes to the camera controls setting
// and update the client-side scene's controls accordingly.
@ -69,7 +64,6 @@ export const ClientSideScene = ({
const canvas = canvasRef.current
canvas.appendChild(sceneInfra.renderer.domElement)
sceneInfra.animate()
sceneInfra.setHighlightCallback(setHighlightRange)
canvas.addEventListener('mousemove', sceneInfra.onMouseMove, false)
canvas.addEventListener('mousedown', sceneInfra.onMouseDown, false)
canvas.addEventListener('mouseup', sceneInfra.onMouseUp, false)

View File

@ -57,6 +57,7 @@ import {
kclManager,
sceneInfra,
codeManager,
editorManager,
} from 'lib/singletons'
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
import { executeAst, useStore } from 'useStore'
@ -96,6 +97,7 @@ import {
getRectangleCallExpressions,
updateRectangleSketch,
} from 'lib/rectangleTool'
import { circleAsCallExpressions, updateCircleSketch } from 'lib/circleTool'
type DraftSegment = 'line' | 'tangentialArcTo'
@ -214,8 +216,9 @@ export class SceneEntities {
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
const baseXColor = 0x000055
const baseYColor = 0x550000
const xAxisGeometry = new BoxGeometry(100000, 0.3, 0.01)
const yAxisGeometry = new BoxGeometry(0.3, 100000, 0.01)
const axisPixelWidth = 1.6
const xAxisGeometry = new BoxGeometry(100000, axisPixelWidth, 0.01)
const yAxisGeometry = new BoxGeometry(axisPixelWidth, 100000, 0.01)
const xAxisMaterial = new MeshBasicMaterial({
color: baseXColor,
depthTest: false,
@ -578,7 +581,7 @@ export class SceneEntities {
...this.mouseEnterLeaveCallbacks(),
})
}
setupRectangleOriginListener = () => {
setupOriginListener = (type: 'circle' | 'rectangle') => {
sceneInfra.setCallbacks({
onClick: (args) => {
const twoD = args.intersectionPoint?.twoD
@ -587,7 +590,7 @@ export class SceneEntities {
return
}
sceneInfra.modelingSend({
type: 'Add rectangle origin',
type: `Add ${type} origin`,
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 = ({
pathToNode,
up,
@ -1323,30 +1474,31 @@ export class SceneEntities {
selected.material.color = defaultPlaneColor(type)
},
onClick: async (args) => {
const checkExtrudeFaceClick = async (): Promise<boolean> => {
const checkExtrudeFaceClick = async (): Promise<
['face' | 'plane' | 'other', string]
> => {
const { streamDimensions } = useStore.getState()
const { entity_id } = await sendSelectEventToEngine(
args?.mouseEvent,
document.getElementById('video-stream') as HTMLVideoElement,
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]
if (artifact?.commandType !== 'solid3d_get_extrusion_face_info')
return false
const faceInfo: Models['FaceIsPlanar_type'] = (
await this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'face_is_planar',
object_id: entity_id,
},
})
)?.data?.data
return ['other', entity_id]
const faceInfo = await getFaceDetails(entity_id)
if (!faceInfo?.origin || !faceInfo?.z_axis || !faceInfo?.y_axis)
return false
const { z_axis, origin, y_axis } = faceInfo
return ['other', entity_id]
const { z_axis, y_axis, origin } = faceInfo
const pathToNode = getNodePathFromSourceRange(
kclManager.ast,
artifact.range
@ -1366,12 +1518,15 @@ export class SceneEntities {
artifact?.additionalData?.type === 'cap'
? artifact.additionalData.info
: '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.mouseEvent.which !== 1) return
@ -1397,6 +1552,7 @@ export class SceneEntities {
plane: planeString,
zAxis,
yAxis,
planeId: faceResult[1],
},
})
},
@ -1423,7 +1579,7 @@ export class SceneEntities {
parent.userData.pathToNode,
'CallExpression'
).node
sceneInfra.highlightCallback([node.start, node.end])
editorManager.setHighlightRange([node.start, node.end])
const yellow = 0xffff00
colorSegment(selected, yellow)
const extraSegmentGroup = parent.getObjectByName(EXTRA_SEGMENT_HANDLE)
@ -1459,10 +1615,10 @@ export class SceneEntities {
}
return
}
sceneInfra.highlightCallback([0, 0])
editorManager.setHighlightRange([0, 0])
},
onMouseLeave: ({ selected, ...rest }: OnMouseEnterLeaveArgs) => {
sceneInfra.highlightCallback([0, 0])
editorManager.setHighlightRange([0, 0])
const parent = getParentGroup(selected, [
STRAIGHT_SEGMENT,
TANGENTIAL_ARC_TO_SEGMENT,
@ -1680,7 +1836,7 @@ export async function getSketchOrientationDetails(
sketchPathToNode: PathToNode
): Promise<{
quat: Quaternion
sketchDetails: SketchDetails
sketchDetails: SketchDetails & { faceId?: string }
}> {
const sketchGroup = sketchGroupFromPathToNode({
pathToNode: sketchPathToNode,
@ -1696,20 +1852,13 @@ export async function getSketchOrientationDetails(
zAxis: [zAxis.x, zAxis.y, zAxis.z],
yAxis: [sketchGroup.yAxis.x, sketchGroup.yAxis.y, sketchGroup.yAxis.z],
origin: [0, 0, 0],
faceId: sketchGroup.on.id,
},
}
}
if (sketchGroup.on.type === 'face') {
const faceInfo: Models['FaceIsPlanar_type'] = (
await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'face_is_planar',
object_id: sketchGroup.on.faceId,
},
})
)?.data?.data
const faceInfo = await getFaceDetails(sketchGroup.on.faceId)
if (!faceInfo?.origin || !faceInfo?.z_axis || !faceInfo?.y_axis)
throw new Error('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],
yAxis: [y_axis.x, y_axis.y, y_axis.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 {
const dummyCam = new PerspectiveCamera()
dummyCam.up.set(0, 0, 1)

View File

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

View File

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

View File

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

View File

@ -61,6 +61,16 @@ const CustomIconMap = {
/>
</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: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path

View File

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

View File

@ -1,13 +1,8 @@
import { undo, redo } from '@codemirror/commands'
import ReactCodeMirror from '@uiw/react-codemirror'
import { TEST } from 'env'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { useConvertToVariable } from 'hooks/useToolbarGuards'
import { Themes, getSystemTheme } from 'lib/theme'
import { useEffect, useMemo, useRef } from 'react'
import { useStore } from 'useStore'
import { processCodeMirrorRanges } from 'lib/selections'
import { useEffect, useMemo } from 'react'
import { highlightSelectionMatches, searchKeymap } from '@codemirror/search'
import { lineHighlightField } from 'editor/highlightextension'
import { roundOff } from 'lib/utils'
@ -21,7 +16,6 @@ import {
EditorView,
dropCursor,
drawSelection,
ViewUpdate,
} from '@codemirror/view'
import {
indentWithTab,
@ -29,7 +23,7 @@ import {
historyKeymap,
history,
} from '@codemirror/commands'
import { lintGutter, lintKeymap, linter } from '@codemirror/lint'
import { lintGutter, lintKeymap } from '@codemirror/lint'
import {
foldGutter,
foldKeymap,
@ -39,25 +33,20 @@ import {
syntaxHighlighting,
defaultHighlightStyle,
} from '@codemirror/language'
import { useModelingContext } from 'hooks/useModelingContext'
import interact from '@replit/codemirror-interact'
import { engineCommandManager, sceneInfra, kclManager } from 'lib/singletons'
import { useKclContext } from 'lang/KclProvider'
import { ModelingMachineEvent } from 'machines/modelingMachine'
import { kclManager, editorManager, codeManager } from 'lib/singletons'
import { useHotkeys } from 'react-hotkeys-hook'
import { isTauri } from 'lib/isTauri'
import { useNavigate } from 'react-router-dom'
import { paths } from 'lib/paths'
import makeUrlPathRelative from 'lib/makeUrlPathRelative'
import { useLspContext } from 'components/LspProvider'
import { Prec, EditorState, Extension, SelectionRange } from '@codemirror/state'
import { Prec, EditorState, Extension } from '@codemirror/state'
import {
closeBrackets,
closeBracketsKeymap,
completionKeymap,
hasNextSnippetField,
} from '@codemirror/autocomplete'
import { kclErrorsToDiagnostics } from 'lang/errors'
export const editorShortcutMeta = {
formatCode: {
@ -77,13 +66,6 @@ export const KclEditorPane = () => {
context.app.theme.current === Themes.System
? getSystemTheme()
: 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 navigate = useNavigate()
@ -96,90 +78,15 @@ export const KclEditorPane = () => {
useHotkeys('mod+z', (e) => {
e.preventDefault()
if (editorView) {
undo(editorView)
}
editorManager.undo()
})
useHotkeys('mod+shift+z', (e) => {
e.preventDefault()
if (editorView) {
redo(editorView)
}
editorManager.redo()
})
const {
context: { selectionRanges },
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 textWrapping = context.textEditor.textWrapping
const cursorBlinking = context.textEditor.blinkingCursor
const editorExtensions = useMemo(() => {
const extensions = [
@ -202,7 +109,7 @@ export const KclEditorPane = () => {
{
key: 'Meta-k',
run: () => {
commandBarSend({ type: 'Open' })
editorManager.commandBarSend({ type: 'Open' })
return false
},
},
@ -216,11 +123,7 @@ export const KclEditorPane = () => {
{
key: editorShortcutMeta.convertToVariable.codeMirror,
run: () => {
if (convertEnabled) {
convertCallback()
return true
}
return false
return editorManager.convertToVariable()
},
},
]),
@ -233,9 +136,6 @@ export const KclEditorPane = () => {
if (!TEST) {
extensions.push(
lintGutter(),
linter((_view: EditorView) => {
return kclErrorsToDiagnostics(errors)
}),
lineNumbers(),
highlightActiveLineGutter(),
highlightSpecialChars(),
@ -288,13 +188,7 @@ export const KclEditorPane = () => {
}
return extensions
}, [
kclLSP,
copilotLSP,
textWrapping.current,
cursorBlinking.current,
convertCallback,
])
}, [kclLSP, copilotLSP, textWrapping.current, cursorBlinking.current])
return (
<div
@ -302,18 +196,15 @@ export const KclEditorPane = () => {
className={'absolute inset-0 ' + (cursorBlinking.current ? 'blink' : '')}
>
<ReactCodeMirror
value={editorCode}
value={codeManager.code}
extensions={editorExtensions}
onUpdate={onUpdate}
theme={theme}
onCreateEditor={(_editorView) => setEditorView(_editorView)}
onCreateEditor={(_editorView) =>
editorManager.setEditorView(_editorView)
}
indentWithTab={false}
basicSetup={false}
/>
</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 { initPromise, parse } from '../../../lang/wasm'
beforeAll(() => initPromise)
beforeAll(async () => {
await initPromise
})
describe('processMemory', () => {
it('should grab the values and remove and geo data', async () => {

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

View File

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

View File

@ -1,4 +1,4 @@
import { useStore } from '../useStore'
import { editorManager } from 'lib/singletons'
import { useEffect } from 'react'
// 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
export function useHotKeyListener() {
const { setIsShiftDown } = useStore((s) => ({
setIsShiftDown: s.setIsShiftDown,
}))
const keyName = 'Shift'
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) =>
event.key === keyName && setIsShiftDown(true)
event.key === keyName && editorManager.setIsShiftDown(true)
const handleKeyUp = (event: KeyboardEvent) =>
event.key === keyName && setIsShiftDown(false)
event.key === keyName && editorManager.setIsShiftDown(false)
window.addEventListener('keydown', handleKeyDown)
window.addEventListener('keyup', handleKeyUp)
return () => {
window.removeEventListener('keydown', handleKeyDown)
window.removeEventListener('keyup', handleKeyUp)
}
}, [setIsShiftDown])
})
}

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import { executeAst } from 'useStore'
import { Selections } from 'lib/selections'
import { KCLError } from './errors'
import { KCLError, kclErrorsToDiagnostics } from './errors'
import { uuidv4 } from 'lib/utils'
import { EngineCommandManager } from './std/engineConnection'
@ -17,7 +17,7 @@ import {
ExtrudeGroup,
} from 'lang/wasm'
import { getNodeFromPath } from './queryAst'
import { codeManager } from 'lib/singletons'
import { codeManager, editorManager } from 'lib/singletons'
export class KclManager {
private _ast: Program = {
@ -90,6 +90,8 @@ export class KclManager {
}
set kclErrors(kclErrors) {
this._kclErrors = kclErrors
let diagnostics = kclErrorsToDiagnostics(kclErrors)
editorManager.setDiagnostics(diagnostics)
this._kclErrorsCallBack(kclErrors)
}
@ -345,6 +347,16 @@ export class KclManager {
void this.engineCommandManager.setPlaneHidden(this.defaultPlanes.yz, 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(

View File

@ -1,7 +1,9 @@
import { KCLError } from './errors'
import { initPromise, parse } from './wasm'
beforeAll(() => initPromise)
beforeAll(async () => {
await initPromise
})
describe('testing AST', () => {
test('5 + 6', () => {

View File

@ -1,7 +1,9 @@
import { parse, initPromise } from './wasm'
import { enginelessExecutor } from '../lib/testHelpers'
beforeAll(() => initPromise)
beforeAll(async () => {
await initPromise
})
describe('testing artifacts', () => {
// Enable rotations #152

View File

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

View File

@ -1,7 +1,9 @@
import { getNodePathFromSourceRange, getNodeFromPath } from './queryAst'
import { Identifier, parse, initPromise, Parameter } from './wasm'
beforeAll(() => initPromise)
beforeAll(async () => {
await initPromise
})
describe('testing getNodePathFromSourceRange', () => {
it('test it gets the right path for a `lineTo` CallExpression within a SketchExpression', () => {

View File

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

View File

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

View File

@ -1,7 +1,9 @@
import { parse, Program, recast, initPromise } from './wasm'
import fs from 'node:fs'
beforeAll(() => initPromise)
beforeAll(async () => {
await initPromise
})
describe('recast', () => {
it('recasts a simple program', () => {
@ -264,9 +266,9 @@ const mySk1 = startSketchAt([0, 0])
|> rx(45, %)
/*
one more for good measure
*/
/*
one more for good measure
*/
`
const { ast } = code2ast(code)
const recasted = recast(ast)
@ -283,7 +285,7 @@ const mySk1 = startSketchAt([0, 0])
// and another with just white space between others below
|> ry(45, %)
|> rx(45, %)
/* one more for good measure */
/* one more for good measure */
`)
})
})

View File

@ -1171,7 +1171,10 @@ export class EngineCommandManager {
type: 'receive-reliable',
data: message,
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(
(callback) => callback(modelingResponse)

View File

@ -25,7 +25,9 @@ const eachQuad: [number, [number, number]][] = [
[675, [1, -1]],
]
beforeAll(() => initPromise)
beforeAll(async () => {
await initPromise
})
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)', () => {
@ -100,7 +102,7 @@ describe('testing changeSketchArguments', () => {
|> startProfileAt([0, 0], %)
|> ${line}
|> lineTo([0.46, -5.82], %)
// |> rx(45, %)
// |> rx(45, %)
`
const code = genCode(lineToChange)
const expectedCode = genCode(lineAfterChange)

View File

@ -8,7 +8,9 @@ import { getSketchSegmentFromSourceRange } from './sketchConstraints'
import { Selection } from 'lib/selections'
import { enginelessExecutor } from '../../lib/testHelpers'
beforeAll(() => initPromise)
beforeAll(async () => {
await initPromise
})
// testing helper function
async function testingSwapSketchFnCall({

View File

@ -11,7 +11,9 @@ import { ToolTip } from '../../useStore'
import { Selections } from 'lib/selections'
import { enginelessExecutor } from '../../lib/testHelpers'
beforeAll(() => initPromise)
beforeAll(async () => {
await initPromise
})
describe('testing getConstraintType', () => {
const helper = getConstraintTypeFromSourceHelper

View File

@ -1,7 +1,9 @@
import { parse, initPromise } from '../wasm'
import { enginelessExecutor } from '../../lib/testHelpers'
beforeAll(() => initPromise)
beforeAll(async () => {
await initPromise
})
describe('testing angledLineThatIntersects', () => {
it('angledLineThatIntersects should intersect with another line', async () => {

View File

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

View File

@ -25,6 +25,7 @@ import { AppInfo } from 'wasm-lib/kcl/bindings/AppInfo'
import { CoreDumpManager } from 'lib/coredump'
import openWindow from 'lib/openWindow'
import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
import { TEST } from 'env'
export type { Program } from '../wasm-lib/kcl/bindings/Program'
export type { Value } from '../wasm-lib/kcl/bindings/Value'
@ -95,10 +96,15 @@ export const wasmUrl = () => {
// Initialise the wasm module.
const initialise = async () => {
const fullUrl = wasmUrl()
const input = await fetch(fullUrl)
const buffer = await input.arrayBuffer()
return init(buffer)
try {
const fullUrl = wasmUrl()
const input = await fetch(fullUrl)
const buffer = await input.arrayBuffer()
return await init(buffer)
} catch (e) {
console.log('Error initialising WASM', e)
throw e
}
}
export const initPromise = initialise()
@ -153,10 +159,6 @@ export const executor = async (
return _programMemory
}
const getSettingsState = import('components/SettingsAuthProvider').then(
(module) => module.getSettingsState
)
export const _executor = async (
node: Program,
programMemory: ProgramMemory = { root: {}, return: null },
@ -164,8 +166,14 @@ export const _executor = async (
isMock: boolean
): Promise<ProgramMemory> => {
try {
const baseUnit =
(await getSettingsState)()?.modeling.defaultUnit.current || 'mm'
let baseUnit = '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(
JSON.stringify(node),
JSON.stringify(programMemory),

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

View File

@ -103,6 +103,7 @@ export const fileLoader: LoaderFunction = async ({
// Update both the state and the editor's code.
// We explicitly do not write to the file here since we are loading from
// the file system and not the editor.
codeManager.updateCurrentFilePath(currentFilePath)
codeManager.updateCodeStateEditor(code)
kclManager.executeCode(true)

View File

@ -1,5 +1,6 @@
import { SceneEntities } from 'clientSideScene/sceneEntities'
import { SceneInfra } from 'clientSideScene/sceneInfra'
import EditorManager from 'editor/manager'
import { KclManager } from 'lang/KclSingleton'
import CodeManager from 'lang/codeManager'
import { EngineCommandManager } from 'lang/std/engineConnection'
@ -8,6 +9,7 @@ export const codeManager = new CodeManager()
export const engineCommandManager = new EngineCommandManager()
// This needs to be after codeManager is created.
export const kclManager = new KclManager(engineCommandManager)
engineCommandManager.getAstCb = () => kclManager.ast
@ -15,3 +17,18 @@ export const sceneInfra = new SceneInfra(engineCommandManager)
engineCommandManager.camControlsCameraChange = sceneInfra.onCameraChange
export const sceneEntitiesManager = new SceneEntities(engineCommandManager)
// This needs to be after sceneInfra and engineCommandManager are is created.
export const editorManager = new EditorManager()
if (typeof window !== 'undefined') {
;(window as any).engineCommandManager = engineCommandManager
;(window as any).kclManager = kclManager
;(window as any).sceneInfra = sceneInfra
;(window as any).sceneEntitiesManager = sceneEntitiesManager
;(window as any).editorManager = editorManager
;(window as any).enableMousePositionLogs = () =>
document.addEventListener('mousemove', (e) =>
console.log(`await page.mouse.click(${e.clientX}, ${e.clientY})`)
)
}

View File

@ -1,7 +1,9 @@
import { Coords2d } from 'lang/std/sketch'
import { isPointsCCW, initPromise } from 'lang/wasm'
beforeAll(() => initPromise)
beforeAll(async () => {
await initPromise
})
describe('test isPointsCW', () => {
test('basic test', () => {

View File

@ -16,6 +16,54 @@ export type CommandBarContext = {
argumentsToSubmit: { [x: string]: unknown }
}
export type CommandBarMachineEvent =
| { type: 'Open' }
| { type: 'Close' }
| { type: 'Clear' }
| {
type: 'Select command'
data: { command: Command }
}
| { type: 'Deselect command' }
| { type: 'Submit command'; data: { [x: string]: unknown } }
| {
type: 'Add argument'
data: { argument: CommandArgumentWithName<unknown> }
}
| {
type: 'Remove argument'
data: { [x: string]: CommandArgumentWithName<unknown> }
}
| {
type: 'Edit argument'
data: { arg: CommandArgumentWithName<unknown> }
}
| {
type: 'Add commands'
data: { commands: Command[] }
}
| {
type: 'Remove commands'
data: { commands: Command[] }
}
| { type: 'Submit argument'; data: { [x: string]: unknown } }
| {
type: 'done.invoke.validateArguments'
data: { [x: string]: unknown }
}
| {
type: 'error.platform.validateArguments'
data: { message: string; arg: CommandArgumentWithName<unknown> }
}
| {
type: 'Find and select command'
data: { name: string; ownerMachine: string }
}
| {
type: 'Change current argument'
data: { [x: string]: CommandArgumentWithName<unknown> }
}
export const commandBarMachine = createMachine(
{
/** @xstate-layout N4IgpgJg5mDOIC5QGED2BbdBDAdhABAEJYBOAxMgDaqxgDaADALqKgAONAlgC6eo6sQAD0QBaAJwA6AGwAmAKwBmBoukAWafIAcDcSoA0IAJ6JZDaZIDs8hgzV6AjA61a1DWQF8PhtJlwFiciowUkYWJBAOWB4+AQiRBFF5CwdpcVkHS1lpVyU5QxNEh1lFGTUsrUUtOQd5SwZLLx8MbDwiUkkqGkgyAHk2MBwwwSiY-kEEswZJbPltM0U3eXLZAsRFeQcrerUHRbTFvfkmkF9WgI6u2ggyADFONv98WkowAGNufDeW-2GI0d443iiHESkktUUilkskqdiUWjWCAcDC0kjUqnElkc6lkoK0JzOT0CnWo1zIAEEIARvn48LA-uwuIC4qAEqIHJjJPJcYtxFoYdJFFjVsZEG5pqCquJxJoGvJxOUCT82sSrj0AEpgdCoABuYC+yog9OYIyZsQmYmhWzMmTqLnU0kyikRGVRbj5lg2SmUam5StpFxIkgAymBXh8HlADQGyKHw58aecGZEzUDWYgchY7CisWoSlpQQ5EVDLJJxKkdIpyypUop-ed2kHCW0Xu9uD1kwDzcCEJCtlUNgWhZZ1OlnaKEBpZJItHVzDZ5xlpPWiZdDc8w22Ow5wozosyLUiVNMSg5ytVKmfrIipzO564z2otPVpI1vKd18SAOJYbgACzAEhI3wUgoAAV3QQZuFgCg-1wGAvjAkgSCgkCSHAyCcG4TtUxZYRED2TQZGSOw80qaQ7ARCc9mmZYSksextGkNJBRXFUOh-f9AOA0CIKgmCABE4E3D5oyTE1-lww8clKBjZF2Bh5SFZwXUUyRVDzJwNE2LQzzYwNJE4gCgJwKNeMw6DJHJAB3LAYlM-AHjYMDuFjMCACN0B4NCMKgnD927dMkWSUs8yY8RISfWQshvcsrDlapshULJPHfZsDKM7iHPM-jJAANSwShOAgX9IzICB+DASQHh1VAAGsqp1Qrit-MByXQvisP8sY8ISZwbCsDllBLSwz1qRFBQsRZ5WRIVkui-TG0M39jJ4jqLNgfLmpK3hTLIQCSFQIM2EoX8ADMjvQSQmqKna2vWvyJL3HrD1SEoyzSdJUkUm9dnUtQn10aK6hkxbiU1HVODAay3M87zE1+J6UwCtN8IQUauR0OZ0ksFwUUqRFPWmUdouPXR3TBjoIahmHKQIHKuqRrtUYSaVShhLF+TkeTzBFQosVKDRM2RVInEdSmg2p6GyE1bU9R8zrsKZqSexFqQHWlHNXHzCbFhnX1+UydFkVxiXJClmGAFEIG8hmld3ZGXp7TR5C5RSCxdjlQV1tR9bqaVdhsSFxDN5AALeOrgPa3ysJgiqcCqmr6sa7bWujxXjQd5nesQZYthsblIQYJx6hUic9AsdEFT2HQzByVLmgDJaw-eSOHPTjbysq6qcFqhrrtT9sO-4ugd1NFGc4QXEPo5eVLGsFxlnKREXGnZI9HcT1mOikO0s-S5w7bqNh9j-bkKOyQTvOy6B9utOHtj7qD1V8pSyrHJZ3STY4QmjQ0R2Ww0oSjuF3u+HAqAIBwEEOlRs48nZBVENkKQNprC42qBoJ0LothaXMJ9aElRXDLj3k3VUpJIBwOfgg4oMx8y41SPUOY8p5DFk2GiKoxsoRVmhGoM2cY2zAQRngChgU0a7HZtFH0ilRp1D5usVh6JXCKD2CUdI8lQ7rlbB8chkkJ6HkxFsBeulbQZAUCwrYCiqzIhyE+fqZtMomTMg-aCwiWYEV2NOBUCghQ6EFGzYsvpwQUT5MxawFECx2JWllRxMdLI2TsrtKMTkXIuMnmeCiZYwrePMFWCKv1XZKVGroWwmxNARK4g4hWG0tp3wSSk16lQpC41ULjV8uxUjSBvFCNEDTdCOkWCY44xCGzgzAJDaGdTnaZHBADYcqg5DpCovzeRno5izA0BeUOh8o5OPgDo+BaNvqV0xNFaE7gdYTnnvkmUChdKEMyF4LwQA */
@ -227,53 +275,7 @@ export const commandBarMachine = createMachine(
},
},
schema: {
events: {} as
| { type: 'Open' }
| { type: 'Close' }
| { type: 'Clear' }
| {
type: 'Select command'
data: { command: Command }
}
| { type: 'Deselect command' }
| { type: 'Submit command'; data: { [x: string]: unknown } }
| {
type: 'Add argument'
data: { argument: CommandArgumentWithName<unknown> }
}
| {
type: 'Remove argument'
data: { [x: string]: CommandArgumentWithName<unknown> }
}
| {
type: 'Edit argument'
data: { arg: CommandArgumentWithName<unknown> }
}
| {
type: 'Add commands'
data: { commands: Command[] }
}
| {
type: 'Remove commands'
data: { commands: Command[] }
}
| { type: 'Submit argument'; data: { [x: string]: unknown } }
| {
type: 'done.invoke.validateArguments'
data: { [x: string]: unknown }
}
| {
type: 'error.platform.validateArguments'
data: { message: string; arg: CommandArgumentWithName<unknown> }
}
| {
type: 'Find and select command'
data: { name: string; ownerMachine: string }
}
| {
type: 'Change current argument'
data: { [x: string]: CommandArgumentWithName<unknown> }
},
events: {} as CommandBarMachineEvent,
},
preserveActionOrder: true,
},

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