Compare commits

...

18 Commits

Author SHA1 Message Date
d2a3bfdb5a Revert "Ensure auth device token is saved to a file so it persists upgrades and reinstalls (#3640)"
This reverts commit f6bb10170d.
2024-08-27 10:12:25 +10:00
dbdc7e5c8b add maker wix (#3668)
* add maker wix

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

* updates

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-08-26 13:54:18 -07:00
f6bb10170d Ensure auth device token is saved to a file so it persists upgrades and reinstalls (#3640)
* ensure auth device token is saved to a file so it persists upgrades and reinstalls
#3639

* write file on check log in

* write file on check log in

* clean up
2024-08-27 05:59:25 +10:00
972dca8743 Change mouse controls display to be easier to understand (#3648)
* Change mouse controls display to be easier to understand

* Fix to not duplicate default camera controls

* Change "Scroll wheel" to "Scroll" on all platforms
2024-08-26 15:05:33 -04:00
e9e933eecd remove priviledged schemes (#3666)
updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-08-26 18:27:20 +00:00
2b1315423f Bump serde_json from 1.0.125 to 1.0.127 in /src/wasm-lib (#3662)
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.125 to 1.0.127.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/1.0.125...1.0.127)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-26 10:03:11 -07:00
bd4c24bc04 Remove references to src-tauri (#3663)
* Remove references to src-tauri

* Remove more ignores from the electron migration
2024-08-26 16:31:28 +00:00
50cc88977c Bump micromatch from 4.0.7 to 4.0.8 (#3649)
Bumps [micromatch](https://github.com/micromatch/micromatch) from 4.0.7 to 4.0.8.
- [Release notes](https://github.com/micromatch/micromatch/releases)
- [Changelog](https://github.com/micromatch/micromatch/blob/4.0.8/CHANGELOG.md)
- [Commits](https://github.com/micromatch/micromatch/compare/4.0.7...4.0.8)

---
updated-dependencies:
- dependency-name: micromatch
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-26 16:16:23 +00:00
bea9a1c3ec Bump syn from 2.0.75 to 2.0.76 in /src/wasm-lib (#3659)
Bumps [syn](https://github.com/dtolnay/syn) from 2.0.75 to 2.0.76.
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/2.0.75...2.0.76)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-26 08:56:37 -07:00
f43411fdb4 Bump serde from 1.0.208 to 1.0.209 in /src/wasm-lib (#3661)
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.208 to 1.0.209.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.208...v1.0.209)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-26 08:55:52 -07:00
max
c2e9d18f92 3036 tests add fillet (#3530)
* addFillet.ts - refactor existing code

* move logic from modelingMachine to addFillet

* rename getPathForSelection into getPathToExtrudeForSegmentSelection

* stuck with kclManager

* stuck 2

* remove engineless exe from fillet test

* pathToExtrudeNode properly tested

* resolve conflicts

* engine initialization update

* cleanup comments

* passed ExecuteArgs instead of Program to executeAst

* afterAll engineCommandManager.tearDown

* resolve conflicts

* mutateAstForRadiusInsertion

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

* save banner from hulk mutations

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

* sweet errors

* purging the as

* make type of getNodeFromPath safe again

* as cleaning part 2

* cleared mutation logic

* last bits

* make the linter happy

---------

Co-authored-by: Kurt Hutten Irev-Dev <k.hutten@protonmail.ch>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-08-26 08:07:20 +02:00
199722c505 fix and tests (#3656)
* fix and tests

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

* fixes

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-08-25 22:14:38 +00:00
f9699d174c header changes and open new window for double click in finder macos (#3652)
* header changes and open new window for double click in finder macos

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

* cleanup

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

* add protocols

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

* updates

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

* extend with info.plist

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

* fixes

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-08-25 19:56:11 +00:00
590a6479e0 double-click to open / open from cli (#3643)
* fixes

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

* add tests

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

* updates

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

* Look at this (photo)Graph *in the voice of Nickelback*

* remove unneeded rust

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

* remove dep on clap

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

* fixups for imports

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

* updates

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

* fix types

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

* bump

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-08-25 00:47:09 +00:00
fbf0d3d953 Nadro/3581/change base unit test (#3621)
* fix: Updating the playwright tests section

* fix: cleaning up formatting for playwright test markdown

* fix: autocomplete put a 3rd *

* chore: e2e playwright for change of base unit in multiple scenarios

* fix: fmt auto format

* fix: fmt and clean up for PR

* fix: removing typo

* fix: added the ) formatting back

* fix: linting errors + formatting

* fix: removing unused testing code from previous logic

---------

Co-authored-by: Jess Frazelle <jessfraz@users.noreply.github.com>
2024-08-24 23:51:50 +00:00
3dd66bc8d2 Add test for not creating main.kcl when there are nested directories (#3647)
Co-authored-by: Jess Frazelle <jessfraz@users.noreply.github.com>
2024-08-24 23:51:36 +00:00
a928b8fbd0 Update README.md to add updating upstreams for kcl-lib (#3650)
Update README.md
2024-08-24 16:33:01 -07:00
d2349bec2b chore: implemented e2e tests for text-to-cad file workflows (#3645) 2024-08-24 15:31:27 -07:00
54 changed files with 1928 additions and 1862 deletions

View File

@ -1,3 +1,3 @@
[codespell]
ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,absolutey,atleast,ue,afterall
skip: **/target,node_modules,build,**/Cargo.lock,./docs/kcl/*.md,./src-tauri/gen/schemas,.yarn.lock,**/yarn.lock
skip: **/target,node_modules,build,**/Cargo.lock,./docs/kcl/*.md,.yarn.lock,**/yarn.lock

View File

@ -37,4 +37,4 @@ jobs:
# We specifically want to test the disable-println feature
# Since it is not enabled by default, we need to specify it
# This is used in kcl-lsp
cargo check --all --features disable-println --features pyo3
cargo check --all --features disable-println --features pyo3 --features cli

View File

@ -38,11 +38,6 @@ jobs:
with:
toolchain: stable
override: true
- name: install dependencies
if: matrix.dir == 'src-tauri'
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
- name: Install vector
run: |
curl --proto '=https' --tlsv1.2 -sSfL https://sh.vector.dev > /tmp/vector.sh

5
.gitignore vendored
View File

@ -54,7 +54,6 @@ e2e/playwright/export-snapshots/*
## generated files
src/**/*.typegen.ts
src-tauri/gen
src/wasm-lib/grackle/stdlib_cube_partial.json
Mac_App_Distribution.provisionprofile
@ -66,7 +65,3 @@ venv
# electron
out/
src-tauri/target
electron-test-projects-dir
electron-test-projects-dir-2

344
Info.plist Normal file
View File

@ -0,0 +1,344 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>LSItemContentTypes</key>
<array>
<string>dev.zoo.kcl</string>
</array>
<key>CFBundleTypeName</key>
<string>KCL</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSTypeIsPackage</key>
<false/>
<key>LSHandlerRank</key>
<string>Owner</string>
</dict>
<dict>
<key>LSItemContentTypes</key>
<array>
<string>dev.zoo.toml</string>
</array>
<key>CFBundleTypeName</key>
<string>TOML</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSTypeIsPackage</key>
<false/>
<key>LSHandlerRank</key>
<string>Default</string>
</dict>
<dict>
<key>LSItemContentTypes</key>
<array>
<string>dev.zoo.gltf</string>
</array>
<key>CFBundleTypeName</key>
<string>glTF</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSTypeIsPackage</key>
<false/>
<key>LSHandlerRank</key>
<string>Default</string>
</dict>
<dict>
<key>LSItemContentTypes</key>
<array>
<string>dev.zoo.glb</string>
</array>
<key>CFBundleTypeName</key>
<string>glb</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSTypeIsPackage</key>
<false/>
<key>LSHandlerRank</key>
<string>Default</string>
</dict>
<dict>
<key>LSItemContentTypes</key>
<array>
<string>dev.zoo.step</string>
</array>
<key>CFBundleTypeName</key>
<string>STEP</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSTypeIsPackage</key>
<false/>
<key>LSHandlerRank</key>
<string>Default</string>
</dict>
<dict>
<key>LSItemContentTypes</key>
<array>
<string>dev.zoo.fbx</string>
</array>
<key>CFBundleTypeName</key>
<string>FBX</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSTypeIsPackage</key>
<false/>
<key>LSHandlerRank</key>
<string>Default</string>
</dict>
<dict>
<key>LSItemContentTypes</key>
<array>
<string>dev.zoo.sldprt</string>
</array>
<key>CFBundleTypeName</key>
<string>Solidworks Part</string>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>LSTypeIsPackage</key>
<false/>
<key>LSHandlerRank</key>
<string>Default</string>
</dict>
<dict>
<key>LSItemContentTypes</key>
<array>
<string>public.geometry-definition-format</string>
</array>
<key>CFBundleTypeName</key>
<string>OBJ</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSTypeIsPackage</key>
<false/>
<key>LSHandlerRank</key>
<string>Default</string>
</dict>
<dict>
<key>LSItemContentTypes</key>
<array>
<string>public.polygon-file-format</string>
</array>
<key>CFBundleTypeName</key>
<string>PLY</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSTypeIsPackage</key>
<false/>
<key>LSHandlerRank</key>
<string>Default</string>
</dict>
<dict>
<key>LSItemContentTypes</key>
<array>
<string>public.standard-tesselated-geometry-format</string>
</array>
<key>CFBundleTypeName</key>
<string>STL</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSTypeIsPackage</key>
<false/>
<key>LSHandlerRank</key>
<string>Default</string>
</dict>
<dict>
<key>LSItemContentTypes</key>
<array>
<string>public.folder</string>
</array>
<key>CFBundleTypeName</key>
<string>Folders</string>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>LSHandlerRank</key>
<string>Alternate</string>
</dict>
</array>
<key>UTExportedTypeDeclarations</key>
<array>
<dict>
<key>UTTypeIdentifier</key>
<string>dev.zoo.kcl</string>
<key>UTTypeReferenceURL</key>
<string>https://zoo.dev/docs/kcl</string>
<key>UTTypeConformsTo</key>
<array>
<string>public.source-code</string>
<string>public.data</string>
<string>public.text</string>
<string>public.plain-text</string>
<string>public.3d-content</string>
<string>public.script</string>
</array>
<key>UTTypeDescription</key>
<string>KCL (KittyCAD Language) document</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>kcl</string>
</array>
<key>public.mime-type</key>
<array>
<string>text/vnd.zoo.kcl</string>
</array>
</dict>
</dict>
<dict>
<key>UTTypeIdentifier</key>
<string>dev.zoo.gltf</string>
<key>UTTypeReferenceURL</key>
<string>https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html</string>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string>
<string>public.text</string>
<string>public.plain-text</string>
<string>public.3d-content</string>
<string>public.json</string>
</array>
<key>UTTypeDescription</key>
<string>Graphics Library Transmission Format (glTF)</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>gltf</string>
</array>
<key>public.mime-type</key>
<array>
<string>model/gltf+json</string>
</array>
</dict>
</dict>
<dict>
<key>UTTypeIdentifier</key>
<string>dev.zoo.glb</string>
<key>UTTypeReferenceURL</key>
<string>https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html</string>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string>
<string>public.3d-content</string>
</array>
<key>UTTypeDescription</key>
<string>Graphics Library Transmission Format (glTF) binary</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>glb</string>
</array>
<key>public.mime-type</key>
<array>
<string>model/gltf-binary</string>
</array>
</dict>
</dict>
<dict>
<key>UTTypeIdentifier</key>
<string>dev.zoo.step</string>
<key>UTTypeReferenceURL</key>
<string>https://www.loc.gov/preservation/digital/formats/fdd/fdd000448.shtml</string>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string>
<string>public.3d-content</string>
<string>public.text</string>
<string>public.plain-text</string>
</array>
<key>UTTypeDescription</key>
<string>STEP-file, ISO 10303-21</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>step</string>
<string>stp</string>
</array>
<key>public.mime-type</key>
<array>
<string>model/step</string>
</array>
</dict>
</dict>
<dict>
<key>UTTypeIdentifier</key>
<string>dev.zoo.sldprt</string>
<key>UTTypeReferenceURL</key>
<string>https://docs.fileformat.com/cad/sldprt/</string>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string>
<string>public.3d-content</string>
</array>
<key>UTTypeDescription</key>
<string>Solidworks Part</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>sldprt</string>
</array>
<key>public.mime-type</key>
<array>
<string>model/vnd.solidworks.sldprt</string>
</array>
</dict>
</dict>
<dict>
<key>UTTypeIdentifier</key>
<string>dev.zoo.fbx</string>
<key>UTTypeReferenceURL</key>
<string>https://en.wikipedia.org/wiki/FBX</string>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string>
<string>public.3d-content</string>
</array>
<key>UTTypeDescription</key>
<string>Autodesk Filmbox (FBX) format</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>fbx</string>
<string>fbxb</string>
</array>
<key>public.mime-type</key>
<array>
<string>model/vnd.autodesk.fbx</string>
</array>
</dict>
</dict>
<dict>
<key>UTTypeIdentifier</key>
<string>dev.zoo.toml</string>
<key>UTTypeReferenceURL</key>
<string>https://toml.io/en/</string>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string>
<string>public.text</string>
<string>public.plain-text</string>
</array>
<key>UTTypeDescription</key>
<string>Tom's Obvious Minimal Language</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>kcl</string>
</array>
<key>public.mime-type</key>
<array>
<string>text/toml</string>
</array>
</dict>
</dict>
</array>
</dict>
</plist>

View File

@ -189,12 +189,22 @@ For more information on fuzzing you can check out
### Playwright tests
You will need a `./e2e/playwright/playwright-secrets.env` file:
```bash
$ touch ./e2e/playwright/playwright-secrets.env
$ cat ./e2e/playwright/playwright-secrets.env
token=<dev.zoo.dev/account/api-tokens>
snapshottoken=<your-snapshot-token>
```
For a portable way to run Playwright you'll need Docker.
#### Generic example
After that, open a terminal and run:
```bash
docker run --network host --rm --init -it playwright/chrome:playwright-1.43.1
docker run --network host --rm --init -it playwright/chrome:playwright-x.xx.x
```
and in another terminal, run:
@ -203,21 +213,27 @@ and in another terminal, run:
PW_TEST_CONNECT_WS_ENDPOINT=ws://127.0.0.1:4444/ yarn playwright test --project="Google Chrome" <test suite>
```
An example of a `<test suite>` is: `e2e/playwright/flow-tests.spec.ts`
YOU WILL NEED A PLAYWRIGHT-SECRETS.ENV FILE:
#### Specific example
open a terminal and run:
```bash
# ./e2e/playwright/playwright-secrets.env
token=<your-token>
snapshottoken=<your-snapshot-token>
docker run --network host --rm --init -it playwright/chrome:playwright-1.46.0
```
and in another terminal, run:
```bash
PW_TEST_CONNECT_WS_ENDPOINT=ws://127.0.0.1:4444/ yarn playwright test --project="Google Chrome" e2e/playwright/command-bar-tests.spec.ts
```
then replace "your-token" with a dev token from dev.zoo.dev/account/api-tokens
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)
**Gotcha**: running the docker container with a mismatched image against your `./node_modules/playwright` will cause a failure. Make sure the versions are matched and up to date.
run headed
```

View File

@ -84,6 +84,63 @@ test.describe('Editor tests', () => {
|> close(%)`)
})
test('if you click the format button it formats your code and executes so lints are still there', async ({
page,
}) => {
const u = await getUtils(page)
await page.setViewportSize({ width: 1000, height: 500 })
await u.waitForAuthSkipAppStart()
// check no error to begin with
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
await u.codeLocator.click()
await page.keyboard.type(`const sketch_001 = startSketchOn('XY')
|> startProfileAt([-10, -10], %)
|> line([20, 0], %)
|> line([0, 20], %)
|> line([-20, 0], %)
|> close(%)`)
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
// error in guter
await expect(page.locator('.cm-lint-marker-info').first()).toBeVisible()
// error text on hover
await page.hover('.cm-lint-marker-info')
await expect(
page.getByText('Identifiers must be lowerCamelCase').first()
).toBeVisible()
await page.locator('#code-pane button:first-child').click()
await page.locator('button:has-text("Format code")').click()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
await expect(page.locator('.cm-content'))
.toHaveText(`const sketch_001 = startSketchOn('XY')
|> startProfileAt([-10, -10], %)
|> line([20, 0], %)
|> line([0, 20], %)
|> line([-20, 0], %)
|> close(%)`)
// error in guter
await expect(page.locator('.cm-lint-marker-info').first()).toBeVisible()
// error text on hover
await page.hover('.cm-lint-marker-info')
await expect(
page.getByText('Identifiers must be lowerCamelCase').first()
).toBeVisible()
})
test('fold gutters work', async ({ page }) => {
const u = await getUtils(page)
@ -241,6 +298,67 @@ test.describe('Editor tests', () => {
|> close(%)`)
})
test('if you use the format keyboard binding it formats your code and executes so lints are shown', async ({
page,
}) => {
const u = await getUtils(page)
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`const sketch_001 = startSketchOn('XY')
|> startProfileAt([-10, -10], %)
|> line([20, 0], %)
|> line([0, 20], %)
|> line([-20, 0], %)
|> close(%)`
)
localStorage.setItem('disableAxis', 'true')
})
await page.setViewportSize({ width: 1000, height: 500 })
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
// error in guter
await expect(page.locator('.cm-lint-marker-info').first()).toBeVisible()
// error text on hover
await page.hover('.cm-lint-marker-info')
await expect(
page.getByText('Identifiers must be lowerCamelCase').first()
).toBeVisible()
// focus the editor
await u.codeLocator.click()
// Hit alt+shift+f to format the code
await page.keyboard.press('Alt+Shift+KeyF')
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
await expect(page.locator('.cm-content'))
.toHaveText(`const sketch_001 = startSketchOn('XY')
|> startProfileAt([-10, -10], %)
|> line([20, 0], %)
|> line([0, 20], %)
|> line([-20, 0], %)
|> close(%)`)
// error in guter
await expect(page.locator('.cm-lint-marker-info').first()).toBeVisible()
// error text on hover
await page.hover('.cm-lint-marker-info')
await expect(
page.getByText('Identifiers must be lowerCamelCase').first()
).toBeVisible()
})
test('if you write kcl with lint errors you get lints', async ({ page }) => {
const u = await getUtils(page)
await page.setViewportSize({ width: 1000, height: 500 })
@ -399,7 +517,7 @@ test.describe('Editor tests', () => {
const width = 0.500
const height = 0.500
const dia = 4
fn squareHole = (l, w) => {
const squareHoleSketch = startSketchOn('XY')
|> startProfileAt([-width / 2, -length / 2], %)

View File

@ -454,6 +454,7 @@ test(
await electronApp.close()
}
)
test(
'File in the file pane should open with a single click',
{ tag: '@electron' },
@ -506,6 +507,69 @@ test(
}
)
test(
'Nested directories in project without main.kcl do not create main.kcl',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
let testDir: string | undefined
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
await fsp.mkdir(join(dir, 'router-template-slate', 'nested'), {
recursive: true,
})
await fsp.copyFile(
executorInputPath('router-template-slate.kcl'),
join(dir, 'router-template-slate', 'nested', 'slate.kcl')
)
await fsp.copyFile(
executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
join(dir, 'router-template-slate', 'nested', 'bracket.kcl')
)
testDir = dir
},
})
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log)
await test.step('Open the project', async () => {
await page.getByText('router-template-slate').click()
await expect(page.getByTestId('loading')).toBeAttached()
await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
})
// It actually loads.
await expect(u.codeLocator).toContainText('mounting bracket')
await expect(u.codeLocator).toContainText('const radius =')
})
await u.openFilePanel()
// Find the current file.
const filesPane = page.locator('#files-pane')
await expect(filesPane.getByText('bracket.kcl')).toBeVisible()
// But there's no main.kcl in the file tree browser.
await expect(filesPane.getByText('main.kcl')).not.toBeVisible()
// No main.kcl file is created on the filesystem.
expect(testDir).toBeDefined()
if (testDir !== undefined) {
// eslint-disable-next-line jest/no-conditional-expect
await expect(
fsp.access(join(testDir, 'router-template-slate', 'main.kcl'))
).rejects.toThrow()
// eslint-disable-next-line jest/no-conditional-expect
await expect(
fsp.access(join(testDir, 'router-template-slate', 'nested', 'main.kcl'))
).rejects.toThrow()
}
await electronApp.close()
}
)
test(
'Deleting projects, can delete individual project, can still create projects after deleting all',
{ tag: '@electron' },

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

View File

@ -367,4 +367,130 @@ test.describe('Testing settings', () => {
await electronApp.close()
}
)
test('Changing modeling default unit', async ({ page }) => {
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await page
.getByRole('button', { name: 'Start Sketch' })
.waitFor({ state: 'visible' })
const userSettingsTab = page.getByRole('radio', { name: 'User' })
// Open the settings modal with lower-right button
await page.getByRole('link', { name: 'Settings' }).last().click()
await expect(
page.getByRole('heading', { name: 'Settings', exact: true })
).toBeVisible()
const resetButton = page.getByRole('button', {
name: 'Restore default settings',
})
// Default unit should be mm
await resetButton.click()
await test.step('Change modeling default unit within project tab', async () => {
const changeUnitOfMeasureInProjectTab = async (unitOfMeasure: string) => {
await test.step(`Set modeling default unit to ${unitOfMeasure}`, async () => {
await page
.getByTestId('modeling-defaultUnit')
.selectOption(`${unitOfMeasure}`)
const toastMessage = page.getByText(
`Set default unit to "${unitOfMeasure}" for this project`
)
await expect(toastMessage).toBeVisible()
})
}
await changeUnitOfMeasureInProjectTab('in')
await changeUnitOfMeasureInProjectTab('ft')
await changeUnitOfMeasureInProjectTab('yd')
await changeUnitOfMeasureInProjectTab('mm')
await changeUnitOfMeasureInProjectTab('cm')
await changeUnitOfMeasureInProjectTab('m')
})
// Go to the user tab
await userSettingsTab.click()
await test.step('Change modeling default unit within user tab', async () => {
const changeUnitOfMeasureInUserTab = async (unitOfMeasure: string) => {
await test.step(`Set modeling default unit to ${unitOfMeasure}`, async () => {
await page
.getByTestId('modeling-defaultUnit')
.selectOption(`${unitOfMeasure}`)
const toastMessage = page.getByText(
`Set default unit to "${unitOfMeasure}" as a user default`
)
await expect(toastMessage).toBeVisible()
})
}
await changeUnitOfMeasureInUserTab('in')
await changeUnitOfMeasureInUserTab('ft')
await changeUnitOfMeasureInUserTab('yd')
await changeUnitOfMeasureInUserTab('mm')
await changeUnitOfMeasureInUserTab('cm')
await changeUnitOfMeasureInUserTab('m')
})
// Close settings
const settingsCloseButton = page.getByTestId('settings-close-button')
await settingsCloseButton.click()
await test.step('Change modeling default unit within command bar', async () => {
const commands = page.getByRole('button', { name: 'Commands' })
const changeUnitOfMeasureInCommandBar = async (unitOfMeasure: string) => {
// Open command bar
await commands.click()
const settingsModelingDefaultUnitCommand = page.getByText(
'Settings · modeling · default unit'
)
await settingsModelingDefaultUnitCommand.click()
const commandOption = page.getByRole('option', {
name: unitOfMeasure,
exact: true,
})
await commandOption.click()
const toastMessage = page.getByText(
`Set default unit to "${unitOfMeasure}" for this project`
)
await expect(toastMessage).toBeVisible()
}
await changeUnitOfMeasureInCommandBar('in')
await changeUnitOfMeasureInCommandBar('ft')
await changeUnitOfMeasureInCommandBar('yd')
await changeUnitOfMeasureInCommandBar('mm')
await changeUnitOfMeasureInCommandBar('cm')
await changeUnitOfMeasureInCommandBar('m')
})
await test.step('Change modeling default unit within gizmo', async () => {
const changeUnitOfMeasureInGizmo = async (
unitOfMeasure: string,
copy: string
) => {
const gizmo = page.getByRole('button', {
name: 'Current units are: ',
})
await gizmo.click()
const button = page.getByRole('button', {
name: copy,
exact: true,
})
await button.click()
const toastMessage = page.getByText(
`Set default unit to "${unitOfMeasure}" for this project`
)
await expect(toastMessage).toBeVisible()
}
await changeUnitOfMeasureInGizmo('in', 'Inches')
await changeUnitOfMeasureInGizmo('ft', 'Feet')
await changeUnitOfMeasureInGizmo('yd', 'Yards')
await changeUnitOfMeasureInGizmo('mm', 'Millimeters')
await changeUnitOfMeasureInGizmo('cm', 'Centimeters')
await changeUnitOfMeasureInGizmo('m', 'Meters')
})
})
})

View File

@ -1,5 +1,11 @@
import { test, expect, Page } from '@playwright/test'
import { getUtils, setup, tearDown } from './test-utils'
import {
getUtils,
setup,
tearDown,
setupElectron,
createProjectAndRenameIt,
} from './test-utils'
test.beforeEach(async ({ context, page }) => {
await setup(context, page)
@ -683,3 +689,60 @@ async function sendPromptFromCommandBar(page: Page, promptStr: string) {
await page.keyboard.press('Enter')
})
}
test(
'Text-to-CAD functionality',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async () => {},
})
await page.setViewportSize({ width: 1200, height: 500 })
// Create and navigate to the project
await createProjectAndRenameIt({ name: 'test-000', page })
await page.getByTestId('project-link').click()
// Wait for Start Sketch otherwise you will not have access Text-to-CAD command
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeEnabled({
timeout: 20_000,
})
// Open the files pane
const filesPaneButton = page.getByTestId('files-pane-button')
await filesPaneButton.click()
await test.step(`Test file creation`, async () => {
await sendPromptFromCommandBar(page, 'lego 2x4')
// File is considered created if it shows up in the Project Files pane
const file = page.getByRole('button', { name: 'lego-2x4.kcl' })
await expect(file).toBeVisible({ timeout: 20_000 })
})
await test.step(`Test file navigation`, async () => {
const file = page.getByRole('button', { name: 'lego-2x4.kcl' })
await file.click()
const kclComment = page.getByText('Lego 2x4 Brick')
// File can be navigated and loaded assuming a specific KCL comment is loaded into the KCL code pane
await expect(kclComment).toBeVisible({ timeout: 20_000 })
})
await test.step(`Test file deletion on rejection`, async () => {
const rejectButton = page.getByRole('button', { name: 'Reject' })
// A file is created and can be navigated to while this prompt is still opened
// Click the "Reject" button within the prompt and it will delete the file.
await rejectButton.click()
const submittingToastMessage = page.getByText(
`Successfully deleted file "lego-2x4.kcl"`
)
await expect(submittingToastMessage).toBeVisible()
})
await electronApp.close()
}
)

View File

@ -4,10 +4,17 @@ import { MakerZIP } from '@electron-forge/maker-zip'
import { MakerDeb } from '@electron-forge/maker-deb'
import { MakerRpm } from '@electron-forge/maker-rpm'
import { VitePlugin } from '@electron-forge/plugin-vite'
import { MakerWix, MakerWixConfig } from '@electron-forge/maker-wix'
import { FusesPlugin } from '@electron-forge/plugin-fuses'
import { FuseV1Options, FuseVersion } from '@electron/fuses'
import path from 'path'
interface ExtendedMakerWixConfig extends MakerWixConfig {
// see https://github.com/electron/forge/issues/3673
// this is an undocumented property of electron-wix-msi
associateExtensions?: string
}
const rootDir = process.cwd()
const config: ForgeConfig = {
@ -23,12 +30,23 @@ const config: ForgeConfig = {
undefined,
executableName: 'zoo-modeling-app',
icon: path.resolve(rootDir, 'assets', 'icon'),
protocols: [
{
name: 'Zoo Studio',
schemes: ['zoo-studio'],
},
],
extendInfo: 'Info.plist', // Information for file associations.
},
rebuildConfig: {},
makers: [
new MakerSquirrel({
setupIcon: path.resolve(rootDir, 'assets', 'icon.ico'),
}),
new MakerWix({
icon: path.resolve(rootDir, 'assets', 'icon.ico'),
associateExtensions: 'kcl',
} as ExtendedMakerWixConfig),
new MakerZIP({}, ['darwin']),
new MakerRpm({
options: {

1
interface.d.ts vendored
View File

@ -31,6 +31,7 @@ export interface IElectronAPI {
sep: typeof path.sep
rename: (prev: string, next: string) => typeof fs.rename
setBaseUrl: (value: string) => void
loadProjectAtStartup: () => Promise<ProjectState | null>
packageJson: {
name: string
}

View File

@ -44,6 +44,7 @@
"isomorphic-fetch": "^3.0.0",
"json-rpc-2.0": "^1.6.0",
"jszip": "^3.10.1",
"minimist": "^1.2.8",
"openid-client": "^5.6.5",
"re-resizable": "^6.9.11",
"react": "^18.3.1",
@ -88,8 +89,8 @@
"remove-importmeta": "sed -i 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\"; sed -i '' 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\" || echo \"sed for both mac and linux\"",
"wasm-prep": "rm -rf src/wasm-lib/pkg && mkdir src/wasm-lib/pkg && rm -rf src/wasm-lib/kcl/bindings",
"lint": "eslint --fix src e2e",
"bump-jsons": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json && echo \"$(jq --arg v \"$VERSION\" '.version=$v' src-tauri/tauri.conf.json --indent 2)\" > src-tauri/tauri.conf.json",
"postinstall": "yarn xstate:typegen",
"bump-jsons": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json",
"postinstall": "yarn xstate:typegen && ./node_modules/.bin/electron-rebuild",
"xstate:typegen": "yarn xstate typegen \"src/**/*.ts?(x)\"",
"make:dev": "make dev",
"generate:machine-api": "npx openapi-typescript ./openapi/machine-api.json -o src/lib/machine-api.d.ts",
@ -124,11 +125,13 @@
"@electron-forge/maker-deb": "^7.4.0",
"@electron-forge/maker-rpm": "^7.4.0",
"@electron-forge/maker-squirrel": "^7.4.0",
"@electron-forge/maker-wix": "^7.4.0",
"@electron-forge/maker-zip": "^7.4.0",
"@electron-forge/plugin-auto-unpack-natives": "^7.4.0",
"@electron-forge/plugin-fuses": "^7.4.0",
"@electron-forge/plugin-vite": "^7.4.0",
"@electron/fuses": "^1.8.0",
"@electron/rebuild": "^3.6.0",
"@iarna/toml": "^2.2.5",
"@lezer/generator": "^1.7.1",
"@playwright/test": "^1.46.1",
@ -137,6 +140,7 @@
"@types/d3-force": "^3.0.10",
"@types/electron": "^1.6.10",
"@types/isomorphic-fetch": "^0.0.39",
"@types/minimist": "^1.2.5",
"@types/mocha": "^10.0.6",
"@types/node": "^22.5.0",
"@types/pixelmatch": "^5.2.6",

View File

@ -33,7 +33,6 @@ import SettingsAuthProvider from 'components/SettingsAuthProvider'
import LspProvider from 'components/LspProvider'
import { KclContextProvider } from 'lang/KclProvider'
import { BROWSER_PROJECT_NAME } from 'lib/constants'
import { getState, setState } from 'lib/desktop'
import { CoreDumpManager } from 'lib/coredump'
import { codeManager, engineCommandManager } from 'lib/singletons'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
@ -71,17 +70,13 @@ const router = createRouter([
loader: async () => {
const onDesktop = isDesktop()
if (onDesktop) {
const appState = await getState()
if (appState) {
// Reset the state.
// We do this so that we load the initial state from the cli but everything
// else we can ignore.
await setState(undefined)
const projectStartupFile =
await window.electron.loadProjectAtStartup()
if (projectStartupFile !== null) {
// Redirect to the file if we have a file path.
if (appState.current_file) {
if (projectStartupFile.length > 0) {
return redirect(
PATHS.FILE + '/' + encodeURIComponent(appState.current_file)
PATHS.FILE + '/' + encodeURIComponent(projectStartupFile)
)
}
}

View File

@ -1,4 +1,4 @@
import { MouseGuard } from 'lib/cameraControls'
import { cameraMouseDragGuards, MouseGuard } from 'lib/cameraControls'
import {
Euler,
MathUtils,
@ -81,24 +81,7 @@ export class CameraControls {
pendingZoom: number | null = null
pendingRotation: Vector2 | null = null
pendingPan: Vector2 | null = null
interactionGuards: MouseGuard = {
pan: {
description: 'Right click + Shift + drag or middle click + drag',
callback: (e) => !!(e.buttons & 4) && !e.ctrlKey,
},
zoom: {
description: 'Scroll wheel or Right click + Ctrl + drag',
dragCallback: (e) => e.button === 2 && e.ctrlKey,
scrollCallback: () => true,
},
rotate: {
description: 'Right click + drag',
callback: (e) => {
console.log('event', e)
return !!(e.buttons & 2)
},
},
}
interactionGuards: MouseGuard = cameraMouseDragGuards.KittyCAD
isFovAnimationInProgress = false
fovBeforeOrtho = 45
get isPerspective() {

View File

@ -4,4 +4,21 @@
*/
.header {
grid-template-columns: 1fr auto 1fr;
-webkit-app-region: drag; /* Make the header of the app draggable */
}
.header button {
-webkit-app-region: no-drag; /* Make the button not draggable */
}
.header a {
-webkit-app-region: no-drag; /* Make the link not draggable */
}
.header textarea {
-webkit-app-region: no-drag; /* Make the textarea not draggable */
}
.header input {
-webkit-app-region: no-drag; /* Make the input not draggable */
}

View File

@ -1,4 +1,4 @@
import type { FileEntry, IndexLoaderData } from 'lib/types'
import type { IndexLoaderData } from 'lib/types'
import { PATHS } from 'lib/paths'
import { ActionButton } from './ActionButton'
import Tooltip from './Tooltip'
@ -20,6 +20,7 @@ import { useModelingContext } from 'hooks/useModelingContext'
import { DeleteConfirmationDialog } from './ProjectCard/DeleteProjectDialog'
import { ContextMenu, ContextMenuItem } from './ContextMenu'
import usePlatform from 'hooks/usePlatform'
import { FileEntry } from 'lib/project'
function getIndentationCSS(level: number) {
return `calc(1rem * ${level + 1})`

View File

@ -15,7 +15,7 @@ import { Extension } from '@codemirror/state'
import { LanguageSupport } from '@codemirror/language'
import { useNavigate } from 'react-router-dom'
import { PATHS } from 'lib/paths'
import { FileEntry } from 'lib/types'
import { FileEntry } from 'lib/project'
import Worker from 'editor/plugins/lsp/worker.ts?worker'
import {
KclWorkerOptions,

View File

@ -7,7 +7,7 @@ import { useHotkeys } from 'react-hotkeys-hook'
import Tooltip from '../Tooltip'
import { DeleteConfirmationDialog } from './DeleteProjectDialog'
import { ProjectCardRenameForm } from './ProjectCardRenameForm'
import { Project } from 'wasm-lib/kcl/bindings/Project'
import { Project } from 'lib/project'
function ProjectCard({
project,

View File

@ -1,7 +1,7 @@
import { ActionButton } from 'components/ActionButton'
import Tooltip from 'components/Tooltip'
import { HTMLProps, forwardRef } from 'react'
import { Project } from 'wasm-lib/kcl/bindings/Project'
import { Project } from 'lib/project'
interface ProjectCardRenameFormProps extends HTMLProps<HTMLFormElement> {
project: Project

View File

@ -1,4 +1,4 @@
import { Project } from 'wasm-lib/kcl/bindings/Project'
import { Project } from 'lib/project'
import { CustomIcon } from './CustomIcon'
import { useEffect, useRef, useState } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'

View File

@ -3,7 +3,7 @@ import { BrowserRouter } from 'react-router-dom'
import ProjectSidebarMenu from './ProjectSidebarMenu'
import { SettingsAuthProviderJest } from './SettingsAuthProvider'
import { CommandBarProvider } from './CommandBar/CommandBarProvider'
import { Project } from 'wasm-lib/kcl/bindings/Project'
import { Project } from 'lib/project'
const now = new Date()
const projectWellFormed = {

View File

@ -25,8 +25,17 @@ const ProjectSidebarMenu = ({
project?: IndexLoaderData['project']
file?: IndexLoaderData['file']
}) => {
// Make room for traffic lights on desktop left side.
// TODO: make sure this doesn't look like shit on Linux or Windows
const trafficLightsOffset =
isDesktop() && window.electron.os.isMac ? 'ml-20' : ''
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 ' +
trafficLightsOffset
}
>
<AppLogoLink project={project} file={file} />
{enableMenu ? (
<ProjectMenuPopover project={project} file={file} />

View File

@ -399,6 +399,9 @@ export class KclManager {
codeManager.updateCodeStateEditor(code)
// Write back to the file system.
codeManager.writeToFile()
// execute the code.
this.executeCode()
}
// There's overlapping responsibility between updateAst and executeAst.
// updateAst was added as it was used a lot before xState migration so makes the port easier.

View File

@ -6,9 +6,13 @@ import {
Expr,
Program,
CallExpression,
makeDefaultPlanes,
PipeExpression,
VariableDeclaration,
} from '../wasm'
import {
addFillet,
getPathToExtrudeForSegmentSelection,
hasValidFilletSelection,
isTagUsedInFillet,
} from './addFillet'
@ -16,9 +20,204 @@ import { getNodeFromPath, getNodePathFromSourceRange } from '../queryAst'
import { createLiteral } from 'lang/modifyAst'
import { err } from 'lib/trap'
import { Selections } from 'lib/selections'
import { engineCommandManager, kclManager } from 'lib/singletons'
import { VITE_KC_DEV_TOKEN } from 'env'
beforeAll(async () => {
await initPromise // Initialize the WASM environment before running tests
await initPromise
// THESE TEST WILL FAIL without VITE_KC_DEV_TOKEN set in .env.development.local
await new Promise((resolve) => {
engineCommandManager.start({
token: VITE_KC_DEV_TOKEN,
width: 256,
height: 256,
makeDefaultPlanes: () => makeDefaultPlanes(engineCommandManager),
setMediaStream: () => {},
setIsStreamReady: () => {},
modifyGrid: async () => {},
callbackOnEngineLiteConnect: async () => {
resolve(true)
},
})
})
}, 20_000)
afterAll(() => {
engineCommandManager.tearDown()
})
const runGetPathToExtrudeForSegmentSelectionTest = async (
code: string,
selectedSegmentSnippet: string,
expectedExtrudeSnippet: string
) => {
// helpers
function getExtrudeExpression(
ast: Program,
pathToExtrudeNode: PathToNode
): CallExpression | PipeExpression | undefined | Error {
if (pathToExtrudeNode.length === 0) return undefined // no extrude node
const extrudeNodeResult = getNodeFromPath(ast, pathToExtrudeNode)
if (err(extrudeNodeResult)) {
return extrudeNodeResult
}
return extrudeNodeResult.node as CallExpression | PipeExpression
}
function getExpectedExtrudeExpression(
ast: Program,
code: string,
expectedExtrudeSnippet: string
): CallExpression | PipeExpression | Error {
const extrudeRange: [number, number] = [
code.indexOf(expectedExtrudeSnippet),
code.indexOf(expectedExtrudeSnippet) + expectedExtrudeSnippet.length,
]
const expedtedExtrudePath = getNodePathFromSourceRange(ast, extrudeRange)
const expedtedExtrudeNodeResult = getNodeFromPath(ast, expedtedExtrudePath)
if (err(expedtedExtrudeNodeResult)) {
return expedtedExtrudeNodeResult
}
const expectedExtrudeNode =
expedtedExtrudeNodeResult.node as VariableDeclaration
return expectedExtrudeNode.declarations[0].init as
| CallExpression
| PipeExpression
}
// ast
const astOrError = parse(code)
if (err(astOrError)) return new Error('AST not found')
const ast = astOrError as Program
// selection
const segmentRange: [number, number] = [
code.indexOf(selectedSegmentSnippet),
code.indexOf(selectedSegmentSnippet) + selectedSegmentSnippet.length,
]
const selection: Selections = {
codeBasedSelections: [
{
range: segmentRange,
type: 'default',
},
],
otherSelections: [],
}
// programMemory and artifactGraph
await kclManager.executeAst({ ast })
const programMemory = kclManager.programMemory
const artifactGraph = engineCommandManager.artifactGraph
// get extrude expression
const pathResult = getPathToExtrudeForSegmentSelection(
ast,
selection,
programMemory,
artifactGraph
)
if (err(pathResult)) return pathResult
const { pathToExtrudeNode } = pathResult
const extrudeExpression = getExtrudeExpression(ast, pathToExtrudeNode)
// test
if (expectedExtrudeSnippet) {
const expectedExtrudeExpression = getExpectedExtrudeExpression(
ast,
code,
expectedExtrudeSnippet
)
if (err(expectedExtrudeExpression)) return expectedExtrudeExpression
expect(extrudeExpression).toEqual(expectedExtrudeExpression)
} else {
expect(extrudeExpression).toBeUndefined()
}
}
describe('Testing getPathToExtrudeForSegmentSelection', () => {
it('should return the correct paths for a valid selection and extrusion', async () => {
const code = `const sketch001 = startSketchOn('XY')
|> startProfileAt([-10, 10], %)
|> line([20, 0], %)
|> line([0, -20], %)
|> line([-20, 0], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const extrude001 = extrude(-15, sketch001)`
const selectedSegmentSnippet = `line([20, 0], %)`
const expectedExtrudeSnippet = `const extrude001 = extrude(-15, sketch001)`
await runGetPathToExtrudeForSegmentSelectionTest(
code,
selectedSegmentSnippet,
expectedExtrudeSnippet
)
})
it('should return the correct paths for a valid selection and extrusion in case of several extrusions and sketches', async () => {
const code = `const sketch001 = startSketchOn('XY')
|> startProfileAt([-30, 30], %)
|> line([15, 0], %)
|> line([0, -15], %)
|> line([-15, 0], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const sketch002 = startSketchOn('XY')
|> startProfileAt([30, 30], %)
|> line([20, 0], %)
|> line([0, -20], %)
|> line([-20, 0], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const sketch003 = startSketchOn('XY')
|> startProfileAt([30, -30], %)
|> line([25, 0], %)
|> line([0, -25], %)
|> line([-25, 0], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const extrude001 = extrude(-15, sketch001)
const extrude002 = extrude(-15, sketch002)
const extrude003 = extrude(-15, sketch003)`
const selectedSegmentSnippet = `line([20, 0], %)`
const expectedExtrudeSnippet = `const extrude002 = extrude(-15, sketch002)`
await runGetPathToExtrudeForSegmentSelectionTest(
code,
selectedSegmentSnippet,
expectedExtrudeSnippet
)
})
it('should not return any path for missing extrusion', async () => {
const code = `const sketch001 = startSketchOn('XY')
|> startProfileAt([-30, 30], %)
|> line([15, 0], %)
|> line([0, -15], %)
|> line([-15, 0], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const sketch002 = startSketchOn('XY')
|> startProfileAt([30, 30], %)
|> line([20, 0], %)
|> line([0, -20], %)
|> line([-20, 0], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const sketch003 = startSketchOn('XY')
|> startProfileAt([30, -30], %)
|> line([25, 0], %)
|> line([0, -25], %)
|> line([-25, 0], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const extrude001 = extrude(-15, sketch001)
const extrude003 = extrude(-15, sketch003)`
const selectedSegmentSnippet = `line([20, 0], %)`
const expectedExtrudeSnippet = ``
await runGetPathToExtrudeForSegmentSelectionTest(
code,
selectedSegmentSnippet,
expectedExtrudeSnippet
)
})
})
const runFilletTest = async (
@ -57,8 +256,6 @@ const runFilletTest = async (
return new Error('Path to extrude node not found')
}
// const radius = createLiteral(5) as Expr
const result = addFillet(ast, pathToSegmentNode, pathToExtrudeNode, radius)
if (err(result)) {
return result
@ -68,7 +265,6 @@ const runFilletTest = async (
expect(newCode).toContain(expectedCode)
}
describe('Testing addFillet', () => {
/**
* 1. Ideal Case

View File

@ -4,9 +4,11 @@ import {
ObjectExpression,
PathToNode,
Program,
ProgramMemory,
Expr,
VariableDeclaration,
VariableDeclarator,
sketchGroupFromKclValue,
} from '../wasm'
import {
createCallExpressionStdLib,
@ -28,62 +30,210 @@ import {
getTagFromCallExpression,
sketchLineHelperMap,
} from '../std/sketch'
import { err } from 'lib/trap'
import { err, trap } from 'lib/trap'
import { Selections, canFilletSelection } from 'lib/selections'
import { KclCommandValue } from 'lib/commandTypes'
import {
ArtifactGraph,
getExtrusionFromSuspectedPath,
} from 'lang/std/artifactGraph'
import { kclManager, engineCommandManager, editorManager } from 'lib/singletons'
/**
* Apply Fillet To Selection
*/
export function applyFilletToSelection(
selection: Selections,
radius: KclCommandValue
): void | Error {
// 1. get AST
let ast = kclManager.ast
const astResult = insertRadiusIntoAst(ast, radius)
if (err(astResult)) return astResult
// 2. get path
const programMemory = kclManager.programMemory
const artifactGraph = engineCommandManager.artifactGraph
const getPathToExtrudeForSegmentSelectionResult =
getPathToExtrudeForSegmentSelection(
ast,
selection,
programMemory,
artifactGraph
)
if (err(getPathToExtrudeForSegmentSelectionResult))
return getPathToExtrudeForSegmentSelectionResult
const { pathToSegmentNode, pathToExtrudeNode } =
getPathToExtrudeForSegmentSelectionResult
// 3. add fillet
const addFilletResult = addFillet(
ast,
pathToSegmentNode,
pathToExtrudeNode,
'variableName' in radius ? radius.variableIdentifierAst : radius.valueAst
)
if (trap(addFilletResult)) return addFilletResult
const { modifiedAst, pathToFilletNode } = addFilletResult
// 4. update ast
updateAstAndFocus(modifiedAst, pathToFilletNode)
}
function insertRadiusIntoAst(
ast: Program,
radius: KclCommandValue
): { ast: Program } | Error {
try {
// Validate and update AST
if (
'variableName' in radius &&
radius.variableName &&
radius.insertIndex !== undefined
) {
const newAst = structuredClone(ast)
newAst.body.splice(radius.insertIndex, 0, radius.variableDeclarationAst)
return { ast: newAst }
}
return { ast }
} catch (error) {
return new Error(`Failed to handle AST: ${(error as Error).message}`)
}
}
export function getPathToExtrudeForSegmentSelection(
ast: Program,
selection: Selections,
programMemory: ProgramMemory,
artifactGraph: ArtifactGraph
): { pathToSegmentNode: PathToNode; pathToExtrudeNode: PathToNode } | Error {
const pathToSegmentNode = getNodePathFromSourceRange(
ast,
selection.codeBasedSelections[0].range
)
const varDecNode = getNodeFromPath<VariableDeclaration>(
ast,
pathToSegmentNode,
'VariableDeclaration'
)
if (err(varDecNode)) return varDecNode
const sketchVar = varDecNode.node.declarations[0].id.name
const sketchGroup = sketchGroupFromKclValue(
kclManager.programMemory.get(sketchVar),
sketchVar
)
if (trap(sketchGroup)) return sketchGroup
const extrusion = getExtrusionFromSuspectedPath(sketchGroup.id, artifactGraph)
if (err(extrusion)) return extrusion
const pathToExtrudeNode = getNodePathFromSourceRange(
ast,
extrusion.codeRef.range
)
if (err(pathToExtrudeNode)) return pathToExtrudeNode
return { pathToSegmentNode, pathToExtrudeNode }
}
async function updateAstAndFocus(
modifiedAst: Program,
pathToFilletNode: PathToNode
) {
const updatedAst = await kclManager.updateAst(modifiedAst, true, {
focusPath: pathToFilletNode,
})
if (updatedAst?.selections) {
editorManager.selectRange(updatedAst?.selections)
}
}
/**
* Add Fillet
*/
export function addFillet(
node: Program,
ast: Program,
pathToSegmentNode: PathToNode,
pathToExtrudeNode: PathToNode,
radius = createLiteral(5) as Expr
// shouldPipe = false, // TODO: Implement this feature
radius: Expr = createLiteral(5)
): { modifiedAst: Program; pathToFilletNode: PathToNode } | Error {
// clone ast to make mutations safe
let _node = structuredClone(node)
// Clone AST to ensure safe mutations
const astClone = structuredClone(ast)
/**
* Add Tag to the Segment Expression
*/
// Modify AST clone : TAG the sketch segment and retrieve tag
const segmentResult = mutateAstWithTagForSketchSegment(
astClone,
pathToSegmentNode
)
if (err(segmentResult)) return segmentResult
const { tag } = segmentResult
// Find the specific sketch segment to tag with the new tag
const sketchSegmentChunk = getNodeFromPath(
_node,
// Modify AST clone : Insert FILLET node and retrieve path to fillet
const filletResult = mutateAstWithFilletNode(
astClone,
pathToExtrudeNode,
radius,
tag
)
if (err(filletResult)) return filletResult
const { pathToFilletNode } = filletResult
return { modifiedAst: astClone, pathToFilletNode }
}
function mutateAstWithTagForSketchSegment(
astClone: Program,
pathToSegmentNode: PathToNode
): { modifiedAst: Program; tag: string } | Error {
const segmentNode = getNodeFromPath<CallExpression>(
astClone,
pathToSegmentNode,
'CallExpression'
)
if (err(sketchSegmentChunk)) return sketchSegmentChunk
const { node: sketchSegmentNode } = sketchSegmentChunk as {
node: CallExpression
}
if (err(segmentNode)) return segmentNode
// Check whether selection is a valid segment from sketchLineHelpersMap
if (!(sketchSegmentNode.callee.name in sketchLineHelperMap)) {
// Check whether selection is a valid segment
if (!(segmentNode.node.callee.name in sketchLineHelperMap)) {
return new Error('Selection is not a sketch segment')
}
// Add tag to the sketch segment or use existing tag
// a helper function that creates the updated node and applies the changes to the AST
const taggedSegment = addTagForSketchOnFace(
{
// previousProgramMemory: programMemory,
pathToNode: pathToSegmentNode,
node: _node,
node: astClone,
},
sketchSegmentNode.callee.name
segmentNode.node.callee.name
)
if (err(taggedSegment)) return taggedSegment
const { tag } = taggedSegment
/**
* Find Extrude Expression automatically
*/
return { modifiedAst: astClone, tag }
}
// 1. Get the sketch name
function mutateAstWithFilletNode(
astClone: Program,
pathToExtrudeNode: PathToNode,
radius: Expr,
tag: string
): { modifiedAst: Program; pathToFilletNode: PathToNode } | Error {
// Locate the extrude call
const locatedExtrudeDeclarator = locateExtrudeDeclarator(
astClone,
pathToExtrudeNode
)
if (err(locatedExtrudeDeclarator)) return locatedExtrudeDeclarator
const { extrudeDeclarator } = locatedExtrudeDeclarator
/**
* Add Fillet to the Extrude expression
* Prepare changes to the AST
*/
// Create the fillet call expression in one line
const filletCall = createCallExpressionStdLib('fillet', [
createObjectExpression({
radius: radius,
@ -92,104 +242,36 @@ export function addFillet(
createPipeSubstitution(),
])
// Locate the extrude call
const extrudeChunk = getNodeFromPath<VariableDeclaration>(
_node,
pathToExtrudeNode,
'VariableDeclaration'
)
if (err(extrudeChunk)) return extrudeChunk
const { node: extrudeVarDecl } = extrudeChunk
const extrudeDeclarator = extrudeVarDecl.declarations[0]
const extrudeInit = extrudeDeclarator.init
if (
!extrudeDeclarator ||
(extrudeInit.type !== 'CallExpression' &&
extrudeInit.type !== 'PipeExpression')
) {
return new Error('Extrude PipeExpression / CallExpression not found.')
}
// determine if extrude is in a PipeExpression or CallExpression
/**
* Mutate the AST
*/
// CallExpression - no fillet
// PipeExpression - fillet exists
const getPathToNodeOfFilletLiteral = (
pathToExtrudeNode: PathToNode,
extrudeDeclarator: VariableDeclarator,
tag: string
): PathToNode => {
let pathToFilletObj: any
let inFillet = false
traverse(extrudeDeclarator.init, {
enter(node, path) {
if (node.type === 'CallExpression' && node.callee.name === 'fillet') {
inFillet = true
}
if (inFillet && node.type === 'ObjectExpression') {
const hasTag = node.properties.some((prop) => {
const isTagProp = prop.key.name === 'tags'
if (isTagProp && prop.value.type === 'ArrayExpression') {
return prop.value.elements.some(
(element) =>
element.type === 'Identifier' && element.name === tag
)
}
return false
})
if (!hasTag) return false
pathToFilletObj = path
node.properties.forEach((prop, index) => {
if (prop.key.name === 'radius') {
pathToFilletObj.push(
['properties', 'ObjectExpression'],
[index, 'index'],
['value', 'Property']
)
}
})
}
},
leave(node) {
if (node.type === 'CallExpression' && node.callee.name === 'fillet') {
inFillet = false
}
},
})
let indexOfPipeExpression = pathToExtrudeNode.findIndex(
(path) => path[1] === 'PipeExpression'
let pathToFilletNode: PathToNode = []
if (extrudeDeclarator.init.type === 'CallExpression') {
// 1. case when no fillet exists
// modify ast with new fillet call by mutating the extrude node
extrudeDeclarator.init = createPipeExpression([
extrudeDeclarator.init,
filletCall,
])
// get path to the fillet node
pathToFilletNode = getPathToNodeOfFilletLiteral(
pathToExtrudeNode,
extrudeDeclarator,
tag
)
indexOfPipeExpression =
indexOfPipeExpression === -1
? pathToExtrudeNode.length
: indexOfPipeExpression
return [
...pathToExtrudeNode.slice(0, indexOfPipeExpression),
...pathToFilletObj,
]
}
return { modifiedAst: astClone, pathToFilletNode }
} else if (extrudeDeclarator.init.type === 'PipeExpression') {
// 2. case when fillet exists
if (extrudeInit.type === 'CallExpression') {
// 1. no fillet case
extrudeDeclarator.init = createPipeExpression([extrudeInit, filletCall])
return {
modifiedAst: _node,
pathToFilletNode: getPathToNodeOfFilletLiteral(
pathToExtrudeNode,
extrudeDeclarator,
tag
),
}
} else if (extrudeInit.type === 'PipeExpression') {
// 2. fillet case
// there are 2 options here:
const existingFilletCall = extrudeInit.body.find((node) => {
const existingFilletCall = extrudeDeclarator.init.body.find((node) => {
return node.type === 'CallExpression' && node.callee.name === 'fillet'
})
@ -198,25 +280,13 @@ export function addFillet(
}
// check if the existing fillet has the same tag as the new fillet
let filletTag = null
if (existingFilletCall.arguments[0].type === 'ObjectExpression') {
const properties = (existingFilletCall.arguments[0] as ObjectExpression)
.properties
const tagsProperty = properties.find((prop) => prop.key.name === 'tags')
if (tagsProperty && tagsProperty.value.type === 'ArrayExpression') {
const elements = (tagsProperty.value as ArrayExpression).elements
if (elements.length > 0 && elements[0].type === 'Identifier') {
filletTag = elements[0].name
}
}
} else {
return new Error('Expected an ObjectExpression node')
}
const filletTag = getFilletTag(existingFilletCall)
if (filletTag !== tag) {
extrudeInit.body.push(filletCall)
// mutate the extrude node with the new fillet call
extrudeDeclarator.init.body.push(filletCall)
return {
modifiedAst: _node,
modifiedAst: astClone,
pathToFilletNode: getPathToNodeOfFilletLiteral(
pathToExtrudeNode,
extrudeDeclarator,
@ -228,9 +298,124 @@ export function addFillet(
return new Error('Unsupported extrude type.')
}
return new Error('Unsupported extrude type.')
return { modifiedAst: astClone, pathToFilletNode }
}
function locateExtrudeDeclarator(
node: Program,
pathToExtrudeNode: PathToNode
): { extrudeDeclarator: VariableDeclarator } | Error {
const extrudeChunk = getNodeFromPath<VariableDeclaration>(
node,
pathToExtrudeNode,
'VariableDeclaration'
)
if (err(extrudeChunk)) return extrudeChunk
const { node: extrudeVarDecl } = extrudeChunk
const extrudeDeclarator = extrudeVarDecl.declarations[0]
if (!extrudeDeclarator) {
return new Error('Extrude Declarator not found.')
}
const extrudeInit = extrudeDeclarator?.init
if (!extrudeInit) {
return new Error('Extrude Init not found.')
}
if (
extrudeInit.type !== 'CallExpression' &&
extrudeInit.type !== 'PipeExpression'
) {
return new Error('Extrude must be a PipeExpression or CallExpression')
}
return { extrudeDeclarator }
}
function getPathToNodeOfFilletLiteral(
pathToExtrudeNode: PathToNode,
extrudeDeclarator: VariableDeclarator,
tag: string
): PathToNode {
let pathToFilletObj: PathToNode = []
let inFillet = false
traverse(extrudeDeclarator.init, {
enter(node, path) {
if (node.type === 'CallExpression' && node.callee.name === 'fillet') {
inFillet = true
}
if (inFillet && node.type === 'ObjectExpression') {
if (!hasTag(node, tag)) return false
pathToFilletObj = getPathToRadiusLiteral(node, path)
}
},
leave(node) {
if (node.type === 'CallExpression' && node.callee.name === 'fillet') {
inFillet = false
}
},
})
let indexOfPipeExpression = pathToExtrudeNode.findIndex(
(path) => path[1] === 'PipeExpression'
)
indexOfPipeExpression =
indexOfPipeExpression === -1
? pathToExtrudeNode.length
: indexOfPipeExpression
return [
...pathToExtrudeNode.slice(0, indexOfPipeExpression),
...pathToFilletObj,
]
}
function hasTag(node: ObjectExpression, tag: string): boolean {
return node.properties.some((prop) => {
if (prop.key.name === 'tags' && prop.value.type === 'ArrayExpression') {
return prop.value.elements.some(
(element) => element.type === 'Identifier' && element.name === tag
)
}
return false
})
}
function getPathToRadiusLiteral(node: ObjectExpression, path: any): PathToNode {
let pathToFilletObj = path
node.properties.forEach((prop, index) => {
if (prop.key.name === 'radius') {
pathToFilletObj.push(
['properties', 'ObjectExpression'],
[index, 'index'],
['value', 'Property']
)
}
})
return pathToFilletObj
}
function getFilletTag(existingFilletCall: CallExpression): string | null {
if (existingFilletCall.arguments[0].type === 'ObjectExpression') {
const properties = (existingFilletCall.arguments[0] as ObjectExpression)
.properties
const tagsProperty = properties.find((prop) => prop.key.name === 'tags')
if (tagsProperty && tagsProperty.value.type === 'ArrayExpression') {
const elements = (tagsProperty.value as ArrayExpression).elements
if (elements.length > 0 && elements[0].type === 'Identifier') {
return elements[0].name
}
}
}
return null
}
/**
* Button states
*/
export const hasValidFilletSelection = ({
selectionRanges,
ast,

View File

@ -1,4 +1,10 @@
import { MouseControlType } from 'wasm-lib/kcl/bindings/MouseControlType'
import { platform } from './utils'
const PLATFORM = platform()
const META =
PLATFORM === 'macos' ? 'Cmd' : PLATFORM === 'windows' ? 'Win' : 'Super'
const ALT = PLATFORM === 'macos' ? 'Option' : 'Alt'
const noModifiersPressed = (e: React.MouseEvent) =>
!e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey
@ -73,99 +79,99 @@ export const btnName = (e: React.MouseEvent) => ({
export const cameraMouseDragGuards: Record<CameraSystem, MouseGuard> = {
KittyCAD: {
pan: {
description: 'Right click + Shift + drag or middle click + drag',
description: 'Shift + Right click drag or middle click drag',
callback: (e) =>
(btnName(e).middle && noModifiersPressed(e)) ||
(btnName(e).right && e.shiftKey),
},
zoom: {
description: 'Scroll wheel or Right click + Ctrl + drag',
description: 'Scroll or Ctrl + Right click drag',
dragCallback: (e) => !!(e.buttons & 2) && e.ctrlKey,
scrollCallback: () => true,
},
rotate: {
description: 'Right click + drag',
description: 'Right click drag',
callback: (e) => btnName(e).right && noModifiersPressed(e),
},
},
OnShape: {
pan: {
description: 'Right click + Ctrl + drag or middle click + drag',
description: 'Ctrl + Right click drag or middle click drag',
callback: (e) =>
(btnName(e).right && e.ctrlKey) ||
(btnName(e).middle && noModifiersPressed(e)),
},
zoom: {
description: 'Scroll wheel',
description: 'Scroll',
dragCallback: () => false,
scrollCallback: () => true,
},
rotate: {
description: 'Right click + drag',
description: 'Right click drag',
callback: (e) => btnName(e).right && noModifiersPressed(e),
},
},
'Trackpad Friendly': {
pan: {
description: 'Left click + Alt + Shift + drag or middle click + drag',
description: `${ALT} + Shift + Left click drag or middle click drag`,
callback: (e) =>
(btnName(e).left && e.altKey && e.shiftKey && !e.metaKey) ||
(btnName(e).middle && noModifiersPressed(e)),
},
zoom: {
description: 'Scroll wheel or Left click + Alt + OS + drag',
description: `Scroll or ${ALT} + ${META} + Left click drag`,
dragCallback: (e) => btnName(e).left && e.altKey && e.metaKey,
scrollCallback: () => true,
},
rotate: {
description: 'Left click + Alt + drag',
description: `${ALT} + Left click drag`,
callback: (e) => btnName(e).left && e.altKey && !e.shiftKey && !e.metaKey,
lenientDragStartButton: 0,
},
},
Solidworks: {
pan: {
description: 'Right click + Ctrl + drag',
description: 'Ctrl + Right click drag',
callback: (e) => btnName(e).right && e.ctrlKey,
lenientDragStartButton: 2,
},
zoom: {
description: 'Scroll wheel or Middle click + Shift + drag',
description: 'Scroll or Shift + Middle click drag',
dragCallback: (e) => btnName(e).middle && e.shiftKey,
scrollCallback: () => true,
},
rotate: {
description: 'Middle click + drag',
description: 'Middle click drag',
callback: (e) => btnName(e).middle && noModifiersPressed(e),
},
},
NX: {
pan: {
description: 'Middle click + Shift + drag',
description: 'Shift + Middle click drag',
callback: (e) => btnName(e).middle && e.shiftKey,
},
zoom: {
description: 'Scroll wheel or Middle click + Ctrl + drag',
description: 'Scroll or Ctrl + Middle click drag',
dragCallback: (e) => btnName(e).middle && e.ctrlKey,
scrollCallback: () => true,
},
rotate: {
description: 'Middle click + drag',
description: 'Middle click drag',
callback: (e) => btnName(e).middle && noModifiersPressed(e),
},
},
Creo: {
pan: {
description: 'Left click + Ctrl + drag',
description: 'Ctrl + Left click drag',
callback: (e) => btnName(e).left && !btnName(e).right && e.ctrlKey,
},
zoom: {
description: 'Scroll wheel or Right click + Ctrl + drag',
description: 'Scroll or Ctrl + Right click drag',
dragCallback: (e) => btnName(e).right && !btnName(e).left && e.ctrlKey,
scrollCallback: () => true,
},
rotate: {
description: 'Middle (or Left + Right) click + Ctrl + drag',
description: 'Ctrl + Middle (or Left + Right) click drag',
callback: (e) => {
const b = btnName(e)
return (b.middle || (b.left && b.right)) && e.ctrlKey
@ -174,16 +180,16 @@ export const cameraMouseDragGuards: Record<CameraSystem, MouseGuard> = {
},
AutoCAD: {
pan: {
description: 'Middle click + drag',
description: 'Middle click drag',
callback: (e) => btnName(e).middle && noModifiersPressed(e),
},
zoom: {
description: 'Scroll wheel',
description: 'Scroll',
dragCallback: () => false,
scrollCallback: () => true,
},
rotate: {
description: 'Middle click + Shift + drag',
description: 'Shift + Middle click drag',
callback: (e) => btnName(e).middle && e.shiftKey,
},
},

View File

@ -1,8 +1,6 @@
import { err } from 'lib/trap'
import { Models } from '@kittycad/lib'
import { Project } from 'wasm-lib/kcl/bindings/Project'
import { ProjectState } from 'wasm-lib/kcl/bindings/ProjectState'
import { FileEntry } from 'wasm-lib/kcl/bindings/FileEntry'
import { Project, FileEntry } from 'lib/project'
import {
defaultAppSettings,
@ -477,18 +475,6 @@ export const writeAppSettingsFile = async (tomlStr: string) => {
return window.electron.writeFile(appSettingsFilePath, tomlStr)
}
let appStateStore: ProjectState | undefined = undefined
export const getState = async (): Promise<ProjectState | undefined> => {
return Promise.resolve(appStateStore)
}
export const setState = async (
state: ProjectState | undefined
): Promise<void> => {
appStateStore = state
}
export const getUser = async (
token: string,
hostname: string

View File

@ -1,5 +1,5 @@
import { isDesktop } from './isDesktop'
import type { FileEntry } from 'lib/types'
import type { FileEntry } from 'lib/project'
import {
FILE_EXT,
INDEX_IDENTIFIER,

View File

@ -0,0 +1,98 @@
import { promises as fs } from 'fs'
import path from 'path'
import os from 'os'
import { v4 as uuidv4 } from 'uuid'
import getCurrentProjectFile from './getCurrentProjectFile'
describe('getCurrentProjectFile', () => {
test('with explicit open file with space (URL encoded)', async () => {
const name = `kittycad-modeling-projects-${uuidv4()}`
const tmpProjectDir = path.join(os.tmpdir(), name)
await fs.mkdir(tmpProjectDir, { recursive: true })
await fs.writeFile(path.join(tmpProjectDir, 'i have a space.kcl'), '')
const state = await getCurrentProjectFile(
path.join(tmpProjectDir, 'i%20have%20a%20space.kcl')
)
expect(state).toBe(path.join(tmpProjectDir, 'i have a space.kcl'))
await fs.rm(tmpProjectDir, { recursive: true, force: true })
})
test('with explicit open file with space', async () => {
const name = `kittycad-modeling-projects-${uuidv4()}`
const tmpProjectDir = path.join(os.tmpdir(), name)
await fs.mkdir(tmpProjectDir, { recursive: true })
await fs.writeFile(path.join(tmpProjectDir, 'i have a space.kcl'), '')
const state = await getCurrentProjectFile(
path.join(tmpProjectDir, 'i have a space.kcl')
)
expect(state).toBe(path.join(tmpProjectDir, 'i have a space.kcl'))
await fs.rm(tmpProjectDir, { recursive: true, force: true })
})
test('with source path dot', async () => {
const name = `kittycad-modeling-projects-${uuidv4()}`
const tmpProjectDir = path.join(os.tmpdir(), name)
await fs.mkdir(tmpProjectDir, { recursive: true })
// Set the current directory to the temp project directory.
const originalCwd = process.cwd()
process.chdir(tmpProjectDir)
try {
const state = await getCurrentProjectFile('.')
if (state instanceof Error) {
throw state
}
expect(state.replace('/private', '')).toBe(
path.join(tmpProjectDir, 'main.kcl')
)
} finally {
process.chdir(originalCwd)
await fs.rm(tmpProjectDir, { recursive: true, force: true })
}
})
test('with main.kcl not existing', async () => {
const name = `kittycad-modeling-projects-${uuidv4()}`
const tmpProjectDir = path.join(os.tmpdir(), name)
await fs.mkdir(tmpProjectDir, { recursive: true })
try {
const state = await getCurrentProjectFile(tmpProjectDir)
expect(state).toBe(path.join(tmpProjectDir, 'main.kcl'))
} finally {
await fs.rm(tmpProjectDir, { recursive: true, force: true })
}
})
test('with directory, main.kcl not existing, other.kcl does', async () => {
const name = `kittycad-modeling-projects-${uuidv4()}`
const tmpProjectDir = path.join(os.tmpdir(), name)
await fs.mkdir(tmpProjectDir, { recursive: true })
await fs.writeFile(path.join(tmpProjectDir, 'other.kcl'), '')
try {
const state = await getCurrentProjectFile(tmpProjectDir)
expect(state).toBe(path.join(tmpProjectDir, 'other.kcl'))
// make sure we didn't create a main.kcl file
await expect(
fs.access(path.join(tmpProjectDir, 'main.kcl'))
).rejects.toThrow()
} finally {
await fs.rm(tmpProjectDir, { recursive: true, force: true })
}
})
})

View File

@ -0,0 +1,116 @@
import * as path from 'path'
import * as fs from 'fs/promises'
import { Models } from '@kittycad/lib/dist/types/src'
import { PROJECT_ENTRYPOINT } from './constants'
// Create a const object with the values
const FILE_IMPORT_FORMATS = {
fbx: 'fbx',
gltf: 'gltf',
obj: 'obj',
ply: 'ply',
sldprt: 'sldprt',
step: 'step',
stl: 'stl',
} as const
// Extract the values into an array
const fileImportFormats: Models['FileImportFormat_type'][] =
Object.values(FILE_IMPORT_FORMATS)
export const allFileImportFormats: string[] = [
...fileImportFormats,
'stp',
'fbxb',
'glb',
]
export const relevantExtensions = ['kcl', ...allFileImportFormats]
/// Get the current project file from the path.
/// This is used for double-clicking on a file in the file explorer,
/// or the command line args, or deep linking.
export default async function getCurrentProjectFile(
pathString: string
): Promise<string | Error> {
// Fix for "." path, which is the current directory.
let sourcePath = pathString === '.' ? process.cwd() : pathString
// URL decode the path.
sourcePath = decodeURIComponent(sourcePath)
// If the path does not start with a slash, it is a relative path.
// We need to convert it to an absolute path.
sourcePath = path.isAbsolute(sourcePath)
? sourcePath
: path.join(process.cwd(), sourcePath)
// If the path is a directory, let's assume it is a project directory.
const stats = await fs.stat(sourcePath)
if (stats.isDirectory()) {
// Walk the directory and look for a kcl file.
const files = await fs.readdir(sourcePath)
const kclFiles = files.filter((file) => path.extname(file) === '.kcl')
if (kclFiles.length === 0) {
let projectFile = path.join(sourcePath, PROJECT_ENTRYPOINT)
// Check if we have a main.kcl file in the project.
try {
await fs.access(projectFile)
} catch {
// Create the default file in the project.
await fs.writeFile(projectFile, '')
}
return projectFile
}
// If a project entrypoint file exists, use it.
// Otherwise, use the first kcl file in the project.
const gotMain = files.filter((file) => file === PROJECT_ENTRYPOINT)
if (gotMain.length === 0) {
return path.join(sourcePath, kclFiles[0])
}
return path.join(sourcePath, PROJECT_ENTRYPOINT)
}
// Check if the extension on what we are trying to open is a relevant file type.
const extension = path.extname(sourcePath).slice(1)
if (!relevantExtensions.includes(extension) && extension !== 'toml') {
return new Error(
`File type (${extension}) cannot be opened with this app: '${sourcePath}', try opening one of the following file types: ${relevantExtensions.join(
', '
)}`
)
}
// We were given a file path, not a directory.
// Let's get the parent directory of the file.
const parent = path.dirname(sourcePath)
// If we got an import model file, we need to check if we have a file in the project for
// this import model.
if (allFileImportFormats.includes(extension)) {
const importFileName = path.basename(sourcePath)
// Check if we have a file in the project for this import model.
const kclWrapperFilename = `${importFileName}.kcl`
const kclWrapperFilePath = path.join(parent, kclWrapperFilename)
try {
await fs.access(kclWrapperFilePath)
} catch {
// Create the file in the project with the default import content.
const content = `// This file was automatically generated by the application when you
// double-clicked on the model file.
// You can edit this file to add your own content.
// But we recommend you keep the import statement as it is.
// For more information on the import statement, see the documentation at:
// https://zoo.dev/docs/kcl/import
const model = import("${importFileName}")`
await fs.writeFile(kclWrapperFilePath, content)
}
return kclWrapperFilePath
}
return sourcePath
}

46
src/lib/project.d.ts vendored Normal file
View File

@ -0,0 +1,46 @@
/**
* The permissions of a file.
*/
export type FilePermission = 'read' | 'write' | 'execute'
/**
* The type of a file.
*/
export type FileType = 'file' | 'directory' | 'symlink'
/**
* Metadata about a file or directory.
*/
export type FileMetadata = {
accessed: string | null
created: string | null
type: FileType | null
size: number
modified: string | null
permission: FilePermission | null
}
/**
* Information about a file or directory.
*/
export type FileEntry = {
path: string
name: string
children: Array<FileEntry> | null
}
/**
* Information about project.
*/
export type Project = {
metadata: FileMetadata | null
kcl_file_count: number
directory_count: number
/**
* The default file to open on load.
*/
default_file: string
path: string
name: string
children: Array<FileEntry> | null
}

View File

@ -1,5 +1,5 @@
import { CustomIconName } from 'components/CustomIcon'
import { Project } from 'wasm-lib/kcl/bindings/Project'
import { Project } from 'lib/project'
const DESC = ':desc'

View File

@ -1,7 +1,4 @@
import { FileEntry } from 'wasm-lib/kcl/bindings/FileEntry'
import { Project } from 'wasm-lib/kcl/bindings/Project'
export type { FileEntry } from 'wasm-lib/kcl/bindings/FileEntry'
import { Project, FileEntry } from 'lib/project'
export type IndexLoaderData = {
code: string | null

View File

@ -1,6 +1,5 @@
import { assign, createMachine } from 'xstate'
import type { FileEntry } from 'lib/types'
import { Project } from 'wasm-lib/kcl/bindings/Project'
import { Project, FileEntry } from 'lib/project'
export const fileMachine = createMachine(
{

View File

@ -1,6 +1,6 @@
import { assign, createMachine } from 'xstate'
import { HomeCommandSchema } from 'lib/commandBarConfigs/homeCommandConfig'
import { Project } from 'wasm-lib/kcl/bindings/Project'
import { Project } from 'lib/project'
export const homeMachine = createMachine(
{

View File

@ -4,7 +4,6 @@ import {
VariableDeclarator,
parse,
recast,
sketchGroupFromKclValue,
} from 'lang/wasm'
import { Axis, Selection, Selections, updateSelections } from 'lib/selections'
import { assign, createMachine } from 'xstate'
@ -35,7 +34,7 @@ import {
setEqualLengthInfo,
} from 'components/Toolbar/EqualLength'
import { deleteFromSelection, extrudeSketch } from 'lang/modifyAst'
import { addFillet } from 'lang/modifyAst/addFillet'
import { applyFilletToSelection } from 'lang/modifyAst/addFillet'
import { getNodeFromPath } from '../lang/queryAst'
import {
applyConstraintEqualAngle,
@ -59,7 +58,6 @@ import { Coords2d } from 'lang/std/sketch'
import { deleteSegment } from 'clientSideScene/ClientSideSceneComp'
import { executeAst } from 'lang/langHelpers'
import toast from 'react-hot-toast'
import { getExtrusionFromSuspectedPath } from 'lang/std/artifactGraph'
export const MODELING_PERSIST_KEY = 'MODELING_PERSIST_KEY'
@ -1163,65 +1161,16 @@ export const modelingMachine = createMachine(
'AST fillet': async (_, event) => {
if (!event.data) return
// Extract inputs
const { selection, radius } = event.data
let ast = kclManager.ast
if (
'variableName' in radius &&
radius.variableName &&
radius.insertIndex !== undefined
) {
const newBody = [...ast.body]
newBody.splice(radius.insertIndex, 0, radius.variableDeclarationAst)
ast.body = newBody
}
const pathToSegmentNode = getNodePathFromSourceRange(
ast,
selection.codeBasedSelections[0].range
// Apply fillet to selection
const applyFilletToSelectionResult = applyFilletToSelection(
selection,
radius
)
const varDecNode = getNodeFromPath<VariableDeclaration>(
ast,
pathToSegmentNode,
'VariableDeclaration'
)
if (err(varDecNode)) return
const sketchVar = varDecNode.node.declarations[0].id.name
const sketchGroup = sketchGroupFromKclValue(
kclManager.programMemory.get(sketchVar),
sketchVar
)
if (trap(sketchGroup)) return
const extrusion = getExtrusionFromSuspectedPath(
sketchGroup.id,
engineCommandManager.artifactGraph
)
const pathToExtrudeNode = err(extrusion)
? []
: getNodePathFromSourceRange(ast, extrusion.codeRef.range)
// we assume that there is only one body related to the sketch
// and apply the fillet to it
const addFilletResult = addFillet(
ast,
pathToSegmentNode,
pathToExtrudeNode,
'variableName' in radius
? radius.variableIdentifierAst
: radius.valueAst
)
if (trap(addFilletResult)) return
const { modifiedAst, pathToFilletNode } = addFilletResult
const updatedAst = await kclManager.updateAst(modifiedAst, true, {
focusPath: pathToFilletNode,
})
if (updatedAst?.selections) {
editorManager.selectRange(updatedAst?.selections)
}
if (err(applyFilletToSelectionResult))
return applyFilletToSelectionResult
},
'conditionally equip line tool': (_, { type }) => {
if (type === 'done.invoke.animate-to-face') {

View File

@ -8,6 +8,13 @@ import { Issuer } from 'openid-client'
import { Bonjour, Service } from 'bonjour-service'
// @ts-ignore: TS1343
import * as kittycad from '@kittycad/lib/import'
import minimist from 'minimist'
import getCurrentProjectFile from 'lib/getCurrentProjectFile'
let mainWindow: BrowserWindow | null = null
// Check the command line arguments for a project path
const args = parseCLIArgs()
// If it's not set, scream.
const NODE_ENV = process.env.NODE_ENV || 'production'
@ -22,8 +29,25 @@ if (require('electron-squirrel-startup')) {
app.quit()
}
const createWindow = () => {
const mainWindow = new BrowserWindow({
const ZOO_STUDIO_PROTOCOL = 'zoo-studio'
/// Register our application to handle all "electron-fiddle://" protocols.
if (process.defaultApp) {
if (process.argv.length >= 2) {
app.setAsDefaultProtocolClient(ZOO_STUDIO_PROTOCOL, process.execPath, [
path.resolve(process.argv[1]),
])
}
} else {
app.setAsDefaultProtocolClient(ZOO_STUDIO_PROTOCOL)
}
// Global app listeners
// Must be done before ready event.
registerStartupListeners()
const createWindow = (): BrowserWindow => {
const newWindow = new BrowserWindow({
autoHideMenuBar: true,
show: false,
width: 1800,
@ -35,13 +59,15 @@ const createWindow = () => {
preload: path.join(__dirname, './preload.js'),
},
icon: path.resolve(process.cwd(), 'assets', 'icon.png'),
frame: false,
titleBarStyle: 'hiddenInset',
})
// and load the index.html of the app.
if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
mainWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL)
newWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL)
} else {
mainWindow.loadFile(
newWindow.loadFile(
path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`)
)
}
@ -49,7 +75,9 @@ const createWindow = () => {
// Open the DevTools.
// mainWindow.webContents.openDevTools()
mainWindow.show()
newWindow.show()
return newWindow
}
// Quit when all windows are closed, except on macOS. There, it's common
@ -64,7 +92,10 @@ app.on('window-all-closed', () => {
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', createWindow)
app.on('ready', (event, data) => {
// Create the mainWindow
mainWindow = createWindow()
})
// For now there is no good reason to separate these out to another file(s)
// There is just not enough code to warrant it and further abstracts everything
@ -159,3 +190,104 @@ ipcMain.handle('find_machine_api', () => {
)
})
})
ipcMain.handle('loadProjectAtStartup', async () => {
// If we are in development mode, we don't want to load a project at
// startup.
// Since the args passed are always '.'
if (NODE_ENV !== 'production') {
return null
}
let projectPath: string | null = null
// macOS: open-file events that were received before the app is ready
const macOpenFiles: string[] = (global as any).macOpenFiles
if (macOpenFiles && macOpenFiles && macOpenFiles.length > 0) {
projectPath = macOpenFiles[0] // We only do one project at a time
}
// Reset this so we don't accidentally use it again.
const macOpenFilesEmpty: string[] = []
// @ts-ignore
global['macOpenFiles'] = macOpenFilesEmpty
// macOS: open-url events that were received before the app is ready
const getOpenUrls: string[] = (global as any).getOpenUrls
if (getOpenUrls && getOpenUrls.length > 0) {
projectPath = getOpenUrls[0] // We only do one project at a
}
// Reset this so we don't accidentally use it again.
// @ts-ignore
global['getOpenUrls'] = []
// Check if we have a project path in the command line arguments
// If we do, we will load the project at that path
if (args._.length > 1) {
if (args._[1].length > 0) {
projectPath = args._[1]
// Reset all this value so we don't accidentally use it again.
args._[1] = ''
}
}
if (projectPath) {
// We have a project path, load the project information.
console.log(`Loading project at startup: ${projectPath}`)
try {
const currentFile = await getCurrentProjectFile(projectPath)
console.log(`Project loaded: ${currentFile}`)
return currentFile
} catch (e) {
console.error(e)
}
return null
}
return null
})
function parseCLIArgs(): minimist.ParsedArgs {
return minimist(process.argv, {})
}
function registerStartupListeners() {
/**
* macOS: when someone drops a file to the not-yet running VSCode, the open-file event fires even before
* the app-ready event. We listen very early for open-file and remember this upon startup as path to open.
*/
const macOpenFiles: string[] = []
// @ts-ignore
global['macOpenFiles'] = macOpenFiles
app.on('open-file', function (event, path) {
event.preventDefault()
macOpenFiles.push(path)
// If we have a mainWindow, lets open another window.
if (mainWindow) {
createWindow()
}
})
/**
* macOS: react to open-url requests.
*/
const openUrls: string[] = []
// @ts-ignore
global['openUrls'] = openUrls
const onOpenUrl = function (
event: { preventDefault: () => void },
url: string
) {
event.preventDefault()
openUrls.push(url)
// If we have a mainWindow, lets open another window.
if (mainWindow) {
createWindow()
}
}
app.on('will-finish-launching', function () {
app.on('open-url', onOpenUrl)
})
}

View File

@ -60,6 +60,9 @@ const listMachines = async (): Promise<MachinesListing> => {
const getMachineApiIp = async (): Promise<String | null> =>
ipcRenderer.invoke('find_machine_api')
const loadProjectAtStartup = async (): Promise<string | null> =>
ipcRenderer.invoke('loadProjectAtStartup')
contextBridge.exposeInMainWorld('electron', {
login,
// Passing fs directly is not recommended since it gives a lot of power
@ -93,6 +96,7 @@ contextBridge.exposeInMainWorld('electron', {
isWindows,
isLinux,
},
loadProjectAtStartup,
// IMPORTANT NOTE: kittycad.ts reads process.env.BASE_URL. But there is
// no way to set it across the bridge boundary. We need to make it a command.
setBaseUrl: (value: string) => (process.env.BASE_URL = value),

View File

@ -31,13 +31,13 @@ import { kclManager } from 'lib/singletons'
import { useLspContext } from 'components/LspProvider'
import { useRefreshSettings } from 'hooks/useRefreshSettings'
import { LowerRightControls } from 'components/LowerRightControls'
import { Project } from 'wasm-lib/kcl/bindings/Project'
import {
createNewProjectDirectory,
listProjects,
renameProjectDirectory,
} from 'lib/desktop'
import { ProjectSearchBar, useProjectSearch } from 'components/ProjectSearchBar'
import { Project } from 'lib/project'
// This route only opens in the desktop context for now,
// as defined in Router.tsx, so we can use the desktop APIs and types.

156
src/wasm-lib/Cargo.lock generated
View File

@ -70,54 +70,12 @@ version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
[[package]]
name = "anstream"
version = "0.6.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1"
[[package]]
name = "anstyle-parse"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648"
dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7"
dependencies = [
"anstyle",
"windows-sys 0.52.0",
]
[[package]]
name = "anyhow"
version = "1.0.86"
@ -169,7 +127,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.75",
"syn 2.0.76",
]
[[package]]
@ -180,7 +138,7 @@ checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.75",
"syn 2.0.76",
]
[[package]]
@ -191,7 +149,7 @@ checksum = "3c87f3f15e7794432337fc718554eaa4dc8f04c9677a950ffe366f20a162ae42"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.75",
"syn 2.0.76",
]
[[package]]
@ -426,12 +384,8 @@ version = "4.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim 0.11.0",
"unicase",
"unicode-width",
]
[[package]]
@ -443,7 +397,7 @@ dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.75",
"syn 2.0.76",
]
[[package]]
@ -452,12 +406,6 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce"
[[package]]
name = "colorchoice"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
[[package]]
name = "colored"
version = "2.1.0"
@ -642,8 +590,8 @@ dependencies = [
"ident_case",
"proc-macro2",
"quote",
"strsim 0.10.0",
"syn 2.0.75",
"strsim",
"syn 2.0.76",
]
[[package]]
@ -654,7 +602,7 @@ checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f"
dependencies = [
"darling_core",
"quote",
"syn 2.0.75",
"syn 2.0.76",
]
[[package]]
@ -709,7 +657,7 @@ checksum = "4078275de501a61ceb9e759d37bdd3d7210e654dbc167ac1a3678ef4435ed57b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.75",
"syn 2.0.76",
"synstructure",
]
@ -738,7 +686,7 @@ dependencies = [
"rustfmt-wrapper",
"serde",
"serde_tokenstream",
"syn 2.0.75",
"syn 2.0.76",
]
[[package]]
@ -749,7 +697,7 @@ checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.75",
"syn 2.0.76",
]
[[package]]
@ -776,7 +724,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.75",
"syn 2.0.76",
]
[[package]]
@ -948,7 +896,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.75",
"syn 2.0.76",
]
[[package]]
@ -1038,7 +986,7 @@ dependencies = [
"inflections",
"proc-macro2",
"quote",
"syn 2.0.75",
"syn 2.0.76",
]
[[package]]
@ -1397,7 +1345,7 @@ dependencies = [
[[package]]
name = "kcl-lib"
version = "0.2.10"
version = "0.2.11"
dependencies = [
"anyhow",
"approx",
@ -1464,7 +1412,7 @@ dependencies = [
"pretty_assertions",
"proc-macro2",
"quote",
"syn 2.0.75",
"syn 2.0.76",
]
[[package]]
@ -1492,7 +1440,6 @@ dependencies = [
"bigdecimal",
"bytes",
"chrono",
"clap",
"data-encoding",
"format_serde_error",
"futures",
@ -1852,7 +1799,7 @@ dependencies = [
"regex",
"regex-syntax 0.8.3",
"structmeta",
"syn 2.0.75",
"syn 2.0.76",
]
[[package]]
@ -1870,7 +1817,7 @@ dependencies = [
"bincode",
"either",
"fnv",
"itertools 0.10.5",
"itertools 0.12.1",
"lazy_static",
"nom",
"quick-xml",
@ -1905,7 +1852,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.75",
"syn 2.0.76",
]
[[package]]
@ -2069,7 +2016,7 @@ dependencies = [
"proc-macro2",
"pyo3-macros-backend",
"quote",
"syn 2.0.75",
"syn 2.0.76",
]
[[package]]
@ -2082,7 +2029,7 @@ dependencies = [
"proc-macro2",
"pyo3-build-config",
"quote",
"syn 2.0.75",
"syn 2.0.76",
]
[[package]]
@ -2544,7 +2491,7 @@ dependencies = [
"proc-macro2",
"quote",
"serde_derive_internals",
"syn 2.0.75",
"syn 2.0.76",
]
[[package]]
@ -2594,9 +2541,9 @@ checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca"
[[package]]
name = "serde"
version = "1.0.208"
version = "1.0.209"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cff085d2cb684faa248efb494c39b68e522822ac0de72ccf08109abde717cfb2"
checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09"
dependencies = [
"serde_derive",
]
@ -2612,13 +2559,13 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.208"
version = "1.0.209"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24008e81ff7613ed8e5ba0cfaf24e2c2f1e5b8a0495711e44fcd4882fca62bcf"
checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.75",
"syn 2.0.76",
]
[[package]]
@ -2629,14 +2576,14 @@ checksum = "330f01ce65a3a5fe59a60c82f3c9a024b573b8a6e875bd233fe5f934e71d54e3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.75",
"syn 2.0.76",
]
[[package]]
name = "serde_json"
version = "1.0.125"
version = "1.0.127"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83c8e735a073ccf5be70aa8066aa984eaf2fa000db6c8d0100ae605b366d31ed"
checksum = "8043c06d9f82bd7271361ed64f415fe5e12a77fdb52e573e7f06a516dea329ad"
dependencies = [
"indexmap 2.2.5",
"itoa",
@ -2653,7 +2600,7 @@ checksum = "0b2e6b945e9d3df726b65d6ee24060aff8e3533d431f677a9695db04eff9dfdb"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.75",
"syn 2.0.76",
]
[[package]]
@ -2674,7 +2621,7 @@ dependencies = [
"proc-macro2",
"quote",
"serde",
"syn 2.0.75",
"syn 2.0.76",
]
[[package]]
@ -2796,12 +2743,6 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "strsim"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01"
[[package]]
name = "structmeta"
version = "0.3.0"
@ -2811,7 +2752,7 @@ dependencies = [
"proc-macro2",
"quote",
"structmeta-derive",
"syn 2.0.75",
"syn 2.0.76",
]
[[package]]
@ -2822,7 +2763,7 @@ checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.75",
"syn 2.0.76",
]
[[package]]
@ -2866,9 +2807,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.75"
version = "2.0.76"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6af063034fc1935ede7be0122941bafa9bacb949334d090b77ca98b5817c7d9"
checksum = "578e081a14e0cefc3279b0472138c513f37b41a08d5a3cca9b6e4e8ceb6cd525"
dependencies = [
"proc-macro2",
"quote",
@ -2889,7 +2830,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.75",
"syn 2.0.76",
]
[[package]]
@ -2996,7 +2937,7 @@ checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.75",
"syn 2.0.76",
]
[[package]]
@ -3091,7 +3032,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.75",
"syn 2.0.76",
]
[[package]]
@ -3244,7 +3185,7 @@ checksum = "84fd902d4e0b9a4b27f2f440108dc034e1758628a9b702f8ec61ad66355422fa"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.75",
"syn 2.0.76",
]
[[package]]
@ -3272,7 +3213,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.75",
"syn 2.0.76",
]
[[package]]
@ -3349,7 +3290,7 @@ checksum = "c88cc88fd23b5a04528f3a8436024f20010a16ec18eb23c164b1242f65860130"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.75",
"syn 2.0.76",
"termcolor",
]
@ -3469,12 +3410,6 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]]
name = "utf8parse"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]]
name = "uuid"
version = "1.10.0"
@ -3513,7 +3448,7 @@ dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
"syn 2.0.75",
"syn 2.0.76",
]
[[package]]
@ -3574,7 +3509,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.75",
"syn 2.0.76",
"wasm-bindgen-shared",
]
@ -3609,7 +3544,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.75",
"syn 2.0.76",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@ -3626,7 +3561,6 @@ version = "0.1.0"
dependencies = [
"anyhow",
"bson",
"clap",
"console_error_panic_hook",
"data-encoding",
"futures",
@ -3935,7 +3869,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.75",
"syn 2.0.76",
]
[[package]]

View File

@ -11,12 +11,11 @@ crate-type = ["cdylib"]
[dependencies]
bson = { version = "2.11.0", features = ["uuid-1", "chrono"] }
clap = "4.5.16"
data-encoding = "2.6.0"
gloo-utils = "0.2.0"
kcl-lib = { path = "kcl" }
kittycad.workspace = true
serde_json = "1.0.125"
serde_json = "1.0.127"
tokio = { version = "1.39.3", features = ["sync"] }
toml = "0.8.19"
uuid = { version = "1.10.0", features = ["v4", "js", "serde"] }

View File

@ -18,9 +18,9 @@ once_cell = "1.19.0"
proc-macro2 = "1"
quote = "1"
regex = "1.10"
serde = { version = "1.0.208", features = ["derive"] }
serde = { version = "1.0.209", features = ["derive"] }
serde_tokenstream = "0.2"
syn = { version = "2.0.75", features = ["full"] }
syn = { version = "2.0.76", features = ["full"] }
[dev-dependencies]
anyhow = "1.0.86"

View File

@ -15,7 +15,7 @@ databake = "0.1.8"
kcl-lib = { path = "../kcl" }
proc-macro2 = "1"
quote = "1"
syn = { version = "2.0.75", features = ["full"] }
syn = { version = "2.0.76", features = ["full"] }
[dev-dependencies]
pretty_assertions = "1.4.0"

View File

@ -10,6 +10,6 @@ anyhow = "1.0.86"
hyper = { version = "0.14.29", features = ["server"] }
kcl-lib = { version = "0.2", path = "../kcl" }
pico-args = "0.5.0"
serde = { version = "1.0.208", features = ["derive"] }
serde_json = "1.0.125"
serde = { version = "1.0.209", features = ["derive"] }
serde_json = "1.0.127"
tokio = { version = "1.39.3", features = ["macros", "rt-multi-thread"] }

View File

@ -1,7 +1,7 @@
[package]
name = "kcl-lib"
description = "KittyCAD Language implementation and tools"
version = "0.2.10"
version = "0.2.11"
edition = "2021"
license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app"
@ -16,7 +16,7 @@ async-recursion = "1.1.1"
async-trait = "0.1.81"
base64 = "0.22.1"
chrono = "0.4.38"
clap = { version = "4.5.16", default-features = false, optional = true }
clap = { version = "4.5.16", default-features = false, optional = true, features = ["std", "derive"] }
convert_case = "0.6.0"
dashmap = "6.0.1"
databake = { version = "0.1.8", features = ["derive"] }
@ -27,7 +27,7 @@ git_rev = "0.1.0"
gltf-json = "1.4.1"
http = { workspace = true }
image = { version = "0.25.1", default-features = false, features = ["png"] }
kittycad = { workspace = true, features = ["clap"] }
kittycad = { workspace = true }
lazy_static = "1.5.0"
measurements = "0.11.0"
mime_guess = "2.0.5"
@ -36,8 +36,8 @@ pyo3 = { version = "0.22.2", optional = true }
reqwest = { version = "0.11.26", default-features = false, features = ["stream", "rustls-tls"] }
ropey = "1.6.1"
schemars = { version = "0.8.17", features = ["impl_json_schema", "url", "uuid1"] }
serde = { version = "1.0.208", features = ["derive"] }
serde_json = "1.0.125"
serde = { version = "1.0.209", features = ["derive"] }
serde_json = "1.0.127"
sha2 = "0.10.8"
tabled = { version = "0.15.0", optional = true }
thiserror = "1.0.63"
@ -66,7 +66,7 @@ tokio-tungstenite = { version = "0.23.1", features = ["rustls-tls-native-roots"]
tower-lsp = { version = "0.20.0", features = ["proposed"] }
[features]
default = ["cli", "engine"]
default = ["engine"]
cli = ["dep:clap"]
# For the lsp server, when run with stdout for rpc we want to disable println.
# This is used for editor extensions that use the lsp server.

View File

@ -18,3 +18,11 @@ We've built a lot of tooling to make contributing to KCL easier. If you are inte
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.
## Bumping the version
If you bump the version of kcl-lib and push it to crates, be sure to update the repos we own that use it as well. These are:
- [kcl.py](https://github.com/kittycad/kcl.py)
- [kcl-lsp](https://github.com/kittycad/kcl-lsp)
- [cli](https://github.com/kittycad/cli)

View File

@ -70,55 +70,6 @@ dependencies = [
"libc",
]
[[package]]
name = "anstream"
version = "0.6.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1"
[[package]]
name = "anstyle-parse"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5"
dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19"
dependencies = [
"anstyle",
"windows-sys 0.52.0",
]
[[package]]
name = "anyhow"
version = "1.0.86"
@ -377,54 +328,6 @@ dependencies = [
"windows-targets 0.52.5",
]
[[package]]
name = "clap"
version = "4.5.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed6719fffa43d0d87e5fd8caeab59be1554fb028cd30edc88fc4369b17971019"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
"unicase",
"unicode-width",
]
[[package]]
name = "clap_derive"
version = "4.5.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0"
dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.75",
]
[[package]]
name = "clap_lex"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce"
[[package]]
name = "colorchoice"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422"
[[package]]
name = "colored"
version = "2.1.0"
@ -596,7 +499,7 @@ dependencies = [
[[package]]
name = "derive-docs"
version = "0.1.24"
version = "0.1.25"
dependencies = [
"Inflector",
"convert_case",
@ -906,12 +809,6 @@ version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hermit-abi"
version = "0.3.9"
@ -1090,12 +987,6 @@ version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
[[package]]
name = "is_terminal_polyfill"
version = "1.70.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800"
[[package]]
name = "itertools"
version = "0.12.1"
@ -1140,7 +1031,7 @@ dependencies = [
[[package]]
name = "kcl-lib"
version = "0.2.6"
version = "0.2.10"
dependencies = [
"anyhow",
"approx",
@ -1149,7 +1040,6 @@ dependencies = [
"base64 0.22.1",
"bson",
"chrono",
"clap",
"convert_case",
"dashmap 6.0.1",
"databake",
@ -1158,6 +1048,7 @@ dependencies = [
"futures",
"git_rev",
"gltf-json",
"http 0.2.12",
"image",
"js-sys",
"kittycad",
@ -1198,9 +1089,9 @@ dependencies = [
[[package]]
name = "kittycad"
version = "0.3.14"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce5e9c51976882cdf6777557fd8c3ee68b00bb53e9307fc1721acb397f2ece9a"
checksum = "fbb7c076d64ad00a29ae900108707d1bbb583944d4b2d005e1eca9914a18c7c2"
dependencies = [
"anyhow",
"async-trait",
@ -1208,7 +1099,6 @@ dependencies = [
"bigdecimal",
"bytes",
"chrono",
"clap",
"data-encoding",
"format_serde_error",
"futures",
@ -2197,7 +2087,7 @@ version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946"
dependencies = [
"heck 0.4.1",
"heck",
"proc-macro2",
"quote",
"rustversion",
@ -2649,12 +2539,6 @@ version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
[[package]]
name = "unicode-width"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6"
[[package]]
name = "untrusted"
version = "0.9.0"
@ -2685,12 +2569,6 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]]
name = "utf8parse"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]]
name = "uuid"
version = "1.10.0"

View File

@ -1,5 +1,3 @@
//! This module contains settings for kcl projects as well as the modeling app.
pub mod types;
#[cfg(not(target_arch = "wasm32"))]
pub mod utils;

View File

@ -1,893 +0,0 @@
//! Types for interacting with files in projects.
#[cfg(not(target_arch = "wasm32"))]
use std::path::{Path, PathBuf};
use anyhow::Result;
use parse_display::{Display, FromStr};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
/// State management for the application.
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq)]
#[ts(export)]
#[serde(rename_all = "snake_case")]
pub struct ProjectState {
pub project: Project,
pub current_file: Option<String>,
}
impl ProjectState {
/// Create a new project state from a path.
#[cfg(not(target_arch = "wasm32"))]
pub async fn new_from_path(path: PathBuf) -> Result<ProjectState> {
// Fix for "." path, which is the current directory.
let source_path = if path == Path::new(".") {
std::env::current_dir().map_err(|e| anyhow::anyhow!("Error getting the current directory: {:?}", e))?
} else {
path
};
// Url decode the path.
let source_path =
std::path::Path::new(&urlencoding::decode(&source_path.display().to_string())?.to_string()).to_path_buf();
// If the path does not start with a slash, it is a relative path.
// We need to convert it to an absolute path.
let source_path = if source_path.is_relative() {
std::env::current_dir()
.map_err(|e| anyhow::anyhow!("Error getting the current directory: {:?}", e))?
.join(source_path)
} else {
source_path
};
// If the path is a directory, let's assume it is a project directory.
if source_path.is_dir() {
// Load the details about the project from the path.
let project = Project::from_path(&source_path)
.await
.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.
let project_file = source_path.join(crate::settings::types::DEFAULT_PROJECT_KCL_FILE);
if !project_file.exists() {
// Create the default file in the project.
// Write the initial project file.
tokio::fs::write(&project_file, vec![]).await?;
}
return Ok(ProjectState {
project,
current_file: Some(project_file.display().to_string()),
});
}
// Check if the extension on what we are trying to open is a relevant file type.
// Get the extension of the file.
let extension = source_path
.extension()
.ok_or_else(|| anyhow::anyhow!("Error getting the extension of the file: `{}`", source_path.display()))?;
let ext = extension.to_string_lossy().to_string();
// Check if the extension is a relevant file type.
if !crate::settings::utils::RELEVANT_EXTENSIONS.contains(&ext) || ext == "toml" {
return Err(anyhow::anyhow!(
"File type ({}) cannot be opened with this app: `{}`, try opening one of the following file types: {}",
ext,
source_path.display(),
crate::settings::utils::RELEVANT_EXTENSIONS.join(", ")
));
}
// We were given a file path, not a directory.
// Let's get the parent directory of the file.
let parent = source_path.parent().ok_or_else(|| {
anyhow::anyhow!(
"Error getting the parent directory of the file: {}",
source_path.display()
)
})?;
// If we got a import model file, we need to check if we have a file in the project for
// this import model.
if crate::settings::utils::IMPORT_FILE_EXTENSIONS.contains(&ext) {
let import_file_name = source_path
.file_name()
.ok_or_else(|| anyhow::anyhow!("Error getting the file name of the file: {}", source_path.display()))?
.to_string_lossy()
.to_string();
// Check if we have a file in the project for this import model.
let kcl_wrapper_filename = format!("{}.kcl", import_file_name);
let kcl_wrapper_file_path = parent.join(&kcl_wrapper_filename);
if !kcl_wrapper_file_path.exists() {
// Create the file in the project.
// With the default import content.
tokio::fs::write(
&kcl_wrapper_file_path,
format!(
r#"// This file was automatically generated by the application when you
// double-clicked on the model file.
// You can edit this file to add your own content.
// But we recommend you keep the import statement as it is.
// For more information on the import statement, see the documentation at:
// https://zoo.dev/docs/kcl/import
const model = import("{}")"#,
import_file_name
)
.as_bytes(),
)
.await?;
}
// Load the details about the project from the parent directory.
// We do this after we generate the import file so that the file is included in the project.
let project = Project::from_path(&parent)
.await
.map_err(|e| anyhow::anyhow!("Error loading project from path {}: {:?}", source_path.display(), e))?;
return Ok(ProjectState {
project,
current_file: Some(kcl_wrapper_file_path.display().to_string()),
});
}
// Load the details about the project from the parent directory.
let project = Project::from_path(&parent)
.await
.map_err(|e| anyhow::anyhow!("Error loading project from path {}: {:?}", source_path.display(), e))?;
Ok(ProjectState {
project,
current_file: Some(source_path.display().to_string()),
})
}
}
/// Information about project.
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq)]
#[ts(export)]
#[serde(rename_all = "snake_case")]
pub struct Project {
#[serde(flatten)]
pub file: FileEntry,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata: Option<FileMetadata>,
#[serde(default)]
#[ts(type = "number")]
pub kcl_file_count: u64,
#[serde(default)]
#[ts(type = "number")]
pub directory_count: u64,
/// The default file to open on load.
pub default_file: String,
}
impl Project {
#[cfg(not(target_arch = "wasm32"))]
/// Populate a project from a path.
pub async fn from_path<P: AsRef<std::path::Path>>(path: P) -> Result<Self> {
// Check if they are using '.' as the path.
let path = if path.as_ref() == std::path::Path::new(".") {
std::env::current_dir()?
} else {
path.as_ref().to_path_buf()
};
// Make sure the path exists.
if !path.exists() {
return Err(anyhow::anyhow!("Path does not exist"));
}
let file = crate::settings::utils::walk_dir(&path).await?;
let metadata = std::fs::metadata(&path).ok().map(|m| m.into());
let mut project = Self {
file: file.clone(),
metadata,
kcl_file_count: 0,
directory_count: 0,
default_file: get_default_kcl_file_for_dir(path, file).await?,
};
project.populate_kcl_file_count()?;
project.populate_directory_count()?;
Ok(project)
}
/// Populate the number of KCL files in the project.
pub fn populate_kcl_file_count(&mut self) -> Result<()> {
let mut count = 0;
if let Some(children) = &self.file.children {
for entry in children.iter() {
if entry.name.ends_with(".kcl") {
count += 1;
} else {
count += entry.kcl_file_count();
}
}
}
self.kcl_file_count = count;
Ok(())
}
/// Populate the number of directories in the project.
pub fn populate_directory_count(&mut self) -> Result<()> {
let mut count = 0;
if let Some(children) = &self.file.children {
for entry in children.iter() {
count += entry.directory_count();
}
}
self.directory_count = count;
Ok(())
}
}
/// 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())
}
#[cfg(not(target_arch = "wasm32"))]
/// Rename a directory for a project.
/// This returns the new path of the directory.
pub async fn rename_project_directory<P>(path: P, new_name: &str) -> Result<std::path::PathBuf>
where
P: AsRef<Path> + Send,
{
if new_name.is_empty() {
return Err(anyhow::anyhow!("New name for project cannot be empty"));
}
// Make sure the path is a directory.
if !path.as_ref().is_dir() {
return Err(anyhow::anyhow!("Path `{}` is not a directory", path.as_ref().display()));
}
// Make sure the new name does not exist.
let new_path = path
.as_ref()
.parent()
.ok_or_else(|| anyhow::anyhow!("Parent directory of `{}` not found", path.as_ref().display()))?
.join(new_name);
if new_path.exists() {
return Err(anyhow::anyhow!(
"Path `{}` already exists, cannot rename to an existing path",
new_path.display()
));
}
tokio::fs::rename(path.as_ref(), &new_path).await?;
Ok(new_path)
}
/// Information about a file or directory.
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq)]
#[ts(export)]
#[serde(rename_all = "snake_case")]
pub struct FileEntry {
pub path: String,
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub children: Option<Vec<FileEntry>>,
}
impl FileEntry {
/// Recursively get the number of kcl files in the file entry.
pub fn kcl_file_count(&self) -> u64 {
let mut count = 0;
if let Some(children) = &self.children {
for entry in children.iter() {
if entry.name.ends_with(".kcl") {
count += 1;
} else {
count += entry.kcl_file_count();
}
}
}
count
}
/// Recursively get the number of directories in the file entry.
pub fn directory_count(&self) -> u64 {
let mut count = 0;
if let Some(children) = &self.children {
for entry in children.iter() {
if entry.children.is_some() {
count += 1;
}
}
}
count
}
}
/// Metadata about a file or directory.
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq)]
#[ts(export)]
#[serde(rename_all = "snake_case")]
pub struct FileMetadata {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub accessed: Option<chrono::DateTime<chrono::Utc>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub created: Option<chrono::DateTime<chrono::Utc>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub r#type: Option<FileType>,
#[serde(default)]
#[ts(type = "number")]
pub size: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub modified: Option<chrono::DateTime<chrono::Utc>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub permission: Option<FilePermission>,
}
/// The type of a file.
#[derive(Debug, Copy, Clone, Deserialize, Serialize, JsonSchema, Display, FromStr, ts_rs::TS, PartialEq, Eq)]
#[ts(export)]
#[serde(rename_all = "snake_case")]
#[display(style = "snake_case")]
pub enum FileType {
/// A file.
File,
/// A directory.
Directory,
/// A symbolic link.
Symlink,
}
/// The permissions of a file.
#[derive(Debug, Copy, Clone, Deserialize, Serialize, JsonSchema, Display, FromStr, ts_rs::TS, PartialEq, Eq)]
#[ts(export)]
#[serde(rename_all = "snake_case")]
#[display(style = "snake_case")]
pub enum FilePermission {
/// Read permission.
Read,
/// Write permission.
Write,
/// Execute permission.
Execute,
}
impl From<std::fs::FileType> for FileType {
fn from(file_type: std::fs::FileType) -> Self {
if file_type.is_file() {
FileType::File
} else if file_type.is_dir() {
FileType::Directory
} else if file_type.is_symlink() {
FileType::Symlink
} else {
unreachable!()
}
}
}
impl From<std::fs::Permissions> for FilePermission {
fn from(permissions: std::fs::Permissions) -> Self {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mode = permissions.mode();
if mode & 0o400 != 0 {
FilePermission::Read
} else if mode & 0o200 != 0 {
FilePermission::Write
} else if mode & 0o100 != 0 {
FilePermission::Execute
} else {
unreachable!()
}
}
#[cfg(not(unix))]
{
if permissions.readonly() {
FilePermission::Read
} else {
FilePermission::Write
}
}
}
}
impl From<std::fs::Metadata> for FileMetadata {
fn from(metadata: std::fs::Metadata) -> Self {
Self {
accessed: metadata.accessed().ok().map(|t| t.into()),
created: metadata.created().ok().map(|t| t.into()),
r#type: Some(metadata.file_type().into()),
size: metadata.len(),
modified: metadata.modified().ok().map(|t| t.into()),
permission: Some(metadata.permissions().into()),
}
}
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
#[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();
}
#[tokio::test]
async fn test_rename_project_directory_empty_dir() {
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 new_name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
let new_dir = super::rename_project_directory(&dir, &new_name).await.unwrap();
assert_eq!(new_dir, std::env::temp_dir().join(&new_name));
std::fs::remove_dir_all(new_dir).unwrap();
}
#[tokio::test]
async fn test_rename_project_directory_empty_name() {
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 result = super::rename_project_directory(&dir, "").await;
assert!(result.is_err());
assert_eq!(result.unwrap_err().to_string(), "New name for project cannot be empty");
std::fs::remove_dir_all(dir).unwrap();
}
#[tokio::test]
async fn test_rename_project_directory_non_empty_dir() {
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 new_name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
let new_dir = super::rename_project_directory(&dir, &new_name).await.unwrap();
assert_eq!(new_dir, std::env::temp_dir().join(&new_name));
std::fs::remove_dir_all(new_dir).unwrap();
}
#[tokio::test]
async fn test_rename_project_directory_non_empty_dir_recursive() {
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 new_name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
let new_dir = super::rename_project_directory(&dir, &new_name).await.unwrap();
assert_eq!(new_dir, std::env::temp_dir().join(&new_name));
std::fs::remove_dir_all(new_dir).unwrap();
}
#[tokio::test]
async fn test_rename_project_directory_dir_is_file() {
let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
let dir = std::env::temp_dir().join(&name);
std::fs::write(&dir, vec![]).unwrap();
let new_name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
let result = super::rename_project_directory(&dir, &new_name).await;
assert!(result.is_err());
assert_eq!(
result.unwrap_err().to_string(),
format!("Path `{}` is not a directory", dir.display())
);
std::fs::remove_file(dir).unwrap();
}
#[tokio::test]
async fn test_rename_project_directory_new_name_exists() {
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 new_name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
let new_dir = std::env::temp_dir().join(&new_name);
std::fs::create_dir_all(&new_dir).unwrap();
let result = super::rename_project_directory(&dir, &new_name).await;
assert!(result.is_err());
assert_eq!(
result.unwrap_err().to_string(),
format!(
"Path `{}` already exists, cannot rename to an existing path",
new_dir.display()
)
);
std::fs::remove_dir_all(new_dir).unwrap();
}
#[tokio::test]
async fn test_project_state_new_from_path_source_path_dot() {
let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
let tmp_project_dir = std::env::temp_dir().join(&name);
std::fs::create_dir_all(&tmp_project_dir).unwrap();
// Set the current directory to the temp project directory.
// This is to simulate the "." path.
std::env::set_current_dir(&tmp_project_dir).unwrap();
let state = super::ProjectState::new_from_path(std::path::PathBuf::from("."))
.await
.unwrap();
assert_eq!(state.project.file.name, name);
assert_eq!(
state
.project
.file
.path
// macOS adds /private to the path i think because we changed curdirs
.trim_start_matches("/private"),
tmp_project_dir.display().to_string()
);
assert_eq!(
state
.current_file
.unwrap()
// macOS adds /private to the path i think because we changed curdirs
.trim_start_matches("/private"),
tmp_project_dir.join("main.kcl").display().to_string()
);
assert_eq!(
state
.project
.default_file
// macOS adds /private to the path i think because we changed curdirs
.trim_start_matches("/private"),
tmp_project_dir.join("main.kcl").display().to_string()
);
std::fs::remove_dir_all(tmp_project_dir).unwrap();
}
#[tokio::test]
async fn test_project_state_new_from_path_main_kcl_not_exists() {
let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
let tmp_project_dir = std::env::temp_dir().join(&name);
std::fs::create_dir_all(&tmp_project_dir).unwrap();
let state = super::ProjectState::new_from_path(tmp_project_dir.clone())
.await
.unwrap();
assert_eq!(state.project.file.name, name);
assert_eq!(state.project.file.path, tmp_project_dir.display().to_string());
assert_eq!(
state.current_file,
Some(tmp_project_dir.join("main.kcl").display().to_string())
);
assert_eq!(
state.project.default_file,
tmp_project_dir.join("main.kcl").display().to_string()
);
std::fs::remove_dir_all(tmp_project_dir).unwrap();
}
#[tokio::test]
async fn test_project_state_new_from_path_main_kcl_exists() {
let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
let tmp_project_dir = std::env::temp_dir().join(&name);
std::fs::create_dir_all(&tmp_project_dir).unwrap();
std::fs::write(tmp_project_dir.join("main.kcl"), vec![]).unwrap();
let state = super::ProjectState::new_from_path(tmp_project_dir.clone())
.await
.unwrap();
assert_eq!(state.project.file.name, name);
assert_eq!(state.project.file.path, tmp_project_dir.display().to_string());
assert_eq!(
state.current_file,
Some(tmp_project_dir.join("main.kcl").display().to_string())
);
assert_eq!(
state.project.default_file,
tmp_project_dir.join("main.kcl").display().to_string()
);
std::fs::remove_dir_all(tmp_project_dir).unwrap();
}
#[tokio::test]
async fn test_project_state_new_from_path_explicit_open_main_kcl() {
let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
let tmp_project_dir = std::env::temp_dir().join(&name);
std::fs::create_dir_all(&tmp_project_dir).unwrap();
std::fs::write(tmp_project_dir.join("main.kcl"), vec![]).unwrap();
let state = super::ProjectState::new_from_path(tmp_project_dir.join("main.kcl"))
.await
.unwrap();
assert_eq!(state.project.file.name, name);
assert_eq!(state.project.file.path, tmp_project_dir.display().to_string());
assert_eq!(
state.current_file,
Some(tmp_project_dir.join("main.kcl").display().to_string())
);
assert_eq!(
state.project.default_file,
tmp_project_dir.join("main.kcl").display().to_string()
);
std::fs::remove_dir_all(tmp_project_dir).unwrap();
}
#[tokio::test]
async fn test_project_state_new_from_path_explicit_open_thing_kcl() {
let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
let tmp_project_dir = std::env::temp_dir().join(&name);
std::fs::create_dir_all(&tmp_project_dir).unwrap();
std::fs::write(tmp_project_dir.join("thing.kcl"), vec![]).unwrap();
let state = super::ProjectState::new_from_path(tmp_project_dir.join("thing.kcl"))
.await
.unwrap();
assert_eq!(state.project.file.name, name);
assert_eq!(state.project.file.path, tmp_project_dir.display().to_string());
assert_eq!(
state.current_file,
Some(tmp_project_dir.join("thing.kcl").display().to_string())
);
assert_eq!(
state.project.default_file,
tmp_project_dir.join("thing.kcl").display().to_string()
);
std::fs::remove_dir_all(tmp_project_dir).unwrap();
}
#[tokio::test]
async fn test_project_state_new_from_path_explicit_open_model_obj() {
let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
let tmp_project_dir = std::env::temp_dir().join(&name);
std::fs::create_dir_all(&tmp_project_dir).unwrap();
std::fs::write(tmp_project_dir.join("model.obj"), vec![]).unwrap();
let state = super::ProjectState::new_from_path(tmp_project_dir.join("model.obj"))
.await
.unwrap();
assert_eq!(state.project.file.name, name);
assert_eq!(state.project.file.path, tmp_project_dir.display().to_string());
assert_eq!(
state.current_file,
Some(tmp_project_dir.join("model.obj.kcl").display().to_string())
);
assert_eq!(
state.project.default_file,
tmp_project_dir.join("model.obj.kcl").display().to_string()
);
// Get the contents of the generated kcl file.
let kcl_file_contents = tokio::fs::read(tmp_project_dir.join("model.obj.kcl")).await.unwrap();
assert_eq!(
String::from_utf8_lossy(&kcl_file_contents),
r#"// This file was automatically generated by the application when you
// double-clicked on the model file.
// You can edit this file to add your own content.
// But we recommend you keep the import statement as it is.
// For more information on the import statement, see the documentation at:
// https://zoo.dev/docs/kcl/import
const model = import("model.obj")"#
);
std::fs::remove_dir_all(tmp_project_dir).unwrap();
}
#[tokio::test]
async fn test_project_state_new_from_path_explicit_open_settings_toml() {
let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
let tmp_project_dir = std::env::temp_dir().join(&name);
std::fs::create_dir_all(&tmp_project_dir).unwrap();
std::fs::write(tmp_project_dir.join("settings.toml"), vec![]).unwrap();
let result = super::ProjectState::new_from_path(tmp_project_dir.join("settings.toml")).await;
assert!(result.is_err());
assert_eq!(result.unwrap_err().to_string(), format!("File type (toml) cannot be opened with this app: `{}`, try opening one of the following file types: stp, glb, fbxb, fbx, gltf, obj, ply, sldprt, step, stl, kcl", tmp_project_dir.join("settings.toml").display()));
std::fs::remove_dir_all(tmp_project_dir).unwrap();
}
#[tokio::test]
async fn test_project_state_new_from_path_explicit_open_non_relevant_file() {
let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
let tmp_project_dir = std::env::temp_dir().join(&name);
std::fs::create_dir_all(&tmp_project_dir).unwrap();
std::fs::write(tmp_project_dir.join("settings.docx"), vec![]).unwrap();
let result = super::ProjectState::new_from_path(tmp_project_dir.join("settings.docx")).await;
assert!(result.is_err());
assert_eq!(result.unwrap_err().to_string(), format!("File type (docx) cannot be opened with this app: `{}`, try opening one of the following file types: stp, glb, fbxb, fbx, gltf, obj, ply, sldprt, step, stl, kcl", tmp_project_dir.join("settings.docx").display()));
std::fs::remove_dir_all(tmp_project_dir).unwrap();
}
#[tokio::test]
async fn test_project_state_new_from_path_explicit_open_no_file_extension() {
let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
let tmp_project_dir = std::env::temp_dir().join(&name);
std::fs::create_dir_all(&tmp_project_dir).unwrap();
std::fs::write(tmp_project_dir.join("file"), vec![]).unwrap();
let result = super::ProjectState::new_from_path(tmp_project_dir.join("file")).await;
assert!(result.is_err());
assert_eq!(
result.unwrap_err().to_string(),
format!(
"Error getting the extension of the file: `{}`",
tmp_project_dir.join("file").display()
)
);
std::fs::remove_dir_all(tmp_project_dir).unwrap();
}
#[tokio::test]
async fn test_project_state_new_from_path_explicit_open_file_with_space_kcl() {
let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
let tmp_project_dir = std::env::temp_dir().join(&name);
std::fs::create_dir_all(&tmp_project_dir).unwrap();
std::fs::write(tmp_project_dir.join("i have a space.kcl"), vec![]).unwrap();
let state = super::ProjectState::new_from_path(tmp_project_dir.join("i have a space.kcl"))
.await
.unwrap();
assert_eq!(state.project.file.name, name);
assert_eq!(state.project.file.path, tmp_project_dir.display().to_string());
assert_eq!(
state.current_file,
Some(tmp_project_dir.join("i have a space.kcl").display().to_string())
);
assert_eq!(
state.project.default_file,
tmp_project_dir.join("i have a space.kcl").display().to_string()
);
std::fs::remove_dir_all(tmp_project_dir).unwrap();
}
#[tokio::test]
async fn test_project_state_new_from_path_explicit_open_file_with_space_kcl_url_encoded() {
let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
let tmp_project_dir = std::env::temp_dir().join(&name);
std::fs::create_dir_all(&tmp_project_dir).unwrap();
std::fs::write(tmp_project_dir.join("i have a space.kcl"), vec![]).unwrap();
let state = super::ProjectState::new_from_path(tmp_project_dir.join("i%20have%20a%20space.kcl"))
.await
.unwrap();
assert_eq!(state.project.file.name, name);
assert_eq!(state.project.file.path, tmp_project_dir.display().to_string());
assert_eq!(
state.current_file,
Some(tmp_project_dir.join("i have a space.kcl").display().to_string())
);
assert_eq!(
state.project.default_file,
tmp_project_dir.join("i have a space.kcl").display().to_string()
);
std::fs::remove_dir_all(tmp_project_dir).unwrap();
}
}

View File

@ -1,6 +1,5 @@
//! Types for kcl project and modeling-app settings.
pub mod file;
pub mod project;
use anyhow::Result;
@ -61,120 +60,6 @@ impl Configuration {
Ok(settings)
}
#[cfg(not(target_arch = "wasm32"))]
/// Initialize the project directory.
pub async fn ensure_project_directory_exists(&self) -> Result<std::path::PathBuf> {
let project_dir = &self.settings.project.directory;
// Check if the directory exists.
if !project_dir.exists() {
// Create the directory.
tokio::fs::create_dir_all(project_dir).await?;
}
Ok(project_dir.clone())
}
#[cfg(not(target_arch = "wasm32"))]
/// Create a new project directory.
pub async fn create_new_project_directory(
&self,
project_name: &str,
initial_code: Option<&str>,
) -> Result<crate::settings::types::file::Project> {
let main_dir = &self.ensure_project_directory_exists().await?;
if project_name.is_empty() {
return Err(anyhow::anyhow!("Project name cannot be empty."));
}
// Create the project directory.
let project_dir = main_dir.join(project_name);
// Create the directory.
if !project_dir.exists() {
tokio::fs::create_dir_all(&project_dir).await?;
}
// Write the initial project file.
let project_file = project_dir.join(DEFAULT_PROJECT_KCL_FILE);
tokio::fs::write(&project_file, initial_code.unwrap_or_default()).await?;
Ok(crate::settings::types::file::Project {
file: crate::settings::types::file::FileEntry {
path: project_dir.to_string_lossy().to_string(),
name: project_name.to_string(),
// We don't need to recursively get all files in the project directory.
// Because we just created it and it's empty.
children: None,
},
default_file: project_file.to_string_lossy().to_string(),
metadata: Some(tokio::fs::metadata(&project_dir).await?.into()),
kcl_file_count: 1,
directory_count: 0,
})
}
#[cfg(not(target_arch = "wasm32"))]
/// List all the projects for the configuration.
pub async fn list_projects(&self) -> Result<Vec<crate::settings::types::file::Project>> {
// Get all the top level directories in the project directory.
let main_dir = &self.ensure_project_directory_exists().await?;
let mut projects = vec![];
let mut entries = tokio::fs::read_dir(main_dir).await?;
while let Some(e) = entries.next_entry().await? {
if !e.file_type().await?.is_dir() || e.file_name().to_string_lossy().starts_with('.') {
// We don't care it's not a directory
// or it's a hidden directory.
continue;
}
// 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)
}
#[cfg(not(target_arch = "wasm32"))]
/// Get information about a project.
pub async fn get_project_info(&self, project_path: &str) -> Result<crate::settings::types::file::Project> {
// Check the directory.
let project_dir = std::path::Path::new(project_path);
if !project_dir.exists() {
return Err(anyhow::anyhow!("Project directory does not exist: {}", project_path));
}
// Make sure it is a directory.
if !project_dir.is_dir() {
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 {
file: walked.clone(),
metadata: Some(tokio::fs::metadata(&project_dir).await?.into()),
kcl_file_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.
project.populate_kcl_file_count()?;
//Populate the number of directories in the project.
project.populate_directory_count()?;
Ok(project)
}
}
/// High level settings.
@ -954,196 +839,4 @@ color = 1567.4"#;
.to_string()
.contains("color: Validation error: color"));
}
#[tokio::test]
async fn test_create_new_project_directory_no_initial_code() {
let mut settings = Configuration::default();
settings.settings.project.directory =
std::env::temp_dir().join(format!("test_project_{}", uuid::Uuid::new_v4()));
let project_name = format!("test_project_{}", uuid::Uuid::new_v4());
let project = settings
.create_new_project_directory(&project_name, None)
.await
.unwrap();
assert_eq!(project.file.name, project_name);
assert_eq!(
project.file.path,
settings
.settings
.project
.directory
.join(&project_name)
.to_string_lossy()
);
assert_eq!(project.kcl_file_count, 1);
assert_eq!(project.directory_count, 0);
assert_eq!(
project.default_file,
std::path::Path::new(&project.file.path)
.join(super::DEFAULT_PROJECT_KCL_FILE)
.to_string_lossy()
);
std::fs::remove_dir_all(&settings.settings.project.directory).unwrap();
}
#[tokio::test]
async fn test_create_new_project_directory_empty_name() {
let mut settings = Configuration::default();
settings.settings.project.directory =
std::env::temp_dir().join(format!("test_project_{}", uuid::Uuid::new_v4()));
let project_name = "";
let project = settings.create_new_project_directory(project_name, None).await;
assert!(project.is_err());
assert_eq!(project.unwrap_err().to_string(), "Project name cannot be empty.");
std::fs::remove_dir_all(&settings.settings.project.directory).unwrap();
}
#[tokio::test]
async fn test_create_new_project_directory_with_initial_code() {
let mut settings = Configuration::default();
settings.settings.project.directory =
std::env::temp_dir().join(format!("test_project_{}", uuid::Uuid::new_v4()));
let project_name = format!("test_project_{}", uuid::Uuid::new_v4());
let initial_code = "initial code";
let project = settings
.create_new_project_directory(&project_name, Some(initial_code))
.await
.unwrap();
assert_eq!(project.file.name, project_name);
assert_eq!(
project.file.path,
settings
.settings
.project
.directory
.join(&project_name)
.to_string_lossy()
);
assert_eq!(project.kcl_file_count, 1);
assert_eq!(project.directory_count, 0);
assert_eq!(
project.default_file,
std::path::Path::new(&project.file.path)
.join(super::DEFAULT_PROJECT_KCL_FILE)
.to_string_lossy()
);
assert_eq!(
tokio::fs::read_to_string(&project.default_file).await.unwrap(),
initial_code
);
std::fs::remove_dir_all(&settings.settings.project.directory).unwrap();
}
#[tokio::test]
async fn test_list_projects() {
let mut settings = Configuration::default();
settings.settings.project.directory =
std::env::temp_dir().join(format!("test_project_{}", uuid::Uuid::new_v4()));
let project_name = format!("test_project_{}", uuid::Uuid::new_v4());
let project = settings
.create_new_project_directory(&project_name, None)
.await
.unwrap();
let projects = settings.list_projects().await.unwrap();
assert_eq!(projects.len(), 1);
assert_eq!(projects[0].file.name, project_name);
assert_eq!(projects[0].file.path, project.file.path);
assert_eq!(projects[0].kcl_file_count, 1);
assert_eq!(projects[0].directory_count, 0);
assert_eq!(projects[0].default_file, project.default_file);
std::fs::remove_dir_all(&settings.settings.project.directory).unwrap();
}
#[tokio::test]
async fn test_list_projects_with_rando_files() {
let mut settings = Configuration::default();
settings.settings.project.directory =
std::env::temp_dir().join(format!("test_project_{}", uuid::Uuid::new_v4()));
let project_name = format!("test_project_{}", uuid::Uuid::new_v4());
let project = settings
.create_new_project_directory(&project_name, None)
.await
.unwrap();
// Create a random file in the root project directory.
let random_file = std::path::Path::new(&settings.settings.project.directory).join("random_file.txt");
tokio::fs::write(&random_file, "random file").await.unwrap();
let projects = settings.list_projects().await.unwrap();
assert_eq!(projects.len(), 1);
assert_eq!(projects[0].file.name, project_name);
assert_eq!(projects[0].file.path, project.file.path);
assert_eq!(projects[0].kcl_file_count, 1);
assert_eq!(projects[0].directory_count, 0);
assert_eq!(projects[0].default_file, project.default_file);
std::fs::remove_dir_all(&settings.settings.project.directory).unwrap();
}
#[tokio::test]
async fn test_list_projects_with_hidden_dir() {
let mut settings = Configuration::default();
settings.settings.project.directory =
std::env::temp_dir().join(format!("test_project_{}", uuid::Uuid::new_v4()));
let project_name = format!("test_project_{}", uuid::Uuid::new_v4());
let project = settings
.create_new_project_directory(&project_name, None)
.await
.unwrap();
// Create a hidden directory in the project directory.
let hidden_dir = std::path::Path::new(&settings.settings.project.directory).join(".git");
tokio::fs::create_dir_all(&hidden_dir).await.unwrap();
let projects = settings.list_projects().await.unwrap();
assert_eq!(projects.len(), 1);
assert_eq!(projects[0].file.name, project_name);
assert_eq!(projects[0].file.path, project.file.path);
assert_eq!(projects[0].kcl_file_count, 1);
assert_eq!(projects[0].directory_count, 0);
assert_eq!(projects[0].default_file, project.default_file);
std::fs::remove_dir_all(&settings.settings.project.directory).unwrap();
}
#[tokio::test]
async fn test_list_projects_with_dir_not_containing_kcl_file() {
let mut settings = Configuration::default();
settings.settings.project.directory =
std::env::temp_dir().join(format!("test_project_{}", uuid::Uuid::new_v4()));
let project_name = format!("test_project_{}", uuid::Uuid::new_v4());
let project = settings
.create_new_project_directory(&project_name, None)
.await
.unwrap();
// Create a directory in the project directory that doesn't contain a KCL file.
let random_dir = std::path::Path::new(&settings.settings.project.directory).join("random_dir");
tokio::fs::create_dir_all(&random_dir).await.unwrap();
let projects = settings.list_projects().await.unwrap();
assert_eq!(projects.len(), 1);
assert_eq!(projects[0].file.name, project_name);
assert_eq!(projects[0].file.path, project.file.path);
assert_eq!(projects[0].kcl_file_count, 1);
assert_eq!(projects[0].directory_count, 0);
assert_eq!(projects[0].default_file, project.default_file);
std::fs::remove_dir_all(&settings.settings.project.directory).unwrap();
}
}

View File

@ -1,93 +0,0 @@
//! Utility functions for settings.
use std::path::Path;
use anyhow::Result;
use clap::ValueEnum;
use crate::settings::types::file::FileEntry;
lazy_static::lazy_static! {
pub static ref IMPORT_FILE_EXTENSIONS: Vec<String> = {
let mut import_file_extensions = vec!["stp".to_string(), "glb".to_string(), "fbxb".to_string()];
let named_extensions = kittycad::types::FileImportFormat::value_variants()
.iter()
.map(|x| format!("{}", x))
.collect::<Vec<String>>();
// Add all the default import formats.
import_file_extensions.extend_from_slice(&named_extensions);
import_file_extensions
};
pub static ref RELEVANT_EXTENSIONS: Vec<String> = {
let mut relevant_extensions = IMPORT_FILE_EXTENSIONS.clone();
relevant_extensions.push("kcl".to_string());
relevant_extensions
};
}
/// Walk a directory recursively and return a list of all files.
#[async_recursion::async_recursion]
pub async fn walk_dir<P>(dir: P) -> Result<FileEntry>
where
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 {
name: dir
.as_ref()
.file_name()
.ok_or_else(|| anyhow::anyhow!("No file name"))?
.to_string_lossy()
.to_string(),
path: dir.as_ref().display().to_string(),
children: None,
};
let mut children = vec![];
let mut entries = tokio::fs::read_dir(&dir.as_ref()).await?;
while let Some(e) = entries.next_entry().await? {
// ignore hidden files and directories (starting with a dot)
if e.file_name().to_string_lossy().starts_with('.') {
continue;
}
if e.file_type().await?.is_dir() {
children.push(walk_dir(e.path()).await?);
} else {
if !is_relevant_file(e.path())? {
continue;
}
children.push(FileEntry {
name: e.file_name().to_string_lossy().to_string(),
path: e.path().display().to_string(),
children: None,
});
}
}
// We don't set this to none if there are no children, because it's a directory.
entry.children = Some(children);
Ok(entry)
}
/// Check if a file is relevant for the application.
fn is_relevant_file<P: AsRef<Path>>(path: P) -> Result<bool> {
if let Some(ext) = path.as_ref().extension() {
Ok(RELEVANT_EXTENSIONS.contains(&ext.to_string_lossy().to_string()))
} else {
Ok(false)
}
}

View File

@ -1199,6 +1199,11 @@
"@babel/helper-validator-identifier" "^7.24.7"
to-fast-properties "^2.0.0"
"@bitdisaster/exe-icon-extractor@^1.0.10":
version "1.0.10"
resolved "https://registry.yarnpkg.com/@bitdisaster/exe-icon-extractor/-/exe-icon-extractor-1.0.10.tgz#3f5107864254c351db1db5c5922452d9d4154e8f"
integrity sha512-iTZ8cVGZ5dglNRyFdSj8U60mHIrC8XNIuOHN/NkM5/dQP4nsmpyqeQTAADLLQgoFCNJD+DiwQCv8dR2cCeWP4g==
"@codemirror/autocomplete@^6.0.0", "@codemirror/autocomplete@^6.17.0":
version "6.17.0"
resolved "https://registry.yarnpkg.com/@codemirror/autocomplete/-/autocomplete-6.17.0.tgz#24ff5fc37fd91f6439df6f4ff9c8e910cde1b053"
@ -1443,6 +1448,18 @@
optionalDependencies:
electron-winstaller "^5.3.0"
"@electron-forge/maker-wix@^7.4.0":
version "7.4.0"
resolved "https://registry.yarnpkg.com/@electron-forge/maker-wix/-/maker-wix-7.4.0.tgz#4c271a70506247f07d704634ef6cbe0fe6060bdc"
integrity sha512-+a5zNh/e8/aguDT7Ya+hEsKkkV7VSSaaB45RaA4ahI91bx/mRAWEhGQjnqakGkSAZkRzM6n37Tedx3wz0/2H4A==
dependencies:
"@electron-forge/maker-base" "7.4.0"
"@electron-forge/shared-types" "7.4.0"
chalk "^4.0.0"
electron-wix-msi "^5.1.3"
log-symbols "^4.0.0"
parse-author "^2.0.0"
"@electron-forge/maker-zip@^7.4.0":
version "7.4.0"
resolved "https://registry.yarnpkg.com/@electron-forge/maker-zip/-/maker-zip-7.4.0.tgz#e82ab6174344c43eb9a30b2fb5e2c2e32de2113d"
@ -1667,7 +1684,7 @@
semver "^7.1.3"
yargs-parser "^21.1.1"
"@electron/rebuild@^3.2.10":
"@electron/rebuild@^3.2.10", "@electron/rebuild@^3.6.0":
version "3.6.0"
resolved "https://registry.yarnpkg.com/@electron/rebuild/-/rebuild-3.6.0.tgz#60211375a5f8541a71eb07dd2f97354ad0b2b96f"
integrity sha512-zF4x3QupRU3uNGaP5X1wjpmcjfw1H87kyqZ00Tc3HvriV+4gmOGuvQjGNkrJuXdsApssdNyVwLsy+TaeTGGcVw==
@ -2064,7 +2081,7 @@
dependencies:
"@lezer/common" "^1.0.0"
"@malept/cross-spawn-promise@^1.0.0":
"@malept/cross-spawn-promise@^1.0.0", "@malept/cross-spawn-promise@^1.1.0":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz#504af200af6b98e198bce768bc1730c6936ae01d"
integrity sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ==
@ -2525,6 +2542,11 @@
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-5.1.2.tgz#07508b45797cb81ec3f273011b054cd0755eddca"
integrity sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==
"@types/minimist@^1.2.5":
version "1.2.5"
resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.5.tgz#ec10755e871497bcd83efe927e43ec46e8c0747e"
integrity sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==
"@types/mocha@^10.0.6":
version "10.0.7"
resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-10.0.7.tgz#4c620090f28ca7f905a94b706f74dc5b57b44f2f"
@ -3871,6 +3893,15 @@ cross-fetch@^3.1.5:
dependencies:
node-fetch "^2.6.12"
cross-spawn-windows-exe@^1.1.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/cross-spawn-windows-exe/-/cross-spawn-windows-exe-1.2.0.tgz#46253b0f497676e766faf4a7061004618b5ac5ec"
integrity sha512-mkLtJJcYbDCxEG7Js6eUnUNndWjyUZwJ3H7bErmmtOYU/Zb99DyUkpamuIZE0b3bhmJyZ7D90uS6f+CGxRRjOw==
dependencies:
"@malept/cross-spawn-promise" "^1.1.0"
is-wsl "^2.2.0"
which "^2.0.2"
cross-spawn@^6.0.0, cross-spawn@^6.0.5:
version "6.0.5"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
@ -4268,6 +4299,22 @@ electron-winstaller@^5.3.0:
optionalDependencies:
"@electron/windows-sign" "^1.1.2"
electron-wix-msi@^5.1.3:
version "5.1.3"
resolved "https://registry.yarnpkg.com/electron-wix-msi/-/electron-wix-msi-5.1.3.tgz#ab85dc1145a7ce7ae7724ed3ca3f92c447988c9a"
integrity sha512-EYj1cm1nZoVHmIIx3o0aKt784lxdEpJnXbEnyypklUCnglqSb7ni+1xi1Vp/gtrGS/mzIxnWBT+x5fIfuDjhvA==
dependencies:
"@electron/windows-sign" "^1.1.2"
debug "^4.3.4"
fs-extra "^10.1.0"
klaw "^4.1.0"
lodash "^4.17.21"
rcedit "^4.0.1"
rcinfo "^0.1.3"
semver "^7.6.0"
optionalDependencies:
"@bitdisaster/exe-icon-extractor" "^1.0.10"
electron@*, electron@^32.0.1:
version "32.0.1"
resolved "https://registry.yarnpkg.com/electron/-/electron-32.0.1.tgz#8bfc186b81a645c6b3b12f91e71db4231fd0c934"
@ -5857,6 +5904,11 @@ is-date-object@^1.0.1, is-date-object@^1.0.5:
dependencies:
has-tostringtag "^1.0.0"
is-docker@^2.0.0:
version "2.2.1"
resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa"
integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==
is-extglob@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
@ -6011,6 +6063,13 @@ is-windows@^1.0.1:
resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==
is-wsl@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271"
integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==
dependencies:
is-docker "^2.0.0"
isarray@^2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723"
@ -6254,6 +6313,11 @@ keyv@^4.0.0, keyv@^4.5.3:
dependencies:
json-buffer "3.0.1"
klaw@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/klaw/-/klaw-4.1.0.tgz#5df608067d8cb62bbfb24374f8e5d956323338f3"
integrity sha512-1zGZ9MF9H22UnkpVeuaGKOjfA2t6WrfdrJmGjy16ykcjnKQDmHVX+KI477rpbGevz/5FD4MC3xf1oxylBgcaQw==
language-subtag-registry@^0.3.20:
version "0.3.23"
resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz#23529e04d9e3b74679d70142df3fd2eb6ec572e7"
@ -6533,9 +6597,9 @@ methods@~1.1.2:
integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==
micromatch@^4.0.4, micromatch@^4.0.5:
version "4.0.7"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.7.tgz#33e8190d9fe474a9895525f5618eee136d46c2e5"
integrity sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==
version "4.0.8"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202"
integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==
dependencies:
braces "^3.0.3"
picomatch "^2.3.1"
@ -7540,6 +7604,18 @@ raw-body@2.5.2:
iconv-lite "0.4.24"
unpipe "1.0.0"
rcedit@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/rcedit/-/rcedit-4.0.1.tgz#892ac47a19204a380f49e00ea38ce070443343c2"
integrity sha512-bZdaQi34krFWhrDn+O53ccBDw0MkAT2Vhu75SqhtvhQu4OPyFM4RoVheyYiVQYdjhUi6EJMVWQ0tR6bCIYVkUg==
dependencies:
cross-spawn-windows-exe "^1.1.0"
rcinfo@^0.1.3:
version "0.1.3"
resolved "https://registry.yarnpkg.com/rcinfo/-/rcinfo-0.1.3.tgz#ac36832d1f1e5970c6379e571480ea5826511fc6"
integrity sha512-c2XV2aYgY7x3BscO+/B/nCTtMvnclZ8w5D7R6zgK4sGOQnE0MjlXhOPynno7yp6Iw1RPNSXBwXwB1svZVRfcSw==
re-resizable@^6.9.11:
version "6.9.17"
resolved "https://registry.yarnpkg.com/re-resizable/-/re-resizable-6.9.17.tgz#78e4349934ff24a8fcb4b6b5a43ff9ed5f319d2a"
@ -8039,7 +8115,7 @@ semver@^6.2.0, semver@^6.3.1:
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
semver@^7.1.1, semver@^7.1.3, semver@^7.2.1, semver@^7.3.2, semver@^7.3.5, semver@^7.3.7:
semver@^7.1.1, semver@^7.1.3, semver@^7.2.1, semver@^7.3.2, semver@^7.3.5, semver@^7.3.7, semver@^7.6.0:
version "7.6.3"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143"
integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==