Merge branch 'main' into coredump-enginecommandmanager
19
README.md
@ -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).
|
||||||
|
@ -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)
|
||||||
|
206
docs/kcl/profileStart.md
Normal file
201
docs/kcl/profileStartX.md
Normal file
200
docs/kcl/profileStartY.md
Normal file
2972
docs/kcl/std.json
@ -424,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,
|
||||||
}) => {
|
}) => {
|
||||||
@ -646,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')
|
||||||
@ -775,6 +849,57 @@ 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('Click through each onboarding step', async ({ page }) => {
|
test('Click through each onboarding step', async ({ page }) => {
|
||||||
const u = getUtils(page)
|
const u = getUtils(page)
|
||||||
|
|
||||||
@ -1093,6 +1218,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(
|
||||||
|
10
package.json
@ -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",
|
||||||
|
@ -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: '|',
|
||||||
|
@ -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' })
|
||||||
|
@ -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 (
|
||||||
<>
|
<>
|
||||||
|
@ -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(
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -551,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)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
37
src/lib/hotkeyWrapper.ts
Normal 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')
|
||||||
|
}
|
40
src/wasm-lib/Cargo.lock
generated
@ -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]]
|
||||||
|
@ -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" }
|
||||||
|
20
src/wasm-lib/kcl/README.md
Normal 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.
|
@ -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),
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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),
|
||||||
|
@ -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()?;
|
||||||
|
Before Width: | Height: | Size: 214 KiB After Width: | Height: | Size: 214 KiB |
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 120 KiB |
After Width: | Height: | Size: 106 KiB |
After Width: | Height: | Size: 109 KiB |
After Width: | Height: | Size: 95 KiB |
@ -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" }"#
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 144 KiB After Width: | Height: | Size: 145 KiB |
Before Width: | Height: | Size: 136 KiB After Width: | Height: | Size: 136 KiB |
Before Width: | Height: | Size: 140 KiB After Width: | Height: | Size: 140 KiB |
Before Width: | Height: | Size: 123 KiB After Width: | Height: | Size: 123 KiB |
Before Width: | Height: | Size: 134 KiB After Width: | Height: | Size: 134 KiB |
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 117 KiB |
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 103 KiB |
Before Width: | Height: | Size: 127 KiB After Width: | Height: | Size: 127 KiB |
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 103 KiB |
Before Width: | Height: | Size: 214 KiB After Width: | Height: | Size: 214 KiB |
Before Width: | Height: | Size: 211 KiB After Width: | Height: | Size: 212 KiB |
Before Width: | Height: | Size: 173 KiB After Width: | Height: | Size: 173 KiB |
Before Width: | Height: | Size: 147 KiB After Width: | Height: | Size: 147 KiB |
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 105 KiB |
137
yarn.lock
@ -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:
|
||||||
|