Compare commits

...

24 Commits

Author SHA1 Message Date
440eb2636a Cut release v0.21.6 (#2450)
* Cut release v0.21.6

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

* trigger ci

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-05-22 12:05:44 +10:00
344e72d7ec Cam fix (#2448)
* Revert "sketch dies on exit XY sketch (#2397)"

This reverts commit 75c6ae6e66.

* cam fix

* fmt
2024-05-22 01:19:13 +00:00
ec7b733a0d fix project list showing projects of double clicked files (#2441)
* make sure there is at least one kcl file in the dir to show in list

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

* open the correct file not assuming main.kcl

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

* add file path tests

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

* update settings paths

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

* new images

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-05-21 18:14:49 -07:00
63159c1cb8 fix reset settings in browser (#2434)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-05-21 13:33:52 -07:00
df62a995b5 Reset data channel seq every connect (closes #336) (#2431)
Reset inSequence back to 1 every time we connect to the Engine,
otherwise we'll continue to think the current sequence is a high number
(while the engine has no memory of the last session -- and may even be a
new engine instance!) and ignore messages until we pass that counter
again.

Signed-off-by: Paul Tagliamonte <paul@zoo.dev>
2024-05-21 14:52:14 -04:00
fa762c1c4d throw error on both ranges (#2428)
* highlight both ranges

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

* add playwright test

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-05-21 00:49:57 -07:00
82b03a9d47 Add stdlib functions for getting sketch profile start and its components (#2373)
* Add stdlib functions for getting sketch profile start and its components

* Fix it up and actually generate snapshots

* cargo fmt

* Use `.to` instead of `.from`

* Update docs with EXPECTORATE=overwrite

* Add README

* fmt

* Update flow test to account for more autocompletion options when typing "start"

---------

Co-authored-by: Jess Frazelle <jessfraz@users.noreply.github.com>
2024-05-21 00:44:02 -07:00
793b7407f6 add TS ast walker (#2425) 2024-05-21 16:44:08 +10:00
040bcc2c09 bump zip (#2423)
* bump zip

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

* rerender images

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

* fix markdown

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-05-21 05:07:56 +00:00
ae2e219394 fix empty tag on sketch on face (#2424)
add test

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-05-20 21:59:56 -07:00
a83f549257 Bump @tauri-apps/plugin-fs from 2.0.0-beta.2 to 2.0.0-beta.3 (#2403)
Bumps @tauri-apps/plugin-fs from 2.0.0-beta.2 to 2.0.0-beta.3.

---
updated-dependencies:
- dependency-name: "@tauri-apps/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-05-20 21:37:27 -07:00
3871d2858f Bump @tauri-apps/plugin-updater from 2.0.0-beta.2 to 2.0.0-beta.3 (#2404)
Bumps @tauri-apps/plugin-updater from 2.0.0-beta.2 to 2.0.0-beta.3.

---
updated-dependencies:
- dependency-name: "@tauri-apps/plugin-updater"
  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-05-20 21:37:13 -07:00
3effb87f8e Bump react-router-dom from 6.22.3 to 6.23.1 (#2405)
Bumps [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) from 6.22.3 to 6.23.1.
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-dom/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router-dom@6.23.1/packages/react-router-dom)

---
updated-dependencies:
- dependency-name: react-router-dom
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-20 21:36:57 -07:00
3f2f035a9b Bump three from 0.163.0 to 0.164.1 (#2406)
Bumps [three](https://github.com/mrdoob/three.js) from 0.163.0 to 0.164.1.
- [Release notes](https://github.com/mrdoob/three.js/releases)
- [Commits](https://github.com/mrdoob/three.js/commits)

---
updated-dependencies:
- dependency-name: three
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-20 21:36:47 -07:00
4735eaef8c Bump vitest from 1.5.0 to 1.6.0 (#2402)
Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 1.5.0 to 1.6.0.
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v1.6.0/packages/vitest)

---
updated-dependencies:
- dependency-name: vitest
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-20 21:36:35 -07:00
69f8da058a add tests for min and max (#2420)
* add tests

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

* Update src/wasm-lib/kcl/src/std/math.rs

Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>

* fmt

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

* add another

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>
2024-05-20 21:14:00 -07:00
93ebf13621 Wrapper for keybindings (codemirror and app) (#2421)
* start

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

* add hotkey wrapper

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

* updates

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-05-20 20:52:33 -07:00
20c4d44b8b Cut release v0.21.5 (#2418) 2024-05-21 10:35:10 +10:00
8ea8f80e32 Sort keys alphabetically in the Variables/Memory panel (#2417) 2024-05-20 17:28:51 -05:00
d73339fd8d Edit mode prep (#2370)
* remove edit mode

* small rename

* update with compat with old edit mode

* exit edit mode still needed

* add log to export
2024-05-21 05:55:34 +10:00
031b230690 Disable home logo link in the browser app (#2371)
* Disable logo link in browser, clean up menu components' organization

* Include the app name in Tauri as screenreader-only text

* Fix tsc issue in component test

* Add a playwright test to ensure logo is never a link in the browser

* Update broken component tests
2024-05-20 14:59:59 -04:00
1125d74f12 Update serde (#2415) 2024-05-20 18:01:48 +00:00
5c7a2822d0 Add some JSDocs comments to types, convert some existing comments. (#2363)
* Add some JSDocs comments to types, convert some existing comments.

* Add JSDoc comments to EngineCommandManager

* Add deprecation message to `lastArtifactMap`

* fmt

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

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

* Rerun CI

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-05-20 13:38:51 -04:00
d44b1f8e54 Fix code editor user input in older version of Safari (#2350)
Fix where code editor wasn't allowing code insertion in older versions of Safari.

Co-authored-by: Jess Frazelle <jessfraz@users.noreply.github.com>
2024-05-20 11:28:34 -04:00
91 changed files with 4810 additions and 295 deletions

View File

@ -59,7 +59,9 @@ followed by:
``` ```
yarn build:wasm-dev yarn build:wasm-dev
``` ```
or if you have the gh cli installed or if you have the gh cli installed
``` ```
./get-latest-wasm-bundle.sh # this will download the latest main wasm bundle ./get-latest-wasm-bundle.sh # this will download the latest main wasm bundle
``` ```
@ -100,6 +102,7 @@ yarn test
Which will run our suite of [Vitest unit](https://vitest.dev/) and [React Testing Library E2E](https://testing-library.com/docs/react-testing-library/intro/) tests, in interactive mode by default. Which will run our suite of [Vitest unit](https://vitest.dev/) and [React Testing Library E2E](https://testing-library.com/docs/react-testing-library/intro/) tests, in interactive mode by default.
For running the rust (not tauri rust though) only, you can For running the rust (not tauri rust though) only, you can
```bash ```bash
cd src/wasm-lib cd src/wasm-lib
cargo test cargo test
@ -162,6 +165,7 @@ console.log(
- `) - `)
) )
``` ```
grab the md list and delete any that are older than the last bump grab the md list and delete any that are older than the last bump
2. Merge the PR 2. Merge the PR
@ -191,23 +195,26 @@ $ cargo +nightly fuzz run parser
For more information on fuzzing you can check out For more information on fuzzing you can check out
[this guide](https://rust-fuzz.github.io/book/cargo-fuzz.html). [this guide](https://rust-fuzz.github.io/book/cargo-fuzz.html).
### Playwright ### Playwright
First time running plawright locally, you'll need to add the secrets file First time running plawright locally, you'll need to add the secrets file
```bash ```bash
touch ./e2e/playwright/playwright-secrets.env touch ./e2e/playwright/playwright-secrets.env
printf 'token="your-token"\nsnapshottoken="your-snapshot-token"' > ./e2e/playwright/playwright-secrets.env printf 'token="your-token"\nsnapshottoken="your-snapshot-token"' > ./e2e/playwright/playwright-secrets.env
``` ```
then replace "your-token" with a dev token from dev.zoo.dev/account/api-tokens then replace "your-token" with a dev token from dev.zoo.dev/account/api-tokens
then: then:
run playwright run playwright
``` ```
yarn playwright test yarn playwright test
``` ```
run a specific test suite run a specific test suite
``` ```
yarn playwright test src/e2e-tests/example.spec.ts yarn playwright test src/e2e-tests/example.spec.ts
``` ```
@ -216,14 +223,17 @@ run a specific test change the test from `test('...` to `test.only('...`
(note if you commit this, the tests will instantly fail without running any of the tests) (note if you commit this, the tests will instantly fail without running any of the tests)
run headed run headed
``` ```
yarn playwright test --headed yarn playwright test --headed
``` ```
run with step through debugger run with step through debugger
``` ```
PWDEBUG=1 yarn playwright test PWDEBUG=1 yarn playwright test
``` ```
However, if you want a debugger I recommend using VSCode and the `playwright` extension, as the above command is a cruder debugger that steps into every function call which is annoying. However, if you want a debugger I recommend using VSCode and the `playwright` extension, as the above command is a cruder debugger that steps into every function call which is annoying.
With the extension you can set a breakpoint after `waitForDefaultPlanesVisibilityChange` in order to skip app loading, then the vscode debugger's "step over" is much better for being able to stay at the right level of abstraction as you debug the code. With the extension you can set a breakpoint after `waitForDefaultPlanesVisibilityChange` in order to skip app loading, then the vscode debugger's "step over" is much better for being able to stay at the right level of abstraction as you debug the code.
@ -268,12 +278,11 @@ Where `./store` should look like this
</details> </details>
However because much of our tests involve clicking in the stream at specific locations, it's code-gen looks `await page.locator('video').click();` when really we need to use a pixel coord, so I think it's of limited use. However because much of our tests involve clicking in the stream at specific locations, it's code-gen looks `await page.locator('video').click();` when really we need to use a pixel coord, so I think it's of limited use.
#### Some notes on CI #### Some notes on CI
The tests are broken into snapshot tests and non-snapshot tests, and they run in that order, they automatically commit new snap shots, so if you see an image commit check it was an intended change. If we have non-determinism in the snapshots such that they are always committing new images, hopefully this annoyance makes us fix them asap, if you notice this happening let Kurt know. But for the odd occasion `git reset --hard HEAD~ && git push -f` is your friend. The tests are broken into snapshot tests and non-snapshot tests, and they run in that order, they automatically commit new snap shots, so if you see an image commit check it was an intended change. If we have non-determinism in the snapshots such that they are always committing new images, hopefully this annoyance makes us fix them asap, if you notice this happening let Kurt know. But for the odd occasion `git reset --hard HEAD~ && git push -f` is your friend.
How to interpret failing playwright tests? How to interpret failing playwright tests?
If your tests fail, click through to the action and see that the tests failed on a line that includes `await page.getByTestId('loading').waitFor({ state: 'detached' })`, this means the test fail because the stream never started. It's you choice if you want to re-run the test, or ignore the failure. If your tests fail, click through to the action and see that the tests failed on a line that includes `await page.getByTestId('loading').waitFor({ state: 'detached' })`, this means the test fail because the stream never started. It's you choice if you want to re-run the test, or ignore the failure.
@ -299,3 +308,7 @@ PS: for the debug panel, the following JSON is useful for snapping the camera
``` ```
</details> </details>
## KCL
For how to contribute to KCL, [see our KCL README](https://github.com/KittyCAD/modeling-app/tree/main/src/wasm-lib/kcl).

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

@ -56,6 +56,9 @@ layout: manual
* [`patternLinear3d`](kcl/patternLinear3d) * [`patternLinear3d`](kcl/patternLinear3d)
* [`pi`](kcl/pi) * [`pi`](kcl/pi)
* [`pow`](kcl/pow) * [`pow`](kcl/pow)
* [`profileStart`](kcl/profileStart)
* [`profileStartX`](kcl/profileStartX)
* [`profileStartY`](kcl/profileStartY)
* [`revolve`](kcl/revolve) * [`revolve`](kcl/revolve)
* [`segAng`](kcl/segAng) * [`segAng`](kcl/segAng)
* [`segEndX`](kcl/segEndX) * [`segEndX`](kcl/segEndX)

File diff suppressed because one or more lines are too long

206
docs/kcl/profileStart.md Normal file

File diff suppressed because one or more lines are too long

201
docs/kcl/profileStartX.md Normal file

File diff suppressed because one or more lines are too long

200
docs/kcl/profileStartY.md Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -348,6 +348,15 @@ test('if you use the format keyboard binding it formats your code', async ({
|> close(%)`) |> close(%)`)
}) })
test('ensure the Zoo logo is not a link in browser app', async ({ page }) => {
await page.setViewportSize({ width: 1000, height: 500 })
await page.goto('/')
const zooLogo = page.locator('[data-testid="app-logo"]')
// Make sure it's not a link
await expect(zooLogo).not.toHaveAttribute('href')
})
test('if you write invalid kcl you get inlined errors', async ({ page }) => { test('if you write invalid kcl you get inlined errors', async ({ page }) => {
const u = getUtils(page) const u = getUtils(page)
await page.setViewportSize({ width: 1000, height: 500 }) await page.setViewportSize({ width: 1000, height: 500 })
@ -415,6 +424,80 @@ test('if you write invalid kcl you get inlined errors', async ({ page }) => {
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
}) })
test('error with 2 source ranges gets 2 diagnostics', async ({ page }) => {
const u = getUtils(page)
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`const length = .750
const width = 0.500
const height = 0.500
const dia = 4
fn squareHole = (l, w) => {
const squareHoleSketch = startSketchOn('XY')
|> startProfileAt([-width / 2, -length / 2], %)
|> lineTo([width / 2, -length / 2], %)
|> lineTo([width / 2, length / 2], %)
|> lineTo([-width / 2, length / 2], %)
|> close(%)
return squareHoleSketch
}
`
)
})
await page.setViewportSize({ width: 1000, height: 500 })
await page.goto('/')
const lspStartPromise = page.waitForEvent('console', async (message) => {
// it would be better to wait for a message that the kcl lsp has started by looking for the message message.text().includes('[lsp] [window/logMessage]')
// but that doesn't seem to make it to the console for macos/safari :(
if (message.text().includes('start kcl lsp')) {
await new Promise((resolve) => setTimeout(resolve, 200))
return true
}
return false
})
await page.goto('/')
await u.waitForAuthSkipAppStart()
await lspStartPromise
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
// check no error to begin with
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
// Click on the bottom of the code editor to add a new line
await page.click('.cm-content')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('Enter')
await page.keyboard.type(`const extrusion = startSketchOn('XY')
|> circle([0, 0], dia/2, %)
|> hole(squareHole(length, width, height), %)
|> extrude(height, %)`)
// error in gutter
await expect(page.locator('.cm-lint-marker-error').first()).toBeVisible()
await page.hover('.cm-lint-marker-error:first-child')
await expect(page.getByText('Expected 2 arguments, got 3')).toBeVisible()
// Make sure there are two diagnostics
await expect(page.locator('.cm-lint-marker-error')).toHaveCount(2)
})
test('if your kcl gets an error from the engine it is inlined', async ({ test('if your kcl gets an error from the engine it is inlined', async ({
page, page,
}) => { }) => {
@ -637,8 +720,8 @@ test('Auto complete works', async ({ page }) => {
await page.click('.cm-content') await page.click('.cm-content')
await page.keyboard.type('const part001 = start') await page.keyboard.type('const part001 = start')
// expect there to be three auto complete options // expect there to be six auto complete options
await expect(page.locator('.cm-completionLabel')).toHaveCount(3) await expect(page.locator('.cm-completionLabel')).toHaveCount(6)
await page.getByText('startSketchOn').click() await page.getByText('startSketchOn').click()
await page.keyboard.type("'XZ'") await page.keyboard.type("'XZ'")
await page.keyboard.press('Tab') await page.keyboard.press('Tab')
@ -766,6 +849,130 @@ test('Project settings can be set and override user settings', async ({
await expect(page.locator('select[name="app-theme"]')).toHaveValue('light') await expect(page.locator('select[name="app-theme"]')).toHaveValue('light')
}) })
test('Project settings can be opened with keybinding from the editor', async ({
page,
}) => {
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/', { waitUntil: 'domcontentloaded' })
await page
.getByRole('button', { name: 'Start Sketch' })
.waitFor({ state: 'visible' })
// Put the cursor in the editor
await page.click('.cm-content')
// Open the settings modal with the browser keyboard shortcut
await page.keyboard.press('Meta+Shift+,')
await expect(
page.getByRole('heading', { name: 'Settings', exact: true })
).toBeVisible()
await page
.locator('select[name="app-theme"]')
.selectOption({ value: 'light' })
// Verify the toast appeared
await expect(
page.getByText(`Set theme to "light" for this project`)
).toBeVisible()
// Check that the theme changed
await expect(page.locator('body')).not.toHaveClass(`body-bg dark`)
// Check that the user setting was not changed
await page.getByRole('radio', { name: 'User' }).click()
await expect(page.locator('select[name="app-theme"]')).toHaveValue('dark')
// Roll back to default "system" theme
await page
.getByText(
'themeRoll back themeRoll back to match defaultThe overall appearance of the appl'
)
.hover()
await page
.getByRole('button', {
name: 'Roll back theme ; Has tooltip: Roll back to match default',
})
.click()
await expect(page.locator('select[name="app-theme"]')).toHaveValue('system')
// Check that the project setting did not change
await page.getByRole('radio', { name: 'Project' }).click()
await expect(page.locator('select[name="app-theme"]')).toHaveValue('light')
})
test('Project and user settings can be reset', async ({ page }) => {
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/', { waitUntil: 'domcontentloaded' })
await page
.getByRole('button', { name: 'Start Sketch' })
.waitFor({ state: 'visible' })
// Put the cursor in the editor
await page.click('.cm-content')
// Open the settings modal with the browser keyboard shortcut
await page.keyboard.press('Meta+Shift+,')
await expect(
page.getByRole('heading', { name: 'Settings', exact: true })
).toBeVisible()
// Click the reset settings button.
await page.getByRole('button', { name: 'Restore default settings' }).click()
await page
.locator('select[name="app-theme"]')
.selectOption({ value: 'light' })
// Verify the toast appeared
await expect(
page.getByText(`Set theme to "light" for this project`)
).toBeVisible()
// Check that the theme changed
await expect(page.locator('body')).not.toHaveClass(`body-bg dark`)
await expect(page.locator('select[name="app-theme"]')).toHaveValue('light')
// Check that the user setting was not changed
await page.getByRole('radio', { name: 'User' }).click()
await expect(page.locator('select[name="app-theme"]')).toHaveValue('system')
// Click the reset settings button.
await page.getByRole('button', { name: 'Restore default settings' }).click()
// Verify it is now set to the default value
await expect(page.locator('select[name="app-theme"]')).toHaveValue('system')
// Set the user theme to light.
await page
.locator('select[name="app-theme"]')
.selectOption({ value: 'light' })
// Verify the toast appeared
await expect(
page.getByText(`Set theme to "light" as a user default`)
).toBeVisible()
// Check that the theme changed
await expect(page.locator('body')).not.toHaveClass(`body-bg dark`)
await expect(page.locator('select[name="app-theme"]')).toHaveValue('light')
await page.getByRole('radio', { name: 'Project' }).click()
await expect(page.locator('select[name="app-theme"]')).toHaveValue('light')
// Click the reset settings button.
await page.getByRole('button', { name: 'Restore default settings' }).click()
// Verify it is now set to the default value
await expect(page.locator('select[name="app-theme"]')).toHaveValue('system')
await page.getByRole('radio', { name: 'User' }).click()
await expect(page.locator('select[name="app-theme"]')).toHaveValue('system')
// Click the reset settings button.
await page.getByRole('button', { name: 'Restore default settings' }).click()
// Verify it is now set to the default value
await expect(page.locator('select[name="app-theme"]')).toHaveValue('system')
})
test('Click through each onboarding step', async ({ page }) => { test('Click through each onboarding step', async ({ page }) => {
const u = getUtils(page) const u = getUtils(page)
@ -1013,7 +1220,6 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
// wait for execution done // wait for execution done
await u.expectCmdLog('[data-message-type="execution-done"]') await u.expectCmdLog('[data-message-type="execution-done"]')
await u.updateCamPosition([0, -1378.01, 0.06])
await u.closeDebugPanel() await u.closeDebugPanel()
// select a line // select a line
@ -1084,6 +1290,51 @@ test.describe('Command bar tests', () => {
await expect(page.locator('body')).not.toHaveClass(`body-bg dark`) await expect(page.locator('body')).not.toHaveClass(`body-bg dark`)
}) })
test('Command bar keybinding works from code editor and can change a setting', async ({
page,
}) => {
// Brief boilerplate
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/', { waitUntil: 'domcontentloaded' })
let cmdSearchBar = page.getByPlaceholder('Search commands')
// Put the cursor in the code editor
await page.click('.cm-content')
// Now try the same, but with the keyboard shortcut, check focus
await page.keyboard.press('Meta+K')
await expect(cmdSearchBar).toBeVisible()
await expect(cmdSearchBar).toBeFocused()
// Try typing in the command bar
await page.keyboard.type('theme')
const themeOption = page.getByRole('option', {
name: 'Settings · app · theme',
})
await expect(themeOption).toBeVisible()
await themeOption.click()
const themeInput = page.getByPlaceholder('Select an option')
await expect(themeInput).toBeVisible()
await expect(themeInput).toBeFocused()
// Select dark theme
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await expect(page.getByRole('option', { name: 'system' })).toHaveAttribute(
'data-headlessui-state',
'active'
)
await page.keyboard.press('Enter')
// Check the toast appeared
await expect(
page.getByText(`Set theme to "system" for this project`)
).toBeVisible()
// Check that the theme changed
await expect(page.locator('body')).not.toHaveClass(`body-bg dark`)
})
test('Can extrude from the command bar', async ({ page }) => { test('Can extrude from the command bar', async ({ page }) => {
await page.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(
@ -1245,7 +1496,6 @@ test('Can add multiple sketches', async ({ page }) => {
await u.clearCommandLogs() await u.clearCommandLogs()
await page.getByRole('button', { name: 'Start Sketch' }).click() await page.getByRole('button', { name: 'Start Sketch' }).click()
await page.waitForTimeout(400) await page.waitForTimeout(400)
await u.updateCamPosition([583, 2000, 370])
await page.mouse.click(650, 450) await page.mouse.click(650, 450)
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
@ -1259,7 +1509,8 @@ test('Can add multiple sketches', async ({ page }) => {
await page.waitForTimeout(100) await page.waitForTimeout(100)
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
const startAt2 = '[22.65, -30.57]' const startAt2 =
process.platform === 'darwin' ? '[9.75, -13.16]' : '[0.93, -1.25]'
await expect( await expect(
(await page.locator('.cm-content').innerText()).replace(/\s/g, '') (await page.locator('.cm-content').innerText()).replace(/\s/g, '')
).toBe( ).toBe(
@ -1273,7 +1524,7 @@ const part002 = startSketchOn('${plane}')
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await page.waitForTimeout(100) await page.waitForTimeout(100)
const num2 = 22.87 const num2 = process.platform === 'darwin' ? 9.84 : 0.94
await expect( await expect(
(await page.locator('.cm-content').innerText()).replace(/\s/g, '') (await page.locator('.cm-content').innerText()).replace(/\s/g, '')
).toBe( ).toBe(
@ -1291,7 +1542,9 @@ const part002 = startSketchOn('${plane}')
const part002 = startSketchOn('${plane}') const part002 = startSketchOn('${plane}')
|> startProfileAt(${startAt2}, %) |> startProfileAt(${startAt2}, %)
|> line([${num2}, 0], %) |> line([${num2}, 0], %)
|> line([0, ${roundOff(num2)}], %)`.replace(/\s/g, '') |> line([0, ${roundOff(
num2 + (process.platform === 'darwin' ? 0.01 : -0.01)
)}], %)`.replace(/\s/g, '')
) )
await page.waitForTimeout(100) await page.waitForTimeout(100)
await page.mouse.click(startXPx, 500 - PUR * 20) await page.mouse.click(startXPx, 500 - PUR * 20)
@ -1302,8 +1555,13 @@ const part002 = startSketchOn('${plane}')
const part002 = startSketchOn('${plane}') const part002 = startSketchOn('${plane}')
|> startProfileAt(${startAt2}, %) |> startProfileAt(${startAt2}, %)
|> line([${num2}, 0], %) |> line([${num2}, 0], %)
|> line([0, ${roundOff(num2)}], %) |> line([0, ${roundOff(
|> line([-45.52, 0], %)`.replace(/\s/g, '') num2 + (process.platform === 'darwin' ? 0.01 : -0.01)
)}], %)
|> line([-${process.platform === 'darwin' ? 19.59 : 1.87}, 0], %)`.replace(
/\s/g,
''
)
) )
}) })

View File

@ -273,6 +273,8 @@ const part001 = startSketchOn('-XZ')
for (let { modelPath, imagePath, outputType } of exportLocations) { for (let { modelPath, imagePath, outputType } of exportLocations) {
// May change depending on the file being dealt with // May change depending on the file being dealt with
let cliCommand = `export ZOO_TOKEN=${secrets.snapshottoken} && zoo file snapshot --output-format=png --src-format=${outputType} ${modelPath} ${imagePath}` let cliCommand = `export ZOO_TOKEN=${secrets.snapshottoken} && zoo file snapshot --output-format=png --src-format=${outputType} ${modelPath} ${imagePath}`
const fileSize = (await fsp.stat(modelPath)).size
console.log(`Size of the file at ${modelPath}: ${fileSize} bytes`)
const parentPath = path.dirname(modelPath) const parentPath = path.dirname(modelPath)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 41 KiB

View File

@ -1,6 +1,6 @@
{ {
"name": "untitled-app", "name": "untitled-app",
"version": "0.21.4", "version": "0.21.6",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@codemirror/autocomplete": "^6.16.0", "@codemirror/autocomplete": "^6.16.0",
@ -17,12 +17,12 @@
"@replit/codemirror-interact": "^6.3.1", "@replit/codemirror-interact": "^6.3.1",
"@tauri-apps/api": "2.0.0-beta.8", "@tauri-apps/api": "2.0.0-beta.8",
"@tauri-apps/plugin-dialog": "^2.0.0-beta.2", "@tauri-apps/plugin-dialog": "^2.0.0-beta.2",
"@tauri-apps/plugin-fs": "^2.0.0-beta.2", "@tauri-apps/plugin-fs": "^2.0.0-beta.3",
"@tauri-apps/plugin-http": "^2.0.0-beta.2", "@tauri-apps/plugin-http": "^2.0.0-beta.2",
"@tauri-apps/plugin-os": "^2.0.0-beta.2", "@tauri-apps/plugin-os": "^2.0.0-beta.2",
"@tauri-apps/plugin-process": "^2.0.0-beta.2", "@tauri-apps/plugin-process": "^2.0.0-beta.2",
"@tauri-apps/plugin-shell": "^2.0.0-beta.2", "@tauri-apps/plugin-shell": "^2.0.0-beta.2",
"@tauri-apps/plugin-updater": "^2.0.0-beta.2", "@tauri-apps/plugin-updater": "^2.0.0-beta.3",
"@testing-library/jest-dom": "^5.14.1", "@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^15.0.2", "@testing-library/react": "^15.0.2",
"@testing-library/user-event": "^14.5.2", "@testing-library/user-event": "^14.5.2",
@ -52,15 +52,15 @@
"react-json-view": "^1.21.3", "react-json-view": "^1.21.3",
"react-modal": "^3.16.1", "react-modal": "^3.16.1",
"react-modal-promise": "^1.0.2", "react-modal-promise": "^1.0.2",
"react-router-dom": "^6.22.3", "react-router-dom": "^6.23.1",
"sketch-helpers": "^0.0.4", "sketch-helpers": "^0.0.4",
"swr": "^2.2.5", "swr": "^2.2.5",
"three": "^0.163.0", "three": "^0.164.1",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "^5.4.5", "typescript": "^5.4.5",
"ua-parser-js": "^1.0.37", "ua-parser-js": "^1.0.37",
"uuid": "^9.0.1", "uuid": "^9.0.1",
"vitest": "^1.5.0", "vitest": "^1.6.0",
"vscode-jsonrpc": "^8.1.0", "vscode-jsonrpc": "^8.1.0",
"vscode-languageserver-protocol": "^3.17.5", "vscode-languageserver-protocol": "^3.17.5",
"wasm-pack": "^0.12.1", "wasm-pack": "^0.12.1",

49
src-tauri/Cargo.lock generated
View File

@ -212,6 +212,15 @@ dependencies = [
"num-traits", "num-traits",
] ]
[[package]]
name = "arbitrary"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110"
dependencies = [
"derive_arbitrary",
]
[[package]] [[package]]
name = "arrayvec" name = "arrayvec"
version = "0.7.4" version = "0.7.4"
@ -1207,6 +1216,17 @@ dependencies = [
"syn 2.0.65", "syn 2.0.65",
] ]
[[package]]
name = "derive_arbitrary"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.65",
]
[[package]] [[package]]
name = "derive_more" name = "derive_more"
version = "0.99.17" version = "0.99.17"
@ -1258,6 +1278,17 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b"
[[package]]
name = "displaydoc"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.65",
]
[[package]] [[package]]
name = "dlib" name = "dlib"
version = "0.5.2" version = "0.5.2"
@ -2579,7 +2610,7 @@ dependencies = [
"wasm-bindgen-futures", "wasm-bindgen-futures",
"web-sys", "web-sys",
"winnow 0.5.40", "winnow 0.5.40",
"zip", "zip 1.3.0",
] ]
[[package]] [[package]]
@ -5471,7 +5502,7 @@ dependencies = [
"tokio", "tokio",
"url", "url",
"windows-sys 0.52.0", "windows-sys 0.52.0",
"zip", "zip 0.6.6",
] ]
[[package]] [[package]]
@ -7123,6 +7154,20 @@ dependencies = [
"zstd", "zstd",
] ]
[[package]]
name = "zip"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1f4a27345eb6f7aa7bd015ba7eb4175fa4e1b462a29874b779e0bbcf96c6ac7"
dependencies = [
"arbitrary",
"crc32fast",
"crossbeam-utils",
"displaydoc",
"indexmap 2.2.6",
"thiserror",
]
[[package]] [[package]]
name = "zstd" name = "zstd"
version = "0.11.2+zstd.1.5.2" version = "0.11.2+zstd.1.5.2"

View File

@ -121,11 +121,8 @@ async fn write_app_settings_file(app: tauri::AppHandle, configuration: Configura
Ok(()) Ok(())
} }
async fn get_project_settings_file_path( async fn get_project_settings_file_path(project_path: &str) -> Result<PathBuf, InvokeError> {
app_settings: Configuration, let project_dir = std::path::Path::new(project_path);
project_name: &str,
) -> Result<PathBuf, InvokeError> {
let project_dir = app_settings.settings.project.directory.join(project_name);
if !project_dir.exists() { if !project_dir.exists() {
tokio::fs::create_dir_all(&project_dir) tokio::fs::create_dir_all(&project_dir)
@ -137,11 +134,8 @@ async fn get_project_settings_file_path(
} }
#[tauri::command] #[tauri::command]
async fn read_project_settings_file( async fn read_project_settings_file(project_path: &str) -> Result<ProjectConfiguration, InvokeError> {
app_settings: Configuration, let settings_path = get_project_settings_file_path(project_path).await?;
project_name: &str,
) -> Result<ProjectConfiguration, InvokeError> {
let settings_path = get_project_settings_file_path(app_settings, project_name).await?;
// Check if this file exists. // Check if this file exists.
if !settings_path.exists() { if !settings_path.exists() {
@ -159,11 +153,10 @@ async fn read_project_settings_file(
#[tauri::command] #[tauri::command]
async fn write_project_settings_file( async fn write_project_settings_file(
app_settings: Configuration, project_path: &str,
project_name: &str,
configuration: ProjectConfiguration, configuration: ProjectConfiguration,
) -> Result<(), InvokeError> { ) -> Result<(), InvokeError> {
let settings_path = get_project_settings_file_path(app_settings, project_name).await?; let settings_path = get_project_settings_file_path(project_path).await?;
let contents = toml::to_string_pretty(&configuration).map_err(|e| InvokeError::from_anyhow(e.into()))?; let contents = toml::to_string_pretty(&configuration).map_err(|e| InvokeError::from_anyhow(e.into()))?;
tokio::fs::write(settings_path, contents.as_bytes()) tokio::fs::write(settings_path, contents.as_bytes())
.await .await

View File

@ -74,5 +74,5 @@
} }
}, },
"productName": "Zoo Modeling App", "productName": "Zoo Modeling App",
"version": "0.21.4" "version": "0.21.6"
} }

View File

@ -23,6 +23,7 @@ import { useRefreshSettings } from 'hooks/useRefreshSettings'
import { ModelingSidebar } from 'components/ModelingSidebar/ModelingSidebar' import { ModelingSidebar } from 'components/ModelingSidebar/ModelingSidebar'
import { LowerRightControls } from 'components/LowerRightControls' import { LowerRightControls } from 'components/LowerRightControls'
import ModalContainer from 'react-modal-promise' import ModalContainer from 'react-modal-promise'
import useHotkeyWrapper from 'lib/hotkeyWrapper'
export function App() { export function App() {
useRefreshSettings(paths.FILE + 'SETTINGS') useRefreshSettings(paths.FILE + 'SETTINGS')
@ -63,8 +64,8 @@ export function App() {
useHotkeys('backspace', (e) => { useHotkeys('backspace', (e) => {
e.preventDefault() e.preventDefault()
}) })
useHotkeys( useHotkeyWrapper(
isTauri() ? 'mod + ,' : 'shift + mod + ,', [isTauri() ? 'mod + ,' : 'shift + mod + ,'],
() => navigate(filePath + paths.SETTINGS), () => navigate(filePath + paths.SETTINGS),
{ {
splitKey: '|', splitKey: '|',

View File

@ -34,7 +34,7 @@ export const AppHeader = ({
} }
> >
<ProjectSidebarMenu <ProjectSidebarMenu
renderAsLink={!enableMenu} enableMenu={enableMenu}
project={project?.project} project={project?.project}
file={project?.file} file={project?.file}
/> />

View File

@ -1,11 +1,11 @@
import { Dialog, Popover, Transition } from '@headlessui/react' import { Dialog, Popover, Transition } from '@headlessui/react'
import { Fragment, useEffect } from 'react' import { Fragment, useEffect } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
import { useCommandsContext } from 'hooks/useCommandsContext' import { useCommandsContext } from 'hooks/useCommandsContext'
import CommandBarArgument from './CommandBarArgument' import CommandBarArgument from './CommandBarArgument'
import CommandComboBox from '../CommandComboBox' import CommandComboBox from '../CommandComboBox'
import CommandBarReview from './CommandBarReview' import CommandBarReview from './CommandBarReview'
import { useLocation } from 'react-router-dom' import { useLocation } from 'react-router-dom'
import useHotkeyWrapper from 'lib/hotkeyWrapper'
export const CommandBar = () => { export const CommandBar = () => {
const { pathname } = useLocation() const { pathname } = useLocation()
@ -22,7 +22,7 @@ export const CommandBar = () => {
}, [pathname]) }, [pathname])
// Hook up keyboard shortcuts // Hook up keyboard shortcuts
useHotkeys(['mod+k', 'mod+/'], () => { useHotkeyWrapper(['mod+k', 'mod+/'], () => {
if (commandBarState.context.commands.length === 0) return if (commandBarState.context.commands.length === 0) return
if (commandBarState.matches('Closed')) { if (commandBarState.matches('Closed')) {
commandBarSend({ type: 'Open' }) commandBarSend({ type: 'Open' })

View File

@ -56,7 +56,7 @@ function CommandBarSelectionInput({
// In future the engine's edit mode will go away and this will be handled differently. // In future the engine's edit mode will go away and this will be handled differently.
useEffect(() => { useEffect(() => {
kclManager.exitEditMode() kclManager.exitEditMode()
return () => kclManager.enterEditMode() return () => kclManager.defaultSelectionFilter()
}, []) }, [])
// Fast-forward through this arg if it's marked as skippable // Fast-forward through this arg if it's marked as skippable

View File

@ -8,7 +8,6 @@ import { Dialog, Disclosure } from '@headlessui/react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faChevronRight, faTrashAlt } from '@fortawesome/free-solid-svg-icons' import { faChevronRight, faTrashAlt } from '@fortawesome/free-solid-svg-icons'
import { useFileContext } from 'hooks/useFileContext' import { useFileContext } from 'hooks/useFileContext'
import { useHotkeys } from 'react-hotkeys-hook'
import styles from './FileTree.module.css' import styles from './FileTree.module.css'
import { sortProject } from 'lib/tauriFS' import { sortProject } from 'lib/tauriFS'
import { FILE_EXT } from 'lib/constants' import { FILE_EXT } from 'lib/constants'
@ -16,6 +15,7 @@ import { CustomIcon } from './CustomIcon'
import { codeManager, kclManager } from 'lib/singletons' import { codeManager, kclManager } from 'lib/singletons'
import { useDocumentHasFocus } from 'hooks/useDocumentHasFocus' import { useDocumentHasFocus } from 'hooks/useDocumentHasFocus'
import { useLspContext } from './LspProvider' import { useLspContext } from './LspProvider'
import useHotkeyWrapper from 'lib/hotkeyWrapper'
function getIndentationCSS(level: number) { function getIndentationCSS(level: number) {
return `calc(1rem * ${level + 1})` return `calc(1rem * ${level + 1})`
@ -333,8 +333,8 @@ export const FileTreeMenu = () => {
send({ type: 'Create file', data: { name: '', makeDir: true } }) send({ type: 'Create file', data: { name: '', makeDir: true } })
} }
useHotkeys('meta + n', createFile) useHotkeyWrapper(['meta + n'], createFile)
useHotkeys('meta + shift + n', createFolder) useHotkeyWrapper(['meta + shift + n'], createFolder)
return ( return (
<> <>

View File

@ -22,8 +22,7 @@ import {
LspWorker, LspWorker,
} from 'editor/plugins/lsp/types' } from 'editor/plugins/lsp/types'
import { wasmUrl } from 'lang/wasm' import { wasmUrl } from 'lang/wasm'
import { PROJECT_ENTRYPOINT } from 'lib/constants'
const DEFAULT_FILE_NAME: string = 'main.kcl'
function getWorkspaceFolders(): LSP.WorkspaceFolder[] { function getWorkspaceFolders(): LSP.WorkspaceFolder[] {
return [] return []
@ -137,7 +136,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
if (isKclLspServerReady && !TEST && kclLspClient) { if (isKclLspServerReady && !TEST && kclLspClient) {
// Set up the lsp plugin. // Set up the lsp plugin.
const lsp = kclLanguage({ const lsp = kclLanguage({
documentUri: `file:///${DEFAULT_FILE_NAME}`, documentUri: `file:///${PROJECT_ENTRYPOINT}`,
workspaceFolders: getWorkspaceFolders(), workspaceFolders: getWorkspaceFolders(),
client: kclLspClient, client: kclLspClient,
}) })
@ -211,7 +210,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
if (isCopilotLspServerReady && !TEST && copilotLspClient) { if (isCopilotLspServerReady && !TEST && copilotLspClient) {
// Set up the lsp plugin. // Set up the lsp plugin.
const lsp = copilotPlugin({ const lsp = copilotPlugin({
documentUri: `file:///${DEFAULT_FILE_NAME}`, documentUri: `file:///${PROJECT_ENTRYPOINT}`,
workspaceFolders: getWorkspaceFolders(), workspaceFolders: getWorkspaceFolders(),
client: copilotLspClient, client: copilotLspClient,
allowHTMLContent: true, allowHTMLContent: true,
@ -236,7 +235,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
redirect: boolean redirect: boolean
) => { ) => {
const currentFilePath = projectBasename( const currentFilePath = projectBasename(
file?.path || DEFAULT_FILE_NAME, file?.path || PROJECT_ENTRYPOINT,
projectPath || '' projectPath || ''
) )
lspClients.forEach((lspClient) => { lspClients.forEach((lspClient) => {
@ -267,7 +266,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
if (file) { if (file) {
// Send that the file was opened. // Send that the file was opened.
const filename = projectBasename( const filename = projectBasename(
file?.path || DEFAULT_FILE_NAME, file?.path || PROJECT_ENTRYPOINT,
project?.path || '' project?.path || ''
) )
lspClients.forEach((lspClient) => { lspClients.forEach((lspClient) => {
@ -285,7 +284,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
const onFileOpen = (filePath: string | null, projectPath: string | null) => { const onFileOpen = (filePath: string | null, projectPath: string | null) => {
const currentFilePath = projectBasename( const currentFilePath = projectBasename(
filePath || DEFAULT_FILE_NAME, filePath || PROJECT_ENTRYPOINT,
projectPath || '' projectPath || ''
) )
lspClients.forEach((lspClient) => { lspClients.forEach((lspClient) => {
@ -302,7 +301,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
const onFileClose = (filePath: string | null, projectPath: string | null) => { const onFileClose = (filePath: string | null, projectPath: string | null) => {
const currentFilePath = projectBasename( const currentFilePath = projectBasename(
filePath || DEFAULT_FILE_NAME, filePath || PROJECT_ENTRYPOINT,
projectPath || '' projectPath || ''
) )
lspClients.forEach((lspClient) => { lspClients.forEach((lspClient) => {

View File

@ -55,9 +55,9 @@ import { Models } from '@kittycad/lib/dist/types/src'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { EditorSelection } from '@uiw/react-codemirror' import { EditorSelection } from '@uiw/react-codemirror'
import { CoreDumpManager } from 'lib/coredump' import { CoreDumpManager } from 'lib/coredump'
import { useHotkeys } from 'react-hotkeys-hook'
import { useSearchParams } from 'react-router-dom' import { useSearchParams } from 'react-router-dom'
import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls' import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls'
import useHotkeyWrapper from 'lib/hotkeyWrapper'
type MachineContext<T extends AnyStateMachine> = { type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T> state: StateFrom<T>
@ -103,7 +103,7 @@ export const ModelingMachineProvider = ({
htmlRef, htmlRef,
token token
) )
useHotkeys('meta + shift + .', () => coreDump(coreDumpManager, true)) useHotkeyWrapper(['meta + shift + .'], () => coreDump(coreDumpManager, true))
// Settings machine setup // Settings machine setup
// const retrievedSettings = useRef( // const retrievedSettings = useRef(

View File

@ -36,10 +36,6 @@ import {
import interact from '@replit/codemirror-interact' import interact from '@replit/codemirror-interact'
import { kclManager, editorManager, codeManager } from 'lib/singletons' import { kclManager, editorManager, codeManager } from 'lib/singletons'
import { useHotkeys } from 'react-hotkeys-hook' 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 { useLspContext } from 'components/LspProvider'
import { Prec, EditorState, Extension } from '@codemirror/state' import { Prec, EditorState, Extension } from '@codemirror/state'
import { import {
@ -67,7 +63,6 @@ export const KclEditorPane = () => {
? getSystemTheme() ? getSystemTheme()
: context.app.theme.current : context.app.theme.current
const { copilotLSP, kclLSP } = useLspContext() const { copilotLSP, kclLSP } = useLspContext()
const navigate = useNavigate()
useEffect(() => { useEffect(() => {
if (typeof window === 'undefined') return if (typeof window === 'undefined') return
@ -76,6 +71,8 @@ export const KclEditorPane = () => {
return () => window.removeEventListener('online', onlineCallback) return () => window.removeEventListener('online', onlineCallback)
}, []) }, [])
// Since these already exist in the editor, we don't need to define them
// with the wrapper.
useHotkeys('mod+z', (e) => { useHotkeys('mod+z', (e) => {
e.preventDefault() e.preventDefault()
editorManager.undo() editorManager.undo()
@ -87,6 +84,7 @@ export const KclEditorPane = () => {
const textWrapping = context.textEditor.textWrapping const textWrapping = context.textEditor.textWrapping
const cursorBlinking = context.textEditor.blinkingCursor const cursorBlinking = context.textEditor.blinkingCursor
const codeMirrorHotkeys = codeManager.getCodemirrorHotkeys()
const editorExtensions = useMemo(() => { const editorExtensions = useMemo(() => {
const extensions = [ const extensions = [
@ -106,20 +104,7 @@ export const KclEditorPane = () => {
...completionKeymap, ...completionKeymap,
...lintKeymap, ...lintKeymap,
indentWithTab, indentWithTab,
{ ...codeMirrorHotkeys,
key: 'Meta-k',
run: () => {
editorManager.commandBarSend({ type: 'Open' })
return false
},
},
{
key: isTauri() ? 'Meta-,' : 'Meta-Shift-,',
run: () => {
navigate(makeUrlPathRelative(paths.SETTINGS))
return false
},
},
{ {
key: editorShortcutMeta.convertToVariable.codeMirror, key: editorShortcutMeta.convertToVariable.codeMirror,
run: () => { run: () => {
@ -188,7 +173,13 @@ export const KclEditorPane = () => {
} }
return extensions return extensions
}, [kclLSP, copilotLSP, textWrapping.current, cursorBlinking.current]) }, [
kclLSP,
copilotLSP,
textWrapping.current,
cursorBlinking.current,
codeMirrorHotkeys,
])
const initialCode = useRef(codeManager.code) const initialCode = useRef(codeManager.code)

View File

@ -61,6 +61,7 @@ export const MemoryPane = () => {
displayObjectSize={true} displayObjectSize={true}
indentWidth={2} indentWidth={2}
quotesOnKeys={false} quotesOnKeys={false}
sortKeys={true}
name={false} name={false}
theme={theme === 'light' ? 'rjv-default' : 'monokai'} theme={theme === 'light' ? 'rjv-default' : 'monokai'}
/> />

View File

@ -119,7 +119,7 @@ function ProjectCard({
<> <>
<Link <Link
className="relative z-0 flex flex-col h-full gap-2 p-1 !no-underline !text-chalkboard-110 dark:!text-chalkboard-10" className="relative z-0 flex flex-col h-full gap-2 p-1 !no-underline !text-chalkboard-110 dark:!text-chalkboard-10"
to={`${paths.FILE}/${encodeURIComponent(project.path)}`} to={`${paths.FILE}/${encodeURIComponent(project.default_file)}`}
data-testid="project-link" data-testid="project-link"
> >
<div className="flex-1">{project.name?.replace(FILE_EXT, '')}</div> <div className="flex-1">{project.name?.replace(FILE_EXT, '')}</div>

View File

@ -24,6 +24,7 @@ const projectWellFormed = {
}, },
kcl_file_count: 1, kcl_file_count: 1,
directory_count: 0, directory_count: 0,
default_file: '/some/path/Simple Box/main.kcl',
} satisfies Project } satisfies Project
describe('ProjectSidebarMenu tests', () => { describe('ProjectSidebarMenu tests', () => {
@ -32,7 +33,7 @@ describe('ProjectSidebarMenu tests', () => {
<BrowserRouter> <BrowserRouter>
<CommandBarProvider> <CommandBarProvider>
<SettingsAuthProviderJest> <SettingsAuthProviderJest>
<ProjectSidebarMenu project={projectWellFormed} /> <ProjectSidebarMenu project={projectWellFormed} enableMenu={true} />
</SettingsAuthProviderJest> </SettingsAuthProviderJest>
</CommandBarProvider> </CommandBarProvider>
</BrowserRouter> </BrowserRouter>
@ -53,7 +54,7 @@ describe('ProjectSidebarMenu tests', () => {
<BrowserRouter> <BrowserRouter>
<CommandBarProvider> <CommandBarProvider>
<SettingsAuthProviderJest> <SettingsAuthProviderJest>
<ProjectSidebarMenu /> <ProjectSidebarMenu enableMenu={true} />
</SettingsAuthProviderJest> </SettingsAuthProviderJest>
</CommandBarProvider> </CommandBarProvider>
</BrowserRouter> </BrowserRouter>
@ -64,22 +65,18 @@ describe('ProjectSidebarMenu tests', () => {
expect(screen.getByTestId('projectName')).toHaveTextContent(APP_NAME) expect(screen.getByTestId('projectName')).toHaveTextContent(APP_NAME)
}) })
test('Renders as a link if set to do so', () => { test('Disables popover menu by default', () => {
render( render(
<BrowserRouter> <BrowserRouter>
<CommandBarProvider> <CommandBarProvider>
<SettingsAuthProviderJest> <SettingsAuthProviderJest>
<ProjectSidebarMenu <ProjectSidebarMenu project={projectWellFormed} />
project={projectWellFormed}
renderAsLink={true}
/>
</SettingsAuthProviderJest> </SettingsAuthProviderJest>
</CommandBarProvider> </CommandBarProvider>
</BrowserRouter> </BrowserRouter>
) )
expect(screen.getByTestId('project-sidebar-link')).toBeInTheDocument() expect(screen.getByTestId('project-name')).toHaveTextContent(
expect(screen.getByTestId('project-sidebar-link-name')).toHaveTextContent(
projectWellFormed.name projectWellFormed.name
) )
}) })

View File

@ -17,53 +17,63 @@ import { engineCommandManager } from 'lib/singletons'
const ProjectSidebarMenu = ({ const ProjectSidebarMenu = ({
project, project,
file, file,
renderAsLink = false, enableMenu = false,
}: { }: {
renderAsLink?: boolean enableMenu?: boolean
project?: IndexLoaderData['project'] project?: IndexLoaderData['project']
file?: IndexLoaderData['file'] file?: IndexLoaderData['file']
}) => { }) => {
const { onProjectClose } = useLspContext()
return ( return (
<div className="!no-underline h-full mr-auto max-h-min min-h-12 min-w-max flex items-center gap-2"> <div className="!no-underline h-full mr-auto max-h-min min-h-12 min-w-max flex items-center gap-2">
<Link <AppLogoLink project={project} file={file} />
onClick={() => { {enableMenu ? (
onProjectClose(file || null, project?.path || null, false)
// Clear the scene and end the session.
engineCommandManager.endSession()
}}
to={paths.HOME}
className="relative h-full grid place-content-center group p-1.5 before:block before:content-[''] before:absolute before:inset-0 before:bottom-2.5 before:z-[-1] before:bg-primary hover:before:brightness-110 before:rounded-b-sm"
>
<Logo className="w-auto h-4 text-chalkboard-10" />
</Link>
{renderAsLink ? (
<>
<Link
onClick={() => {
onProjectClose(file || null, project?.path || null, false)
// Clear the scene and end the session.
engineCommandManager.endSession()
}}
to={paths.HOME}
className="!no-underline"
data-testid="project-sidebar-link"
>
<span
className="hidden text-sm text-chalkboard-110 dark:text-chalkboard-20 whitespace-nowrap lg:block"
data-testid="project-sidebar-link-name"
>
{project?.name ? project.name : APP_NAME}
</span>
</Link>
</>
) : (
<ProjectMenuPopover project={project} file={file} /> <ProjectMenuPopover project={project} file={file} />
) : (
<span
className="hidden select-none cursor-default text-sm text-chalkboard-110 dark:text-chalkboard-20 whitespace-nowrap lg:block"
data-testid="project-name"
>
{project?.name ? project.name : APP_NAME}
</span>
)} )}
</div> </div>
) )
} }
function AppLogoLink({
project,
file,
}: {
project?: IndexLoaderData['project']
file?: IndexLoaderData['file']
}) {
const { onProjectClose } = useLspContext()
const wrapperClassName =
"relative h-full grid place-content-center group p-1.5 before:block before:content-[''] before:absolute before:inset-0 before:bottom-2.5 before:z-[-1] before:bg-primary before:rounded-b-sm"
const logoClassName = 'w-auto h-4 text-chalkboard-10'
return isTauri() ? (
<Link
data-testid="app-logo"
onClick={() => {
onProjectClose(file || null, project?.path || null, false)
// Clear the scene and end the session.
engineCommandManager.endSession()
}}
to={paths.HOME}
className={wrapperClassName + ' hover:before:brightness-110'}
>
<Logo className={logoClassName} />
<span className="sr-only">{APP_NAME}</span>
</Link>
) : (
<div className={wrapperClassName} data-testid="app-logo">
<Logo className={logoClassName} />
<span className="sr-only">{APP_NAME}</span>
</div>
)
}
function ProjectMenuPopover({ function ProjectMenuPopover({
project, project,
file, file,

View File

@ -172,7 +172,7 @@ export const SettingsAuthProviderBase = ({
}, },
'Execute AST': () => kclManager.executeCode(true), 'Execute AST': () => kclManager.executeCode(true),
persistSettings: (context) => persistSettings: (context) =>
saveSettings(context, loadedProject?.project?.name), saveSettings(context, loadedProject?.project?.path),
}, },
} }
) )

View File

@ -134,6 +134,7 @@ code {
#code-mirror-override > div, #code-mirror-override > div,
#code-mirror-override .cm-editor { #code-mirror-override .cm-editor {
@apply bg-transparent h-full; @apply bg-transparent h-full;
@apply select-auto;
} }
#code-mirror-override .cm-scroller { #code-mirror-override .cm-scroller {

View File

@ -188,7 +188,7 @@ export class KclManager {
engineCommandManager: this.engineCommandManager, engineCommandManager: this.engineCommandManager,
}) })
sceneInfra.modelingSend({ type: 'code edit during sketch' }) sceneInfra.modelingSend({ type: 'code edit during sketch' })
enterEditMode(programMemory, this.engineCommandManager) defaultSelectionFilter(programMemory, this.engineCommandManager)
this.isExecuting = false this.isExecuting = false
// Check the cancellation token for this execution before applying side effects // Check the cancellation token for this execution before applying side effects
if (this._cancelTokens.get(currentExecutionId)) { if (this._cancelTokens.get(currentExecutionId)) {
@ -348,9 +348,6 @@ export class KclManager {
void this.engineCommandManager.setPlaneHidden(this.defaultPlanes.yz, true) void this.engineCommandManager.setPlaneHidden(this.defaultPlanes.yz, true)
void this.engineCommandManager.setPlaneHidden(this.defaultPlanes.xz, true) void this.engineCommandManager.setPlaneHidden(this.defaultPlanes.xz, true)
} }
enterEditMode() {
enterEditMode(this.programMemory, this.engineCommandManager)
}
exitEditMode() { exitEditMode() {
this.engineCommandManager.sendSceneCommand({ this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req', type: 'modeling_cmd_req',
@ -358,9 +355,12 @@ export class KclManager {
cmd: { type: 'edit_mode_exit' }, cmd: { type: 'edit_mode_exit' },
}) })
} }
defaultSelectionFilter() {
defaultSelectionFilter(this.programMemory, this.engineCommandManager)
}
} }
function enterEditMode( function defaultSelectionFilter(
programMemory: ProgramMemory, programMemory: ProgramMemory,
engineCommandManager: EngineCommandManager engineCommandManager: EngineCommandManager
) { ) {

View File

@ -6,6 +6,7 @@ import { isTauri } from 'lib/isTauri'
import { writeTextFile } from '@tauri-apps/plugin-fs' import { writeTextFile } from '@tauri-apps/plugin-fs'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { editorManager } from 'lib/singletons' import { editorManager } from 'lib/singletons'
import { KeyBinding } from '@uiw/react-codemirror'
const PERSIST_CODE_TOKEN = 'persistCode' const PERSIST_CODE_TOKEN = 'persistCode'
@ -13,6 +14,7 @@ export default class CodeManager {
private _code: string = bracket private _code: string = bracket
#updateState: (arg: string) => void = () => {} #updateState: (arg: string) => void = () => {}
private _currentFilePath: string | null = null private _currentFilePath: string | null = null
private _hotkeys: { [key: string]: () => void } = {}
constructor() { constructor() {
if (isTauri()) { if (isTauri()) {
@ -48,6 +50,20 @@ export default class CodeManager {
this.#updateState = setCode this.#updateState = setCode
} }
registerHotkey(hotkey: string, callback: () => void) {
this._hotkeys[hotkey] = callback
}
getCodemirrorHotkeys(): KeyBinding[] {
return Object.keys(this._hotkeys).map((key) => ({
key,
run: () => {
this._hotkeys[key]()
return false
},
}))
}
updateCurrentFilePath(path: string) { updateCurrentFilePath(path: string) {
this._currentFilePath = path this._currentFilePath = path
} }

View File

@ -1,20 +1,21 @@
import { ToolTip } from '../useStore' import { ToolTip } from '../useStore'
import { Selection, Selections } from 'lib/selections' import { Selection, Selections } from 'lib/selections'
import { import {
ArrayExpression,
BinaryExpression, BinaryExpression,
Program,
SyntaxType,
Value,
CallExpression, CallExpression,
ExpressionStatement, ExpressionStatement,
VariableDeclaration,
ReturnStatement,
ArrayExpression,
PathToNode, PathToNode,
PipeExpression,
Program,
ProgramMemory, ProgramMemory,
ReturnStatement,
SketchGroup, SketchGroup,
SourceRange, SourceRange,
PipeExpression, SyntaxType,
Value,
VariableDeclaration,
VariableDeclarator,
} from './wasm' } from './wasm'
import { createIdentifier, splitPathAtLastIndex } from './modifyAst' import { createIdentifier, splitPathAtLastIndex } from './modifyAst'
import { getSketchSegmentFromSourceRange } from './std/sketchConstraints' import { getSketchSegmentFromSourceRange } from './std/sketchConstraints'
@ -295,6 +296,58 @@ export function getNodePathFromSourceRange(
return path return path
} }
type KCLNode =
| Value
| ExpressionStatement
| VariableDeclaration
| VariableDeclarator
| ReturnStatement
export function traverse(
node: KCLNode,
option: {
enter?: (node: KCLNode) => void
leave?: (node: KCLNode) => void
}
) {
option?.enter?.(node)
const _traverse = (node: KCLNode) => traverse(node, option)
if (node.type === 'VariableDeclaration') {
node.declarations.forEach(_traverse)
} else if (node.type === 'VariableDeclarator') {
_traverse(node.init)
} else if (node.type === 'PipeExpression') {
node.body.forEach(_traverse)
} else if (node.type === 'CallExpression') {
_traverse(node.callee)
node.arguments.forEach(_traverse)
} else if (node.type === 'BinaryExpression') {
_traverse(node.left)
_traverse(node.right)
} else if (node.type === 'Identifier') {
// do nothing
} else if (node.type === 'Literal') {
// do nothing
} else if (node.type === 'ArrayExpression') {
node.elements.forEach(_traverse)
} else if (node.type === 'ObjectExpression') {
node.properties.forEach(({ key, value }) => {
_traverse(key)
_traverse(value)
})
} else if (node.type === 'UnaryExpression') {
_traverse(node.argument)
} else if (node.type === 'MemberExpression') {
// hmm this smell
_traverse(node.object)
_traverse(node.property)
} else if ('body' in node && Array.isArray(node.body)) {
node.body.forEach(_traverse)
}
option?.leave?.(node)
}
export interface PrevVariable<T> { export interface PrevVariable<T> {
key: string key: string
value: T value: T

View File

@ -54,8 +54,16 @@ interface PendingCommand extends CommandInfo {
resolve: (val: ResolveCommand) => void resolve: (val: ResolveCommand) => void
} }
/**
* The ArtifactMap is a client-side representation of the artifacts that
* have been sent to the server-side engine. It is used to keep track of
* the state of each command, and to resolve the promise that was returned.
* It is also used to keep track of what entities are in the engine scene,
* so that we can associate IDs returned from the engine with the
* lines of KCL code that generated them.
*/
export interface ArtifactMap { export interface ArtifactMap {
[key: string]: ResultCommand | PendingCommand | FailedCommand [commandId: string]: ResultCommand | PendingCommand | FailedCommand
} }
interface NewTrackArgs { interface NewTrackArgs {
@ -63,10 +71,11 @@ interface NewTrackArgs {
mediaStream: MediaStream mediaStream: MediaStream
} }
// This looks funny, I know. This is needed because node and the browser /** This looks funny, I know. This is needed because node and the browser
// disagree as to the type. In a browser it's a number, but in node it's a * disagree as to the type. In a browser it's a number, but in node it's a
// "Timeout". * "Timeout".
type Timeout = ReturnType<typeof setTimeout> */
type IsomorphicTimeout = ReturnType<typeof setTimeout>
type ClientMetrics = Models['ClientMetrics_type'] type ClientMetrics = Models['ClientMetrics_type']
@ -188,9 +197,11 @@ export type EngineConnectionState =
| State<EngineConnectionStateType.Disconnecting, DisconnectingValue> | State<EngineConnectionStateType.Disconnecting, DisconnectingValue>
| State<EngineConnectionStateType.Disconnected, void> | State<EngineConnectionStateType.Disconnected, void>
// EngineConnection encapsulates the connection(s) to the Engine /**
// for the EngineCommandManager; namely, the underlying WebSocket * EngineConnection encapsulates the connection(s) to the Engine
// and WebRTC connections. * for the EngineCommandManager; namely, the underlying WebSocket
* and WebRTC connections.
*/
class EngineConnection { class EngineConnection {
websocket?: WebSocket websocket?: WebSocket
pc?: RTCPeerConnection pc?: RTCPeerConnection
@ -227,23 +238,40 @@ class EngineConnection {
this.onConnectionStateChange(this._state) this.onConnectionStateChange(this._state)
} }
private failedConnTimeout: Timeout | null private failedConnTimeout: IsomorphicTimeout | null
readonly url: string readonly url: string
private readonly token?: string private readonly token?: string
// For now, this is only used by the NetworkHealthIndicator. /**For now, this is only used by the NetworkHealthIndicator.
// We can eventually use it for more, but one step at a time. * We can eventually use it for more, but one step at a time.
*/
private onConnectionStateChange: (state: EngineConnectionState) => void private onConnectionStateChange: (state: EngineConnectionState) => void
// These are used for the EngineCommandManager and were created /**
// before onConnectionStateChange existed. * Used for the EngineCommandManager, created before
* onConnectionStateChange existed.
*/
private onEngineConnectionOpen: (engineConnection: EngineConnection) => void private onEngineConnectionOpen: (engineConnection: EngineConnection) => void
/**
* Used for the EngineCommandManager, created before
* onConnectionStateChange existed.
*/
private onConnectionStarted: (engineConnection: EngineConnection) => void private onConnectionStarted: (engineConnection: EngineConnection) => void
/**
* Used for the EngineCommandManager, created before
* onConnectionStateChange existed.
*/
private onClose: (engineConnection: EngineConnection) => void private onClose: (engineConnection: EngineConnection) => void
/**
* Used for the EngineCommandManager, created before
* onConnectionStateChange existed.
*/
private onNewTrack: (track: NewTrackArgs) => void private onNewTrack: (track: NewTrackArgs) => void
// TODO: actual type is ClientMetrics /**
* @todo actual type is `ClientMetrics`
*/
public webrtcStatsCollector?: () => Promise<WebRTCClientMetrics> public webrtcStatsCollector?: () => Promise<WebRTCClientMetrics>
private engineCommandManager: EngineCommandManager private engineCommandManager: EngineCommandManager
@ -322,11 +350,13 @@ class EngineConnection {
} }
} }
// connect will attempt to connect to the Engine over a WebSocket, and /**
// establish the WebRTC connections. * Attempts to connect to the Engine over a WebSocket, and
// * establish the WebRTC connections.
// This will attempt the full handshake, and retry if the connection *
// did not establish. * This will attempt the full handshake, and retry if the connection
* did not establish.
*/
connect() { connect() {
if (this.isConnecting() || this.isReady()) { if (this.isConnecting() || this.isReady()) {
return return
@ -521,6 +551,7 @@ class EngineConnection {
// Everything is now connected. // Everything is now connected.
this.state = { type: EngineConnectionStateType.ConnectionEstablished } this.state = { type: EngineConnectionStateType.ConnectionEstablished }
this.engineCommandManager.inSequence = 1
this.onEngineConnectionOpen(this) this.onEngineConnectionOpen(this)
}) })
@ -908,21 +939,66 @@ export type CommandLog =
data: null data: null
} }
/**
* The EngineCommandManager is the main interface to the Engine for Modeling App.
*
* It is responsible for sending commands to the Engine, and managing the state
* of those commands. It also sets up and tears down the connection to the Engine
* through the {@link EngineConnection} class.
*
* It also maintains an {@link artifactMap} that keeps track of the state of each
* command, and the artifacts that have been generated by those commands.
*/
export class EngineCommandManager { export class EngineCommandManager {
/**
* The artifactMap is a client-side representation of the commands that have been sent
* to the server-side geometry engine, and the state of their resulting artifacts.
*
* It is used to keep track of the state of each command, which can fail, succeed, or be
* pending.
*
* It is also used to keep track of our client's understanding of what is in the engine scene
* so that we can map to and from KCL code. Each artifact maintains a source range to the part
* of the KCL code that generated it.
*/
artifactMap: ArtifactMap = {} artifactMap: ArtifactMap = {}
/**
* The {@link ArtifactMap} from the previous engine connection. This is used as a fallback
* when the engine connection is reset without a full client-side refresh.
*
* @deprecated This was used during a short time when we were choosing to not execute the engine in certain cases.
*/
lastArtifactMap: ArtifactMap = {} lastArtifactMap: ArtifactMap = {}
/**
* The client-side representation of the scene command artifacts that have been sent to the server;
* that is, the *non-modeling* commands and corresponding artifacts.
*
* For modeling commands, see {@link artifactMap}.
*/
sceneCommandArtifacts: ArtifactMap = {} sceneCommandArtifacts: ArtifactMap = {}
/**
* A counter that is incremented with each command sent over the *unreliable* channel to the engine.
* This is compared to the latest received {@link inSequence} number to determine if we should ignore
* any out-of-order late responses in the unreliable channel.
*/
outSequence = 1 outSequence = 1
/**
* The latest sequence number received from the engine over the *unreliable* channel.
* This is compared to the {@link outSequence} number to determine if we should ignore
* any out-of-order late responses in the unreliable channel.
*/
inSequence = 1 inSequence = 1
pool?: string pool?: string
engineConnection?: EngineConnection engineConnection?: EngineConnection
defaultPlanes: DefaultPlanes | null = null defaultPlanes: DefaultPlanes | null = null
commandLogs: CommandLog[] = [] commandLogs: CommandLog[] = []
_commandLogCallBack: (command: CommandLog[]) => void = () => {} _commandLogCallBack: (command: CommandLog[]) => void = () => {}
// Folks should realize that wait for ready does not get called _everytime_
// the connection resets and restarts, it only gets called the first time.
// Be careful what you put here.
private resolveReady = () => {} private resolveReady = () => {}
/** Folks should realize that wait for ready does not get called _everytime_
* the connection resets and restarts, it only gets called the first time.
*
* Be careful what you put here.
*/
waitForReady: Promise<void> = new Promise((resolve) => { waitForReady: Promise<void> = new Promise((resolve) => {
this.resolveReady = resolve this.resolveReady = resolve
}) })

37
src/lib/hotkeyWrapper.ts Normal file
View File

@ -0,0 +1,37 @@
import { Options, useHotkeys } from 'react-hotkeys-hook'
import { useEffect } from 'react'
import { codeManager } from './singletons'
// Hotkey wrapper wraps hotkeys for the app (outside of the editor)
// With hotkeys inside the editor.
// This way we can have hotkeys defined in one place and not have to worry about
// conflicting hotkeys, or them only being implemented for the app but not
// inside the editor.
// TODO: would be nice if this didn't have to be a react hook. It's not needed
// for the code mirror stuff but but it is needed for the useHotkeys hook.
export default function useHotkeyWrapper(
hotkey: string[],
callback: () => void,
additionalOptions?: Options
) {
useHotkeys(hotkey, callback, additionalOptions)
useEffect(() => {
for (const key of hotkey) {
const keybinding = mapHotkeyToCodeMirrorHotkey(key)
codeManager.registerHotkey(keybinding, callback)
}
})
}
// Convert hotkey to code mirror hotkey
// See: https://codemirror.net/docs/ref/#view.KeyBinding
function mapHotkeyToCodeMirrorHotkey(hotkey: string): string {
return hotkey
.replaceAll('+', '-')
.replaceAll(' ', '')
.replaceAll('mod', 'Meta')
.replaceAll('meta', 'Meta')
.replaceAll('ctrl', 'Ctrl')
.replaceAll('shift', 'Shift')
.replaceAll('alt', 'Alt')
}

View File

@ -38,8 +38,8 @@ export const settingsLoader: LoaderFunction = async ({
configuration configuration
) )
if (projectPathData) { if (projectPathData) {
const { project_name } = projectPathData const { project_path } = projectPathData
const { settings: s } = await loadAndValidateSettings(project_name) const { settings: s } = await loadAndValidateSettings(project_path)
settings = s settings = s
} }
} }
@ -118,6 +118,7 @@ export const fileLoader: LoaderFunction = async ({
children: [], children: [],
kcl_file_count: 0, kcl_file_count: 0,
directory_count: 0, directory_count: 0,
default_file: project_path,
}, },
file: { file: {
name: current_file_name, name: current_file_name,

View File

@ -77,7 +77,29 @@ export async function getEventForSelectWithPoint(
}, },
} }
} }
const _artifact = engineCommandManager.artifactMap[data.entity_id] let _artifact = engineCommandManager.artifactMap[data.entity_id]
if (!_artifact) {
// This logic for getting the parent id is for solid2ds as in edit mode it return the face id
// but we don't recognise that in the artifact map because we store the path id when the path is
// created, the solid2d is implicitly created with the close stdlib function
// there's plans to get the faceId back from the solid2d creation
// https://github.com/KittyCAD/engine/issues/2094
// at which point we can add it to the artifact map and remove this logic
const parentId = (
await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
type: 'entity_get_parent_id',
entity_id: data.entity_id,
},
cmd_id: uuidv4(),
})
)?.data?.data?.entity_id
const parentArtifact = engineCommandManager.artifactMap[parentId]
if (parentArtifact) {
_artifact = parentArtifact
}
}
const sourceRange = _artifact?.range const sourceRange = _artifact?.range
if (_artifact) { if (_artifact) {
if (_artifact.commandType === 'solid3d_get_extrusion_face_info') { if (_artifact.commandType === 'solid3d_get_extrusion_face_info') {

View File

@ -147,7 +147,7 @@ export interface AppSettings {
} }
export async function loadAndValidateSettings( export async function loadAndValidateSettings(
projectName?: string projectPath?: string
): Promise<AppSettings> { ): Promise<AppSettings> {
const settings = createSettings() const settings = createSettings()
const inTauri = isTauri() const inTauri = isTauri()
@ -166,9 +166,9 @@ export async function loadAndValidateSettings(
setSettingsAtLevel(settings, 'user', appSettingsPayload) setSettingsAtLevel(settings, 'user', appSettingsPayload)
// Load the project settings if they exist // Load the project settings if they exist
if (projectName) { if (projectPath) {
const projectSettings = inTauri const projectSettings = inTauri
? await readProjectSettingsFile(appSettings, projectName) ? await readProjectSettingsFile(projectPath)
: readLocalStorageProjectSettingsFile() : readLocalStorageProjectSettingsFile()
const projectSettingsPayload = const projectSettingsPayload =
@ -182,7 +182,7 @@ export async function loadAndValidateSettings(
export async function saveSettings( export async function saveSettings(
allSettings: typeof settings, allSettings: typeof settings,
projectName?: string projectPath?: string
) { ) {
// Make sure we have wasm initialized. // Make sure we have wasm initialized.
await initPromise await initPromise
@ -204,7 +204,7 @@ export async function saveSettings(
) )
} }
if (!projectName) { if (!projectPath) {
// If we're not saving project settings, we're done. // If we're not saving project settings, we're done.
return return
} }
@ -217,7 +217,7 @@ export async function saveSettings(
// Write the project settings. // Write the project settings.
if (inTauri) { if (inTauri) {
await writeProjectSettingsFile(appSettings, projectName, projectSettings) await writeProjectSettingsFile(projectPath, projectSettings)
} else { } else {
localStorage.setItem( localStorage.setItem(
localStorageProjectSettingsPath(), localStorageProjectSettingsPath(),

View File

@ -8,23 +8,36 @@ import { Project } from 'wasm-lib/kcl/bindings/Project'
import { FileEntry } from 'wasm-lib/kcl/bindings/FileEntry' import { FileEntry } from 'wasm-lib/kcl/bindings/FileEntry'
import { ProjectState } from 'wasm-lib/kcl/bindings/ProjectState' import { ProjectState } from 'wasm-lib/kcl/bindings/ProjectState'
import { ProjectRoute } from 'wasm-lib/kcl/bindings/ProjectRoute' import { ProjectRoute } from 'wasm-lib/kcl/bindings/ProjectRoute'
import { isTauri } from './isTauri'
// Get the app state from tauri. // Get the app state from tauri.
export async function getState(): Promise<ProjectState | undefined> { export async function getState(): Promise<ProjectState | undefined> {
if (!isTauri()) {
return undefined
}
return await invoke<ProjectState | undefined>('get_state') return await invoke<ProjectState | undefined>('get_state')
} }
// Set the app state in tauri. // Set the app state in tauri.
export async function setState(state: ProjectState | undefined): Promise<void> { export async function setState(state: ProjectState | undefined): Promise<void> {
if (!isTauri()) {
return
}
return await invoke('set_state', { state }) return await invoke('set_state', { state })
} }
// Get the initial default dir for holding all projects. // Get the initial default dir for holding all projects.
export async function getInitialDefaultDir(): Promise<string> { export async function getInitialDefaultDir(): Promise<string> {
if (!isTauri()) {
return ''
}
return invoke<string>('get_initial_default_dir') return invoke<string>('get_initial_default_dir')
} }
export async function showInFolder(path: string | undefined): Promise<void> { export async function showInFolder(path: string | undefined): Promise<void> {
if (!isTauri()) {
return
}
if (!path) { if (!path) {
console.error('path is undefined cannot call tauri showInFolder') console.error('path is undefined cannot call tauri showInFolder')
return return
@ -34,7 +47,10 @@ export async function showInFolder(path: string | undefined): Promise<void> {
export async function initializeProjectDirectory( export async function initializeProjectDirectory(
settings: Configuration settings: Configuration
): Promise<string> { ): Promise<string | undefined> {
if (!isTauri()) {
return undefined
}
return await invoke<string>('initialize_project_directory', { return await invoke<string>('initialize_project_directory', {
configuration: settings, configuration: settings,
}) })
@ -127,24 +143,20 @@ export async function writeAppSettingsFile(
// Read project settings file. // Read project settings file.
export async function readProjectSettingsFile( export async function readProjectSettingsFile(
appSettings: Configuration, projectPath: string
projectName: string
): Promise<ProjectConfiguration> { ): Promise<ProjectConfiguration> {
return await invoke<ProjectConfiguration>('read_project_settings_file', { return await invoke<ProjectConfiguration>('read_project_settings_file', {
appSettings, projectPath,
projectName,
}) })
} }
// Write project settings file. // Write project settings file.
export async function writeProjectSettingsFile( export async function writeProjectSettingsFile(
appSettings: Configuration, projectPath: string,
projectName: string,
settings: ProjectConfiguration settings: ProjectConfiguration
): Promise<void> { ): Promise<void> {
return await invoke('write_project_settings_file', { return await invoke('write_project_settings_file', {
appSettings, projectPath,
projectName,
configuration: settings, configuration: settings,
}) })
} }

View File

@ -890,6 +890,7 @@ export const modelingMachine = createMachine(
}) })
.then(async () => { .then(async () => {
// there doesn't appear to be an animation, but if there was one we could add a wait here // there doesn't appear to be an animation, but if there was one we could add a wait here
await engineCommandManager.sendSceneCommand({ await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req', type: 'modeling_cmd_req',
cmd_id: uuidv4(), cmd_id: uuidv4(),
@ -898,6 +899,31 @@ export const modelingMachine = createMachine(
}, },
}) })
sceneInfra.camControls.syncDirection = 'engineToClient' sceneInfra.camControls.syncDirection = 'engineToClient'
await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_set_perspective',
},
})
const center = { x: 0, y: 0, z: 0 }
const camPos = sceneInfra.camControls.camera.position
if (camPos.x === 0 && camPos.y === 0) {
// looking straight up or down is going to cause issues with the engine
// tweaking the center to be a little off center
// TODO come up with a proper fix
center.y = 0.05
}
await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
center,
vantage: sceneInfra.camControls.camera.position,
up: { x: 0, y: 0, z: 1 },
},
})
await engineCommandManager.sendSceneCommand({ await engineCommandManager.sendSceneCommand({
// CameraControls subscribes to default_camera_get_settings response events // CameraControls subscribes to default_camera_get_settings response events
// firing this at connection ensure the camera's are synced initially // firing this at connection ensure the camera's are synced initially
@ -1027,7 +1053,8 @@ export const modelingMachine = createMachine(
filter: ['face', 'plane'], filter: ['face', 'plane'],
}, },
}), }),
'set selection filter to defaults': () => kclManager.enterEditMode(), 'set selection filter to defaults': () =>
kclManager.defaultSelectionFilter(),
}, },
// end actions // end actions
} }

View File

@ -177,6 +177,15 @@ dependencies = [
"num-traits 0.2.18", "num-traits 0.2.18",
] ]
[[package]]
name = "arbitrary"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110"
dependencies = [
"derive_arbitrary",
]
[[package]] [[package]]
name = "arc-swap" name = "arc-swap"
version = "1.7.0" version = "1.7.0"
@ -979,6 +988,17 @@ dependencies = [
"syn 2.0.65", "syn 2.0.65",
] ]
[[package]]
name = "derive_arbitrary"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.65",
]
[[package]] [[package]]
name = "diesel_derives" name = "diesel_derives"
version = "2.1.3" version = "2.1.3"
@ -1851,15 +1871,6 @@ dependencies = [
"either", "either",
] ]
[[package]]
name = "itertools"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57"
dependencies = [
"either",
]
[[package]] [[package]]
name = "itertools" name = "itertools"
version = "0.12.1" version = "0.12.1"
@ -2674,7 +2685,7 @@ dependencies = [
"bincode", "bincode",
"either", "either",
"fnv", "fnv",
"itertools 0.11.0", "itertools 0.12.1",
"lazy_static", "lazy_static",
"nom", "nom",
"quick-xml", "quick-xml",
@ -5303,13 +5314,16 @@ dependencies = [
[[package]] [[package]]
name = "zip" name = "zip"
version = "0.6.6" version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" checksum = "f1f4a27345eb6f7aa7bd015ba7eb4175fa4e1b462a29874b779e0bbcf96c6ac7"
dependencies = [ dependencies = [
"byteorder", "arbitrary",
"crc32fast", "crc32fast",
"crossbeam-utils", "crossbeam-utils",
"displaydoc",
"indexmap 2.2.5",
"thiserror",
] ]
[[package]] [[package]]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 79 KiB

View File

@ -43,7 +43,7 @@ url = { version = "2.5.0", features = ["serde"] }
uuid = { version = "1.8.0", features = ["v4", "js", "serde"] } uuid = { version = "1.8.0", features = ["v4", "js", "serde"] }
validator = { version = "0.18.1", features = ["derive"] } validator = { version = "0.18.1", features = ["derive"] }
winnow = "0.5.40" winnow = "0.5.40"
zip = { version = "0.6.6", default-features = false } zip = { version = "1.3.0", default-features = false }
[target.'cfg(target_arch = "wasm32")'.dependencies] [target.'cfg(target_arch = "wasm32")'.dependencies]
js-sys = { version = "0.3.69" } js-sys = { version = "0.3.69" }

View File

@ -0,0 +1,20 @@
# KCL
Our language for defining geometry and working with our Geometry Engine efficiently. Short for KittyCAD Language, named after our Design API.
## Contributing a standard library function
We've built a lot of tooling to make contributing to KCL easier. If you are interested in contributing a new standard library function to KCL, here is the rough process:
1. Open just the wasm-lib folder in your editor of choice. VS Code, for example, struggles to run rust-analyzer on the entire modeling-app directory because it's such a turducken of TS and Rust code.
2. Find the definition for similar standard library functions in `./kcl/src/std` and place your new one near it or in the same category file.
3. Add your new code. A new standard library function consists of:
4. A `pub async` of the actual standard library function in Rust
5. A doc comment block containing at least one example using your new standard library function (the Rust compiler will error if you don't provide an example our teammates are dope)
6. A `stdlib` macro providing the name that will need to be written by KCL users to use the function (this is usually a camelCase version of your Rust implementation, which is named with snake_case)
7. An inner function that is published only to the crate
8. Add your new standard library function to [the long list of CORE_FNS in mod.rs](https://github.com/KittyCAD/modeling-app/blob/main/src/wasm-lib/kcl/src/std/mod.rs#L42)
9. Get a production Zoo dev token and run `export KITTYCAD_API_TOKEN=your-token-here` in a terminal
10. Run `TWENTY_TWENTY=overwrite cargo nextest run --workspace --no-fail-fast` to take snapshot tests of your example code running in the engine
11. Run `EXPECTORATE=overwrite cargo test --all generate_stdlib -- --nocapture` to generate new Markdown documentation for your function that will be used [to generate docs on our website](https://zoo.dev/docs/kcl).
12. Create a PR in GitHub.

View File

@ -1214,7 +1214,11 @@ impl CallExpression {
let func = memory.get(&fn_name, self.into())?; let func = memory.get(&fn_name, self.into())?;
let result = func let result = func
.call_fn(fn_args, memory.clone(), ctx.clone()) .call_fn(fn_args, memory.clone(), ctx.clone())
.await? .await
.map_err(|e| {
// Add the call expression to the source ranges.
e.add_source_ranges(vec![self.into()])
})?
.ok_or_else(|| { .ok_or_else(|| {
KclError::UndefinedValue(KclErrorDetails { KclError::UndefinedValue(KclErrorDetails {
message: format!("Result of user-defined function {} is undefined", fn_name), message: format!("Result of user-defined function {} is undefined", fn_name),

View File

@ -142,6 +142,25 @@ impl KclError {
new new
} }
pub fn add_source_ranges(&self, source_ranges: Vec<SourceRange>) -> Self {
let mut new = self.clone();
match &mut new {
KclError::Lexical(e) => e.source_ranges.extend(source_ranges),
KclError::Syntax(e) => e.source_ranges.extend(source_ranges),
KclError::Semantic(e) => e.source_ranges.extend(source_ranges),
KclError::Type(e) => e.source_ranges.extend(source_ranges),
KclError::Unimplemented(e) => e.source_ranges.extend(source_ranges),
KclError::Unexpected(e) => e.source_ranges.extend(source_ranges),
KclError::ValueAlreadyDefined(e) => e.source_ranges.extend(source_ranges),
KclError::UndefinedValue(e) => e.source_ranges.extend(source_ranges),
KclError::InvalidExpression(e) => e.source_ranges.extend(source_ranges),
KclError::Engine(e) => e.source_ranges.extend(source_ranges),
KclError::Internal(e) => e.source_ranges.extend(source_ranges),
}
new
}
} }
/// This is different than to_string() in that it will serialize the Error /// This is different than to_string() in that it will serialize the Error

View File

@ -498,14 +498,13 @@ impl Backend {
for (entry, value) in self.code_map.inner().await.iter() { for (entry, value) in self.code_map.inner().await.iter() {
let file_name = entry.replace("file://", "").to_string(); let file_name = entry.replace("file://", "").to_string();
let options = zip::write::FileOptions::default().compression_method(zip::CompressionMethod::Stored); let options = zip::write::SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
zip.start_file(file_name, options)?; zip.start_file(file_name, options)?;
zip.write_all(value)?; zip.write_all(value)?;
} }
// Apply the changes you've made. // Apply the changes you've made.
// Dropping the `ZipWriter` will have the same effect, but may silently fail // Dropping the `ZipWriter` will have the same effect, but may silently fail
zip.finish()?; zip.finish()?;
drop(zip);
Ok(buf) Ok(buf)
} }

View File

@ -1,5 +1,6 @@
//! Types for interacting with files in projects. //! Types for interacting with files in projects.
#[cfg(not(target_arch = "wasm32"))]
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use anyhow::Result; use anyhow::Result;
@ -7,7 +8,7 @@ use parse_display::{Display, FromStr};
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::settings::types::{Configuration, DEFAULT_PROJECT_KCL_FILE}; use crate::settings::types::Configuration;
/// State management for the application. /// State management for the application.
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq)] #[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq)]
@ -48,7 +49,7 @@ impl ProjectState {
.map_err(|e| anyhow::anyhow!("Error loading project from path {}: {:?}", source_path.display(), e))?; .map_err(|e| anyhow::anyhow!("Error loading project from path {}: {:?}", source_path.display(), e))?;
// Check if we have a main.kcl file in the project. // Check if we have a main.kcl file in the project.
let project_file = source_path.join(DEFAULT_PROJECT_KCL_FILE); let project_file = source_path.join(crate::settings::types::DEFAULT_PROJECT_KCL_FILE);
if !project_file.exists() { if !project_file.exists() {
// Create the default file in the project. // Create the default file in the project.
@ -247,6 +248,8 @@ pub struct Project {
#[serde(default)] #[serde(default)]
#[ts(type = "number")] #[ts(type = "number")]
pub directory_count: u64, pub directory_count: u64,
/// The default file to open on load.
pub default_file: String,
} }
impl Project { impl Project {
@ -266,12 +269,13 @@ impl Project {
} }
let file = crate::settings::utils::walk_dir(&path).await?; let file = crate::settings::utils::walk_dir(&path).await?;
let metadata = std::fs::metadata(path).ok().map(|m| m.into()); let metadata = std::fs::metadata(&path).ok().map(|m| m.into());
let mut project = Self { let mut project = Self {
file, file: file.clone(),
metadata, metadata,
kcl_file_count: 0, kcl_file_count: 0,
directory_count: 0, directory_count: 0,
default_file: get_default_kcl_file_for_dir(path, file).await?,
}; };
project.populate_kcl_file_count()?; project.populate_kcl_file_count()?;
project.populate_directory_count()?; project.populate_directory_count()?;
@ -309,6 +313,40 @@ impl Project {
} }
} }
/// Get the default KCL file for a directory.
/// This determines what the default file to open is.
#[cfg(not(target_arch = "wasm32"))]
#[async_recursion::async_recursion]
pub async fn get_default_kcl_file_for_dir<P>(dir: P, file: FileEntry) -> Result<String>
where
P: AsRef<Path> + Send,
{
// Make sure the dir is a directory.
if !dir.as_ref().is_dir() {
return Err(anyhow::anyhow!("Path `{}` is not a directory", dir.as_ref().display()));
}
let default_file = dir.as_ref().join(crate::settings::types::DEFAULT_PROJECT_KCL_FILE);
if !default_file.exists() {
// Find a kcl file in the directory.
if let Some(children) = file.children {
for entry in children.iter() {
if entry.name.ends_with(".kcl") {
return Ok(dir.as_ref().join(&entry.name).display().to_string());
} else if entry.children.is_some() {
// Recursively find a kcl file in the directory.
return get_default_kcl_file_for_dir(entry.path.clone(), entry.clone()).await;
}
}
}
// If we didn't find a kcl file, create one.
tokio::fs::write(&default_file, vec![]).await?;
}
Ok(default_file.display().to_string())
}
/// Information about a file or directory. /// Information about a file or directory.
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq)] #[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq)]
#[ts(export)] #[ts(export)]
@ -588,4 +626,100 @@ mod tests {
} }
); );
} }
#[test]
fn test_project_route_from_route_non_main_file() {
let mut configuration = crate::settings::types::Configuration::default();
configuration.settings.project.directory =
std::path::PathBuf::from("/Users/macinatormax/Documents/kittycad-modeling-projects");
let route = "/Users/macinatormax/Documents/kittycad-modeling-projects/assembly/thing.kcl";
let state = super::ProjectRoute::from_route(&configuration, route).unwrap();
assert_eq!(
state,
super::ProjectRoute {
project_name: Some("assembly".to_string()),
project_path: "/Users/macinatormax/Documents/kittycad-modeling-projects/assembly".to_string(),
current_file_name: Some("thing.kcl".to_string()),
current_file_path: Some(
"/Users/macinatormax/Documents/kittycad-modeling-projects/assembly/thing.kcl".to_string()
),
}
);
}
#[tokio::test]
async fn test_default_kcl_file_for_dir_non_exist() {
let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
let dir = std::env::temp_dir().join(&name);
std::fs::create_dir_all(&dir).unwrap();
let file = crate::settings::utils::walk_dir(&dir).await.unwrap();
let default_file = super::get_default_kcl_file_for_dir(&dir, file).await.unwrap();
assert_eq!(default_file, dir.join("main.kcl").display().to_string());
std::fs::remove_dir_all(dir).unwrap();
}
#[tokio::test]
async fn test_default_kcl_file_for_dir_main_kcl() {
let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
let dir = std::env::temp_dir().join(&name);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("main.kcl"), vec![]).unwrap();
let file = crate::settings::utils::walk_dir(&dir).await.unwrap();
let default_file = super::get_default_kcl_file_for_dir(&dir, file).await.unwrap();
assert_eq!(default_file, dir.join("main.kcl").display().to_string());
std::fs::remove_dir_all(dir).unwrap();
}
#[tokio::test]
async fn test_default_kcl_file_for_dir_thing_kcl() {
let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
let dir = std::env::temp_dir().join(&name);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("thing.kcl"), vec![]).unwrap();
let file = crate::settings::utils::walk_dir(&dir).await.unwrap();
let default_file = super::get_default_kcl_file_for_dir(&dir, file).await.unwrap();
assert_eq!(default_file, dir.join("thing.kcl").display().to_string());
std::fs::remove_dir_all(dir).unwrap();
}
#[tokio::test]
async fn test_default_kcl_file_for_dir_nested_main_kcl() {
let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
let dir = std::env::temp_dir().join(&name);
std::fs::create_dir_all(&dir).unwrap();
std::fs::create_dir_all(dir.join("assembly")).unwrap();
std::fs::write(dir.join("assembly").join("main.kcl"), vec![]).unwrap();
let file = crate::settings::utils::walk_dir(&dir).await.unwrap();
let default_file = super::get_default_kcl_file_for_dir(&dir, file).await.unwrap();
assert_eq!(
default_file,
dir.join("assembly").join("main.kcl").display().to_string()
);
std::fs::remove_dir_all(dir).unwrap();
}
#[tokio::test]
async fn test_default_kcl_file_for_dir_nested_thing_kcl() {
let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
let dir = std::env::temp_dir().join(&name);
std::fs::create_dir_all(&dir).unwrap();
std::fs::create_dir_all(dir.join("assembly")).unwrap();
std::fs::write(dir.join("assembly").join("thing.kcl"), vec![]).unwrap();
let file = crate::settings::utils::walk_dir(&dir).await.unwrap();
let default_file = super::get_default_kcl_file_for_dir(&dir, file).await.unwrap();
assert_eq!(
default_file,
dir.join("assembly").join("thing.kcl").display().to_string()
);
std::fs::remove_dir_all(dir).unwrap();
}
} }

View File

@ -109,6 +109,7 @@ impl Configuration {
// Because we just created it and it's empty. // Because we just created it and it's empty.
children: None, children: None,
}, },
default_file: project_file.to_string_lossy().to_string(),
metadata: Some(tokio::fs::metadata(&project_dir).await?.into()), metadata: Some(tokio::fs::metadata(&project_dir).await?.into()),
kcl_file_count: 1, kcl_file_count: 1,
directory_count: 0, directory_count: 0,
@ -130,7 +131,13 @@ impl Configuration {
continue; continue;
} }
projects.push(self.get_project_info(&e.path().display().to_string()).await?); // Make sure the project has at least one kcl file in it.
let project = self.get_project_info(&e.path().display().to_string()).await?;
if project.kcl_file_count == 0 {
continue;
}
projects.push(project);
} }
Ok(projects) Ok(projects)
@ -150,11 +157,14 @@ impl Configuration {
return Err(anyhow::anyhow!("Project path is not a directory: {}", project_path)); return Err(anyhow::anyhow!("Project path is not a directory: {}", project_path));
} }
let walked = crate::settings::utils::walk_dir(project_dir).await?;
let mut project = crate::settings::types::file::Project { let mut project = crate::settings::types::file::Project {
file: crate::settings::utils::walk_dir(project_dir).await?, file: walked.clone(),
metadata: Some(tokio::fs::metadata(&project_dir).await?.into()), metadata: Some(tokio::fs::metadata(&project_dir).await?.into()),
kcl_file_count: 0, kcl_file_count: 0,
directory_count: 0, directory_count: 0,
default_file: crate::settings::types::file::get_default_kcl_file_for_dir(project_dir, walked).await?,
}; };
// Populate the number of KCL files in the project. // Populate the number of KCL files in the project.

View File

@ -33,6 +33,16 @@ pub async fn walk_dir<P>(dir: P) -> Result<FileEntry>
where where
P: AsRef<Path> + Send, P: AsRef<Path> + Send,
{ {
// Make sure the path is a directory.
if !dir.as_ref().is_dir() {
return Err(anyhow::anyhow!("Path `{}` is not a directory", dir.as_ref().display()));
}
// Make sure the directory exists.
if !dir.as_ref().exists() {
return Err(anyhow::anyhow!("Directory `{}` does not exist", dir.as_ref().display()));
}
let mut entry = FileEntry { let mut entry = FileEntry {
name: dir name: dir
.as_ref() .as_ref()

View File

@ -533,3 +533,38 @@ pub async fn to_degrees(args: Args) -> Result<MemoryItem, KclError> {
fn inner_to_degrees(num: f64) -> Result<f64, KclError> { fn inner_to_degrees(num: f64) -> Result<f64, KclError> {
Ok(num.to_degrees()) Ok(num.to_degrees())
} }
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn test_inner_max() {
let nums = vec![4.0, 5.0, 6.0];
let result = inner_max(nums);
assert_eq!(result, 6.0);
}
#[test]
fn test_inner_max_with_neg() {
let nums = vec![4.0, -5.0];
let result = inner_max(nums);
assert_eq!(result, 4.0);
}
#[test]
fn test_inner_min() {
let nums = vec![4.0, 5.0, 6.0];
let result = inner_min(nums);
assert_eq!(result, 4.0);
}
#[test]
fn test_inner_min_with_neg() {
let nums = vec![4.0, -5.0];
let result = inner_min(nums);
assert_eq!(result, -5.0);
}
}

View File

@ -68,6 +68,9 @@ lazy_static! {
Box::new(crate::std::sketch::StartSketchAt), Box::new(crate::std::sketch::StartSketchAt),
Box::new(crate::std::sketch::StartSketchOn), Box::new(crate::std::sketch::StartSketchOn),
Box::new(crate::std::sketch::StartProfileAt), Box::new(crate::std::sketch::StartProfileAt),
Box::new(crate::std::sketch::ProfileStartX),
Box::new(crate::std::sketch::ProfileStartY),
Box::new(crate::std::sketch::ProfileStart),
Box::new(crate::std::sketch::Close), Box::new(crate::std::sketch::Close),
Box::new(crate::std::sketch::Arc), Box::new(crate::std::sketch::Arc),
Box::new(crate::std::sketch::TangentialArc), Box::new(crate::std::sketch::TangentialArc),

View File

@ -12,7 +12,7 @@ use crate::{
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
executor::{ executor::{
BasePath, ExtrudeGroup, ExtrudeSurface, Face, GeoMeta, MemoryItem, Path, Plane, PlaneType, Point2d, Point3d, BasePath, ExtrudeGroup, ExtrudeSurface, Face, GeoMeta, MemoryItem, Path, Plane, PlaneType, Point2d, Point3d,
Position, Rotation, SketchGroup, SketchGroupSet, SketchSurface, SourceRange, Position, Rotation, SketchGroup, SketchGroupSet, SketchSurface, SourceRange, UserVal,
}, },
std::{ std::{
utils::{ utils::{
@ -995,27 +995,35 @@ async fn start_sketch_on_face(
args: Args, args: Args,
) -> Result<Box<Face>, KclError> { ) -> Result<Box<Face>, KclError> {
let extrude_plane_id = match tag { let extrude_plane_id = match tag {
SketchOnFaceTag::String(ref s) => extrude_group SketchOnFaceTag::String(ref s) => {
.value if s.is_empty() {
.iter() return Err(KclError::Type(KclErrorDetails {
.find_map(|extrude_surface| match extrude_surface { message: "Expected a non-empty tag for the face to sketch on".to_string(),
ExtrudeSurface::ExtrudePlane(extrude_plane) if extrude_plane.name == *s => {
Some(Ok(extrude_plane.face_id))
}
ExtrudeSurface::ExtrudeArc(extrude_arc) if extrude_arc.name == *s => {
Some(Err(KclError::Type(KclErrorDetails {
message: format!("Cannot sketch on a non-planar surface: `{}`", tag),
source_ranges: vec![args.source_range],
})))
}
ExtrudeSurface::ExtrudePlane(_) | ExtrudeSurface::ExtrudeArc(_) => None,
})
.ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!("Expected a face with the tag `{}`", tag),
source_ranges: vec![args.source_range], source_ranges: vec![args.source_range],
}));
}
extrude_group
.value
.iter()
.find_map(|extrude_surface| match extrude_surface {
ExtrudeSurface::ExtrudePlane(extrude_plane) if extrude_plane.name == *s => {
Some(Ok(extrude_plane.face_id))
}
ExtrudeSurface::ExtrudeArc(extrude_arc) if extrude_arc.name == *s => {
Some(Err(KclError::Type(KclErrorDetails {
message: format!("Cannot sketch on a non-planar surface: `{}`", tag),
source_ranges: vec![args.source_range],
})))
}
ExtrudeSurface::ExtrudePlane(_) | ExtrudeSurface::ExtrudeArc(_) => None,
}) })
})??, .ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!("Expected a face with the tag `{}`", tag),
source_ranges: vec![args.source_range],
})
})??
}
SketchOnFaceTag::StartOrEnd(StartOrEnd::Start) => extrude_group.start_cap_id.ok_or_else(|| { SketchOnFaceTag::StartOrEnd(StartOrEnd::Start) => extrude_group.start_cap_id.ok_or_else(|| {
KclError::Type(KclErrorDetails { KclError::Type(KclErrorDetails {
message: "Expected a start face to sketch on".to_string(), message: "Expected a start face to sketch on".to_string(),
@ -1206,6 +1214,78 @@ pub(crate) async fn inner_start_profile_at(
Ok(Box::new(sketch_group)) Ok(Box::new(sketch_group))
} }
/// Returns the X component of the sketch profile start point.
pub async fn profile_start_x(args: Args) -> Result<MemoryItem, KclError> {
let sketch_group: Box<SketchGroup> = args.get_sketch_group()?;
let x = inner_profile_start_x(sketch_group)?;
args.make_user_val_from_f64(x)
}
/// ```no_run
/// const sketch001 = startSketchOn('XY')
/// |> startProfileAt([5, 2], %)
/// |> angledLine([-26.6, 50], %)
/// |> angledLine([90, 50], %)
/// |> angledLineToX({ angle: 30, to: profileStartX(%) }, %)
/// ```
#[stdlib {
name = "profileStartX"
}]
pub(crate) fn inner_profile_start_x(sketch_group: Box<SketchGroup>) -> Result<f64, KclError> {
Ok(sketch_group.start.to[0])
}
/// Returns the Y component of the sketch profile start point.
pub async fn profile_start_y(args: Args) -> Result<MemoryItem, KclError> {
let sketch_group: Box<SketchGroup> = args.get_sketch_group()?;
let x = inner_profile_start_y(sketch_group)?;
args.make_user_val_from_f64(x)
}
/// ```no_run
/// const sketch001 = startSketchOn('XY')
/// |> startProfileAt([5, 2], %)
/// |> angledLine({ angle: -60, length: 14 }, %)
/// |> angledLineToY({ angle: 30, to: profileStartY(%) }, %)
/// ```
#[stdlib {
name = "profileStartY"
}]
pub(crate) fn inner_profile_start_y(sketch_group: Box<SketchGroup>) -> Result<f64, KclError> {
Ok(sketch_group.start.to[1])
}
/// Returns the sketch profile start point.
pub async fn profile_start(args: Args) -> Result<MemoryItem, KclError> {
let sketch_group: Box<SketchGroup> = args.get_sketch_group()?;
let point = inner_profile_start(sketch_group)?;
Ok(MemoryItem::UserVal(UserVal {
value: serde_json::to_value(point).map_err(|e| {
KclError::Type(KclErrorDetails {
message: format!("Failed to convert point to json: {}", e),
source_ranges: vec![args.source_range],
})
})?,
meta: Default::default(),
}))
}
/// ```no_run
/// const sketch001 = startSketchOn('XY')
/// |> startProfileAt([5, 2], %)
/// |> angledLine({ angle: 120, length: 50 }, %, 'seg01')
/// |> angledLine({ angle: segAng('seg01', %) + 120, length: 50 }, %)
/// |> lineTo(profileStart(%), %)
/// |> close(%)
/// |> extrude(20, %)
/// ```
#[stdlib {
name = "profileStart"
}]
pub(crate) fn inner_profile_start(sketch_group: Box<SketchGroup>) -> Result<[f64; 2], KclError> {
Ok(sketch_group.start.to)
}
/// Close the current sketch. /// Close the current sketch.
pub async fn close(args: Args) -> Result<MemoryItem, KclError> { pub async fn close(args: Args) -> Result<MemoryItem, KclError> {
let (sketch_group, tag): (Box<SketchGroup>, Option<String>) = args.get_sketch_group_and_optional_tag()?; let (sketch_group, tag): (Box<SketchGroup>, Option<String>) = args.get_sketch_group_and_optional_tag()?;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 214 KiB

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

View File

@ -2057,3 +2057,58 @@ const bracket = startSketchOn('XY')
r#"engine: KclErrorDetails { source_ranges: [SourceRange([1443, 1443])], message: "Modeling command failed: Some([ApiError { error_code: BadRequest, message: \"Fillet failed\" }])" }"# r#"engine: KclErrorDetails { source_ranges: [SourceRange([1443, 1443])], message: "Modeling command failed: Some([ApiError { error_code: BadRequest, message: \"Fillet failed\" }])" }"#
); );
} }
#[tokio::test(flavor = "multi_thread")]
async fn serial_test_error_empty_start_sketch_on_string() {
let code = r#"const part001 = startSketchOn('-XZ')
|> startProfileAt([75.75, 184.25], %)
|> line([190.03, -118.13], %)
|> line([-33.38, -202.86], %)
|> line([-315.86, -64.2], %)
|> tangentialArcTo([-147.66, 121.34], %)
|> close(%)
|> extrude(100, %)
const secondSketch = startSketchOn(part001, '')
|> circle([-20, 50], 40, %)
|> extrude(20, %)
"#;
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm).await;
assert!(result.is_err());
assert_eq!(
result.err().unwrap().to_string(),
r#"type: KclErrorDetails { source_ranges: [SourceRange([272, 298])], message: "Expected a non-empty tag for the face to sketch on" }"#
);
}
#[tokio::test(flavor = "multi_thread")]
async fn serial_test_error_user_function_wrong_args() {
let code = r#"const length = .750
const width = 0.500
const height = 0.500
const dia = 4
fn squareHole = (l, w) => {
const squareHoleSketch = startSketchOn('XY')
|> startProfileAt([-width / 2, -length / 2], %)
|> lineTo([width / 2, -length / 2], %)
|> lineTo([width / 2, length / 2], %)
|> lineTo([-width / 2, length / 2], %)
|> close(%)
return squareHoleSketch
}
const extrusion = startSketchOn('XY')
|> circle([0, 0], dia/2, %)
|> hole(squareHole(length, width, height), %)
|> extrude(height, %)
"#;
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm).await;
assert!(result.is_err());
assert_eq!(
result.err().unwrap().to_string(),
r#"semantic: KclErrorDetails { source_ranges: [SourceRange([92, 364]), SourceRange([444, 477])], message: "Expected 2 arguments, got 3" }"#
);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 KiB

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 214 KiB

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 211 KiB

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 173 KiB

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 198 KiB

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

After

Width:  |  Height:  |  Size: 127 KiB

View File

@ -77,16 +77,6 @@ async fn setup(code: &str, name: &str) -> Result<(ExecutorContext, Program, uuid
) )
.await?; .await?;
// Enter edit mode.
// We can't get control points of an existing sketch without being in edit mode.
ctx.engine
.send_modeling_cmd(
uuid::Uuid::new_v4(),
SourceRange::default(),
ModelingCmd::EditModeEnter { target: sketch_id },
)
.await?;
Ok((ctx, program, sketch_id)) Ok((ctx, program, sketch_id))
} }

137
yarn.lock
View File

@ -1970,10 +1970,10 @@
"@react-hook/latest" "^1.0.2" "@react-hook/latest" "^1.0.2"
"@react-hook/passive-layout-effect" "^1.2.0" "@react-hook/passive-layout-effect" "^1.2.0"
"@remix-run/router@1.15.3": "@remix-run/router@1.16.1":
version "1.15.3" version "1.16.1"
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.15.3.tgz#d2509048d69dbb72d5389a14945339f1430b2d3c" resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.16.1.tgz#73db3c48b975eeb06d0006481bde4f5f2d17d1cd"
integrity sha512-Oy8rmScVrVxWZVOpEF57ovlnhpZ8CCPlnIIumVcV9nFdiSIrus99+Lw78ekXyGvVDlIsFJbSfmSovJUhCWYV3w== integrity sha512-es2g3dq6Nb07iFxGk5GuHN20RwBZOsuDQN7izWIisUcv9r+d2C5jQxqmgkdebXgReWfiyUabcki6Fg77mSNrig==
"@replit/codemirror-interact@^6.3.1": "@replit/codemirror-interact@^6.3.1":
version "6.3.1" version "6.3.1"
@ -2106,6 +2106,11 @@
resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.2.0.tgz#874d36135e4badce2719e7bdc556ce240cbaff14" resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.2.0.tgz#874d36135e4badce2719e7bdc556ce240cbaff14"
integrity sha512-P5XgYoAw/vfW65byBbJQCw+cagdXDT/qH6wmABiLt4v4YBT2q2vqCOhihe+D1Nt325F/S/0Tkv6C5z0Lv+VBQQ== integrity sha512-P5XgYoAw/vfW65byBbJQCw+cagdXDT/qH6wmABiLt4v4YBT2q2vqCOhihe+D1Nt325F/S/0Tkv6C5z0Lv+VBQQ==
"@tauri-apps/api@2.0.0-beta.11":
version "2.0.0-beta.11"
resolved "https://registry.yarnpkg.com/@tauri-apps/api/-/api-2.0.0-beta.11.tgz#7ec6f6f007da4e55315270619448ce39fd11b4e3"
integrity sha512-wJRY+fBUm3KpqZDHMIz5HRv+1vlnvRJ/dFxiyY3NlINTx2qXqDou5qWYcP1CuZXsd39InWVPV3FAZvno/kGCkA==
"@tauri-apps/api@2.0.0-beta.4": "@tauri-apps/api@2.0.0-beta.4":
version "2.0.0-beta.4" version "2.0.0-beta.4"
resolved "https://registry.yarnpkg.com/@tauri-apps/api/-/api-2.0.0-beta.4.tgz#7688950f6e03f38b3bac73585f8f4cdd61be6aa6" resolved "https://registry.yarnpkg.com/@tauri-apps/api/-/api-2.0.0-beta.4.tgz#7688950f6e03f38b3bac73585f8f4cdd61be6aa6"
@ -2189,12 +2194,12 @@
dependencies: dependencies:
"@tauri-apps/api" "2.0.0-beta.4" "@tauri-apps/api" "2.0.0-beta.4"
"@tauri-apps/plugin-fs@^2.0.0-beta.2": "@tauri-apps/plugin-fs@^2.0.0-beta.3":
version "2.0.0-beta.2" version "2.0.0-beta.3"
resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-fs/-/plugin-fs-2.0.0-beta.2.tgz#b2dfcd72422f778e4c32edcfad24c1e96299c760" resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-fs/-/plugin-fs-2.0.0-beta.3.tgz#461628704a10ce5177c49f3f65153aa9ecd8df3b"
integrity sha512-jqeRBrm0h9QUoep5OzHx5R0vgFCYVAmZIy45jJpR7hHvnEgUwDU8JLUUVPvWniq6tUtxjwr1V/a0Hm9pE9V+NQ== integrity sha512-LBgA7S10NwcitHaugIfmCSkewz45vSz1VOpMHhzvE38i1r1KpuTSHlr3MZ0LLq93tH/lvhYZ+3LAml4Sriwthw==
dependencies: dependencies:
"@tauri-apps/api" "2.0.0-beta.4" "@tauri-apps/api" "2.0.0-beta.11"
"@tauri-apps/plugin-http@^2.0.0-beta.2": "@tauri-apps/plugin-http@^2.0.0-beta.2":
version "2.0.0-beta.2" version "2.0.0-beta.2"
@ -2224,12 +2229,12 @@
dependencies: dependencies:
"@tauri-apps/api" "2.0.0-beta.4" "@tauri-apps/api" "2.0.0-beta.4"
"@tauri-apps/plugin-updater@^2.0.0-beta.2": "@tauri-apps/plugin-updater@^2.0.0-beta.3":
version "2.0.0-beta.2" version "2.0.0-beta.3"
resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-updater/-/plugin-updater-2.0.0-beta.2.tgz#60002e54ad647a56db5e1b0b54e792f399d425a4" resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-updater/-/plugin-updater-2.0.0-beta.3.tgz#d6403363e540005e4bd38ff8d0cd689f40db96d0"
integrity sha512-T8EkAXawbyV/6/Lcf1VVIWhtGuals63zKn+udYNqlC8CRM5iYQ+8bM8Nmy2E+pIzkkx93d1t6/8geFitLZPmKw== integrity sha512-bD1ikPz80uK9YJKNYpYlA6StSp9lr0Ob1kGLG2XdmOgspv7SZLLNVzMORtKeqgepxwG99qdYGDDegT3Ll6+UlA==
dependencies: dependencies:
"@tauri-apps/api" "2.0.0-beta.4" "@tauri-apps/api" "2.0.0-beta.11"
"@testing-library/dom@^10.0.0": "@testing-library/dom@^10.0.0":
version "10.0.0" version "10.0.0"
@ -2720,44 +2725,44 @@
"@types/babel__core" "^7.20.5" "@types/babel__core" "^7.20.5"
react-refresh "^0.14.0" react-refresh "^0.14.0"
"@vitest/expect@1.5.0": "@vitest/expect@1.6.0":
version "1.5.0" version "1.6.0"
resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-1.5.0.tgz#961190510a2723bd4abf5540bcec0a4dfd59ef14" resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-1.6.0.tgz#0b3ba0914f738508464983f4d811bc122b51fb30"
integrity sha512-0pzuCI6KYi2SIC3LQezmxujU9RK/vwC1U9R0rLuGlNGcOuDWxqWKu6nUdFsX9tH1WU0SXtAxToOsEjeUn1s3hA== integrity sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==
dependencies: dependencies:
"@vitest/spy" "1.5.0" "@vitest/spy" "1.6.0"
"@vitest/utils" "1.5.0" "@vitest/utils" "1.6.0"
chai "^4.3.10" chai "^4.3.10"
"@vitest/runner@1.5.0": "@vitest/runner@1.6.0":
version "1.5.0" version "1.6.0"
resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-1.5.0.tgz#1f7cb78ee4064e73e53d503a19c1b211c03dfe0c" resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-1.6.0.tgz#a6de49a96cb33b0e3ba0d9064a3e8d6ce2f08825"
integrity sha512-7HWwdxXP5yDoe7DTpbif9l6ZmDwCzcSIK38kTSIt6CFEpMjX4EpCgT6wUmS0xTXqMI6E/ONmfgRKmaujpabjZQ== integrity sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==
dependencies: dependencies:
"@vitest/utils" "1.5.0" "@vitest/utils" "1.6.0"
p-limit "^5.0.0" p-limit "^5.0.0"
pathe "^1.1.1" pathe "^1.1.1"
"@vitest/snapshot@1.5.0", "@vitest/snapshot@^1.2.2": "@vitest/snapshot@1.6.0", "@vitest/snapshot@^1.2.2":
version "1.5.0" version "1.6.0"
resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-1.5.0.tgz#cd2d611fd556968ce8fb6b356a09b4593c525947" resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-1.6.0.tgz#deb7e4498a5299c1198136f56e6e0f692e6af470"
integrity sha512-qpv3fSEuNrhAO3FpH6YYRdaECnnRjg9VxbhdtPwPRnzSfHVXnNzzrpX4cJxqiwgRMo7uRMWDFBlsBq4Cr+rO3A== integrity sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==
dependencies: dependencies:
magic-string "^0.30.5" magic-string "^0.30.5"
pathe "^1.1.1" pathe "^1.1.1"
pretty-format "^29.7.0" pretty-format "^29.7.0"
"@vitest/spy@1.5.0": "@vitest/spy@1.6.0":
version "1.5.0" version "1.6.0"
resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-1.5.0.tgz#1369a1bec47f46f18eccfa45f1e8fbb9b5e15e77" resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-1.6.0.tgz#362cbd42ccdb03f1613798fde99799649516906d"
integrity sha512-vu6vi6ew5N5MMHJjD5PoakMRKYdmIrNJmyfkhRpQt5d9Ewhw9nZ5Aqynbi3N61bvk9UvZ5UysMT6ayIrZ8GA9w== integrity sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==
dependencies: dependencies:
tinyspy "^2.2.0" tinyspy "^2.2.0"
"@vitest/utils@1.5.0": "@vitest/utils@1.6.0":
version "1.5.0" version "1.6.0"
resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-1.5.0.tgz#90c9951f4516f6d595da24876b58e615f6c99863" resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-1.6.0.tgz#5c5675ca7d6f546a7b4337de9ae882e6c57896a1"
integrity sha512-BDU0GNL8MWkRkSRdNFvCUCAVOeHaUlVJ9Tx0TYBZyXaaOTmGtUFObzchCivIBrIwKzvZA7A9sCejVhXM2aY98A== integrity sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==
dependencies: dependencies:
diff-sequences "^29.6.3" diff-sequences "^29.6.3"
estree-walker "^3.0.3" estree-walker "^3.0.3"
@ -7614,20 +7619,20 @@ react-refresh@^0.14.0:
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.14.0.tgz#4e02825378a5f227079554d4284889354e5f553e" resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.14.0.tgz#4e02825378a5f227079554d4284889354e5f553e"
integrity sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ== integrity sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==
react-router-dom@^6.22.3: react-router-dom@^6.23.1:
version "6.22.3" version "6.23.1"
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.22.3.tgz#9781415667fd1361a475146c5826d9f16752a691" resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.23.1.tgz#30cbf266669693e9492aa4fc0dde2541ab02322f"
integrity sha512-7ZILI7HjcE+p31oQvwbokjk6OA/bnFxrhJ19n82Ex9Ph8fNAq+Hm/7KchpMGlTgWhUxRHMMCut+vEtNpWpowKw== integrity sha512-utP+K+aSTtEdbWpC+4gxhdlPFwuEfDKq8ZrPFU65bbRJY+l706qjR7yaidBpo3MSeA/fzwbXWbKBI6ftOnP3OQ==
dependencies: dependencies:
"@remix-run/router" "1.15.3" "@remix-run/router" "1.16.1"
react-router "6.22.3" react-router "6.23.1"
react-router@6.22.3: react-router@6.23.1:
version "6.22.3" version "6.23.1"
resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.22.3.tgz#9d9142f35e08be08c736a2082db5f0c9540a885e" resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.23.1.tgz#d08cbdbd9d6aedc13eea6e94bc6d9b29cb1c4be9"
integrity sha512-dr2eb3Mj5zK2YISHK++foM9w4eBnO23eKnZEDs7c880P6oKbrjz/Svg9+nxqtHQK+oMW4OtjZca0RqPglXxguQ== integrity sha512-fzcOaRF69uvqbbM7OhvQyBTFDVrrGlsFdS3AL+1KfIBtGETibHzi3FkoTRyiDJnWNc2VxrfvR+657ROHjaNjqQ==
dependencies: dependencies:
"@remix-run/router" "1.15.3" "@remix-run/router" "1.16.1"
react-textarea-autosize@^8.3.2: react-textarea-autosize@^8.3.2:
version "8.5.2" version "8.5.2"
@ -8469,10 +8474,10 @@ thenify-all@^1.0.0:
dependencies: dependencies:
any-promise "^1.0.0" any-promise "^1.0.0"
three@^0.163.0: three@^0.164.1:
version "0.163.0" version "0.164.1"
resolved "https://registry.yarnpkg.com/three/-/three-0.163.0.tgz#cbfefbfd64a1353ab7cc8bf0fc396ddca1875a49" resolved "https://registry.yarnpkg.com/three/-/three-0.164.1.tgz#b742f76bd8dfd3736ba0d86a12dfddb73c5cdcc0"
integrity sha512-HlMgCb2TF/dTLRtknBnjUTsR8FsDqBY43itYop2+Zg822I+Kd0Ua2vs8CvfBVefXkBdNDrLMoRTGCIIpfCuDew== integrity sha512-iC/hUBbl1vzFny7f5GtqzVXYjMJKaTPxiCxXfrvVdBi1Sf+jhd1CAkitiFwC7mIBFCo3MrDLJG97yisoaWig0w==
through@^2.3.8: through@^2.3.8:
version "2.3.8" version "2.3.8"
@ -8857,10 +8862,10 @@ validate-npm-package-license@^3.0.4:
spdx-correct "^3.0.0" spdx-correct "^3.0.0"
spdx-expression-parse "^3.0.0" spdx-expression-parse "^3.0.0"
vite-node@1.5.0: vite-node@1.6.0:
version "1.5.0" version "1.6.0"
resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-1.5.0.tgz#7f74dadfecb15bca016c5ce5ef85e5cc4b82abf2" resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-1.6.0.tgz#2c7e61129bfecc759478fa592754fd9704aaba7f"
integrity sha512-tV8h6gMj6vPzVCa7l+VGq9lwoJjW8Y79vst8QZZGiuRAfijU+EEWuc0kFpmndQrWhMMhet1jdSF+40KSZUqIIw== integrity sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==
dependencies: dependencies:
cac "^6.7.14" cac "^6.7.14"
debug "^4.3.4" debug "^4.3.4"
@ -8910,16 +8915,16 @@ vitest-webgl-canvas-mock@^1.1.0:
cssfontparser "^1.2.1" cssfontparser "^1.2.1"
parse-color "^1.0.0" parse-color "^1.0.0"
vitest@^1.5.0: vitest@^1.6.0:
version "1.5.0" version "1.6.0"
resolved "https://registry.yarnpkg.com/vitest/-/vitest-1.5.0.tgz#6ebb396bd358650011a9c96c18fa614b668365c1" resolved "https://registry.yarnpkg.com/vitest/-/vitest-1.6.0.tgz#9d5ad4752a3c451be919e412c597126cffb9892f"
integrity sha512-d8UKgR0m2kjdxDWX6911uwxout6GHS0XaGH1cksSIVVG8kRlE7G7aBw7myKQCvDI5dT4j7ZMa+l706BIORMDLw== integrity sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==
dependencies: dependencies:
"@vitest/expect" "1.5.0" "@vitest/expect" "1.6.0"
"@vitest/runner" "1.5.0" "@vitest/runner" "1.6.0"
"@vitest/snapshot" "1.5.0" "@vitest/snapshot" "1.6.0"
"@vitest/spy" "1.5.0" "@vitest/spy" "1.6.0"
"@vitest/utils" "1.5.0" "@vitest/utils" "1.6.0"
acorn-walk "^8.3.2" acorn-walk "^8.3.2"
chai "^4.3.10" chai "^4.3.10"
debug "^4.3.4" debug "^4.3.4"
@ -8933,7 +8938,7 @@ vitest@^1.5.0:
tinybench "^2.5.1" tinybench "^2.5.1"
tinypool "^0.8.3" tinypool "^0.8.3"
vite "^5.0.0" vite "^5.0.0"
vite-node "1.5.0" vite-node "1.6.0"
why-is-node-running "^2.2.2" why-is-node-running "^2.2.2"
vscode-jsonrpc@8.2.0, vscode-jsonrpc@^8.1.0: vscode-jsonrpc@8.2.0, vscode-jsonrpc@^8.1.0: