Compare commits
24 Commits
update-sna
...
v0.21.6
Author | SHA1 | Date | |
---|---|---|---|
440eb2636a | |||
344e72d7ec | |||
ec7b733a0d | |||
63159c1cb8 | |||
df62a995b5 | |||
fa762c1c4d | |||
82b03a9d47 | |||
793b7407f6 | |||
040bcc2c09 | |||
ae2e219394 | |||
a83f549257 | |||
3871d2858f | |||
3effb87f8e | |||
3f2f035a9b | |||
4735eaef8c | |||
69f8da058a | |||
93ebf13621 | |||
20c4d44b8b | |||
8ea8f80e32 | |||
d73339fd8d | |||
031b230690 | |||
1125d74f12 | |||
5c7a2822d0 | |||
d44b1f8e54 |
17
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,7 +278,6 @@ 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
|
||||||
@ -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
@ -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,
|
||||||
|
''
|
||||||
|
)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 41 KiB |
12
package.json
@ -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
@ -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"
|
||||||
|
@ -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
|
||||||
|
@ -74,5 +74,5 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"productName": "Zoo Modeling App",
|
"productName": "Zoo Modeling App",
|
||||||
"version": "0.21.4"
|
"version": "0.21.6"
|
||||||
}
|
}
|
||||||
|
@ -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: '|',
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
@ -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' })
|
||||||
|
@ -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
|
||||||
|
@ -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 (
|
||||||
<>
|
<>
|
||||||
|
@ -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) => {
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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'}
|
||||||
/>
|
/>
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -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)
|
<ProjectMenuPopover project={project} file={file} />
|
||||||
// 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
|
<span
|
||||||
className="hidden text-sm text-chalkboard-110 dark:text-chalkboard-20 whitespace-nowrap lg:block"
|
className="hidden select-none cursor-default text-sm text-chalkboard-110 dark:text-chalkboard-20 whitespace-nowrap lg:block"
|
||||||
data-testid="project-sidebar-link-name"
|
data-testid="project-name"
|
||||||
>
|
>
|
||||||
{project?.name ? project.name : APP_NAME}
|
{project?.name ? project.name : APP_NAME}
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<ProjectMenuPopover project={project} file={file} />
|
|
||||||
)}
|
)}
|
||||||
</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,
|
||||||
|
@ -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),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
) {
|
) {
|
||||||
|
@ -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
|
||||||
|
@ -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
@ -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')
|
||||||
|
}
|
@ -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,
|
||||||
|
@ -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') {
|
||||||
|
@ -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(),
|
||||||
|
@ -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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
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]]
|
||||||
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 80 KiB |
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 82 KiB |
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 79 KiB |
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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.
|
||||||
|
@ -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()
|
||||||
|
@ -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,7 +995,14 @@ 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) => {
|
||||||
|
if s.is_empty() {
|
||||||
|
return Err(KclError::Type(KclErrorDetails {
|
||||||
|
message: "Expected a non-empty tag for the face to sketch on".to_string(),
|
||||||
|
source_ranges: vec![args.source_range],
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
extrude_group
|
||||||
.value
|
.value
|
||||||
.iter()
|
.iter()
|
||||||
.find_map(|extrude_surface| match extrude_surface {
|
.find_map(|extrude_surface| match extrude_surface {
|
||||||
@ -1015,7 +1022,8 @@ async fn start_sketch_on_face(
|
|||||||
message: format!("Expected a face with the tag `{}`", tag),
|
message: format!("Expected a face with the tag `{}`", tag),
|
||||||
source_ranges: vec![args.source_range],
|
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: 83 KiB After Width: | Height: | Size: 82 KiB |
Before Width: | Height: | Size: 214 KiB After Width: | Height: | Size: 214 KiB |
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 120 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: 83 KiB After Width: | Height: | Size: 83 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: 198 KiB After Width: | Height: | Size: 198 KiB |
Before Width: | Height: | Size: 132 KiB After Width: | Height: | Size: 148 KiB |
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 108 KiB |
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 102 KiB |
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 92 KiB |
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 105 KiB |
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 100 KiB |
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 100 KiB |
Before Width: | Height: | Size: 127 KiB After Width: | Height: | Size: 127 KiB |
@ -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
@ -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:
|
||||||
|