Compare commits
37 Commits
kcl-0.2.7
...
kurt-test-
Author | SHA1 | Date | |
---|---|---|---|
ab5b83bbce | |||
ed339a6b9a | |||
1d19fc6b7e | |||
5b5355376f | |||
5c90f72c91 | |||
026a8d19cb | |||
6dd0981709 | |||
b231a26115 | |||
3f47486fb5 | |||
57e97d16d0 | |||
dbdc7e5c8b | |||
f6bb10170d | |||
972dca8743 | |||
e9e933eecd | |||
2b1315423f | |||
bd4c24bc04 | |||
50cc88977c | |||
bea9a1c3ec | |||
f43411fdb4 | |||
c2e9d18f92 | |||
199722c505 | |||
f9699d174c | |||
590a6479e0 | |||
fbf0d3d953 | |||
3dd66bc8d2 | |||
a928b8fbd0 | |||
d2349bec2b | |||
6e10f75ff6 | |||
03e289af20 | |||
efc140abbf | |||
4dfad19b7e | |||
e56c634b35 | |||
00292abc98 | |||
483d6903d6 | |||
3780996374 | |||
2fde71228a | |||
5cd8ab3812 |
@ -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
|
||||
|
2
.github/workflows/cargo-check.yml
vendored
@ -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
|
||||
|
5
.github/workflows/cargo-test.yml
vendored
@ -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
|
||||
|
4
.github/workflows/playwright.yml
vendored
@ -423,14 +423,14 @@ jobs:
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ !cancelled() && (success() || failure()) }}
|
||||
with:
|
||||
name: test-results-electron-${{ github.sha }}
|
||||
name: test-results-electron-${{ matrix.os }}-${{ github.sha }}
|
||||
path: test-results/
|
||||
retention-days: 30
|
||||
overwrite: true
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ !cancelled() && (success() || failure()) }}
|
||||
with:
|
||||
name: playwright-report-electron-${{ github.sha }}
|
||||
name: playwright-report-electron-${{ matrix.os }}-${{ github.sha }}
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
||||
overwrite: true
|
||||
|
5
.gitignore
vendored
@ -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
@ -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>
|
31
README.md
@ -110,7 +110,6 @@ Which commands from setup are one off vs need to be run every time?
|
||||
The following will need to be run when checking out a new commit and guarantees the build is not stale:
|
||||
```bash
|
||||
yarn install
|
||||
yarn wasm-prep
|
||||
yarn build:wasm-dev # or yarn build:wasm for slower but more production-like build
|
||||
yarn start # or yarn build:local && yarn serve for slower but more production-like build
|
||||
```
|
||||
@ -189,12 +188,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 +212,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
|
||||
|
||||
```
|
||||
|
835
docs/kcl/hollow.md
Normal file
@ -44,6 +44,7 @@ layout: manual
|
||||
* [`getPreviousAdjacentEdge`](kcl/getPreviousAdjacentEdge)
|
||||
* [`helix`](kcl/helix)
|
||||
* [`hole`](kcl/hole)
|
||||
* [`hollow`](kcl/hollow)
|
||||
* [`import`](kcl/import)
|
||||
* [`inch`](kcl/inch)
|
||||
* [`int`](kcl/int)
|
||||
|
5173
docs/kcl/std.json
@ -6,7 +6,39 @@ test.afterEach(async ({ page }, testInfo) => {
|
||||
await tearDown(page, testInfo)
|
||||
})
|
||||
|
||||
test.describe('Electron user sidebar menu tests', () => {
|
||||
test.describe('Electron app header tests', () => {
|
||||
test(
|
||||
'Open Command Palette button has correct shortcut',
|
||||
{ tag: '@electron' },
|
||||
async ({ browserName }, testInfo) => {
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async () => {},
|
||||
})
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
|
||||
// No space before the shortcut since it checks textContent.
|
||||
let text
|
||||
switch (process.platform) {
|
||||
case 'darwin':
|
||||
text = 'Commands⌘K'
|
||||
break
|
||||
case 'win32':
|
||||
text = 'CommandsCtrl+K'
|
||||
break
|
||||
default: // 'linux' etc.
|
||||
text = 'CommandsCtrl+K'
|
||||
break
|
||||
}
|
||||
const commandsButton = page.getByRole('button', { name: 'Commands' })
|
||||
await expect(commandsButton).toBeVisible()
|
||||
await expect(commandsButton).toHaveText(text)
|
||||
|
||||
await electronApp.close()
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
'User settings has correct shortcut',
|
||||
{ tag: '@electron' },
|
@ -1,6 +1,13 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
import { getUtils, setup, setupElectron, tearDown } from './test-utils'
|
||||
import {
|
||||
getUtils,
|
||||
setup,
|
||||
setupElectron,
|
||||
tearDown,
|
||||
executorInputPath,
|
||||
} from './test-utils'
|
||||
import { join } from 'path'
|
||||
import { bracket } from 'lib/exampleKcl'
|
||||
import { TEST_CODE_LONG_WITH_ERROR_OUT_OF_VIEW } from './storageStates'
|
||||
import fsp from 'fs/promises'
|
||||
@ -223,26 +230,24 @@ test(
|
||||
'Opening multiple panes persists when switching projects',
|
||||
{ tag: '@electron' },
|
||||
async ({ browserName }, testInfo) => {
|
||||
test.skip(
|
||||
process.platform === 'win32',
|
||||
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
|
||||
)
|
||||
// Setup multiple projects.
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async (dir) => {
|
||||
const routerTemplateDir = join(dir, 'router-template-slate')
|
||||
const bracketDir = join(dir, 'bracket')
|
||||
await Promise.all([
|
||||
fsp.mkdir(`${dir}/router-template-slate`, { recursive: true }),
|
||||
fsp.mkdir(`${dir}/bracket`, { recursive: true }),
|
||||
fsp.mkdir(routerTemplateDir, { recursive: true }),
|
||||
fsp.mkdir(bracketDir, { recursive: true }),
|
||||
])
|
||||
await Promise.all([
|
||||
fsp.copyFile(
|
||||
'src/wasm-lib/tests/executor/inputs/router-template-slate.kcl',
|
||||
`${dir}/router-template-slate/main.kcl`
|
||||
executorInputPath('router-template-slate.kcl'),
|
||||
join(routerTemplateDir, 'main.kcl')
|
||||
),
|
||||
fsp.copyFile(
|
||||
'src/wasm-lib/tests/executor/inputs/focusrite_scarlett_mounting_braket.kcl',
|
||||
`${dir}/bracket/main.kcl`
|
||||
executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
|
||||
join(bracketDir, 'main.kcl')
|
||||
),
|
||||
])
|
||||
},
|
||||
|
@ -1,5 +1,11 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
import { getUtils, setupElectron, tearDown } from './test-utils'
|
||||
import { join } from 'path'
|
||||
import {
|
||||
getUtils,
|
||||
setupElectron,
|
||||
tearDown,
|
||||
executorInputPath,
|
||||
} from './test-utils'
|
||||
import fsp from 'fs/promises'
|
||||
|
||||
test.afterEach(async ({ page }, testInfo) => {
|
||||
@ -10,22 +16,19 @@ test(
|
||||
'export works on the first try',
|
||||
{ tag: '@electron' },
|
||||
async ({ browserName }, testInfo) => {
|
||||
test.skip(
|
||||
process.platform === 'win32',
|
||||
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
|
||||
)
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async (dir) => {
|
||||
await Promise.all([fsp.mkdir(`${dir}/bracket`, { recursive: true })])
|
||||
const bracketDir = join(dir, 'bracket')
|
||||
await Promise.all([fsp.mkdir(bracketDir, { recursive: true })])
|
||||
await Promise.all([
|
||||
fsp.copyFile(
|
||||
'src/wasm-lib/tests/executor/inputs/router-template-slate.kcl',
|
||||
`${dir}/bracket/other.kcl`
|
||||
executorInputPath('router-template-slate.kcl'),
|
||||
join(bracketDir, 'other.kcl')
|
||||
),
|
||||
fsp.copyFile(
|
||||
'src/wasm-lib/tests/executor/inputs/focusrite_scarlett_mounting_braket.kcl',
|
||||
`${dir}/bracket/main.kcl`
|
||||
executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
|
||||
join(bracketDir, 'main.kcl')
|
||||
),
|
||||
])
|
||||
},
|
||||
|
@ -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], %)
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
import { setupElectron, tearDown } from './test-utils'
|
||||
import { setupElectron, tearDown, executorInputPath } from './test-utils'
|
||||
import { join } from 'path'
|
||||
import fsp from 'fs/promises'
|
||||
|
||||
test.afterEach(async ({ page }, testInfo) => {
|
||||
@ -10,17 +11,14 @@ test(
|
||||
'When machine-api server not found butt is disabled and shows the reason',
|
||||
{ tag: '@electron' },
|
||||
async ({ browserName }, testInfo) => {
|
||||
test.skip(
|
||||
process.platform === 'win32',
|
||||
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
|
||||
)
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async (dir) => {
|
||||
await fsp.mkdir(`${dir}/bracket`, { recursive: true })
|
||||
const bracketDir = join(dir, 'bracket')
|
||||
await fsp.mkdir(bracketDir, { recursive: true })
|
||||
await fsp.copyFile(
|
||||
'src/wasm-lib/tests/executor/inputs/focusrite_scarlett_mounting_braket.kcl',
|
||||
`${dir}/bracket/main.kcl`
|
||||
executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
|
||||
join(bracketDir, 'main.kcl')
|
||||
)
|
||||
},
|
||||
})
|
||||
@ -58,17 +56,14 @@ test(
|
||||
'When machine-api server not found home screen & project status shows the reason',
|
||||
{ tag: '@electron' },
|
||||
async ({ browserName }, testInfo) => {
|
||||
test.skip(
|
||||
process.platform === 'win32',
|
||||
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
|
||||
)
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async (dir) => {
|
||||
await fsp.mkdir(`${dir}/bracket`, { recursive: true })
|
||||
const bracketDir = join(dir, 'bracket')
|
||||
await fsp.mkdir(bracketDir, { recursive: true })
|
||||
await fsp.copyFile(
|
||||
'src/wasm-lib/tests/executor/inputs/focusrite_scarlett_mounting_braket.kcl',
|
||||
`${dir}/bracket/main.kcl`
|
||||
executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
|
||||
join(bracketDir, 'main.kcl')
|
||||
)
|
||||
},
|
||||
})
|
||||
|
@ -1,6 +1,13 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
import { join } from 'path'
|
||||
import fsp from 'fs/promises'
|
||||
import { getUtils, setup, setupElectron, tearDown } from './test-utils'
|
||||
import {
|
||||
getUtils,
|
||||
setup,
|
||||
setupElectron,
|
||||
tearDown,
|
||||
executorInputPath,
|
||||
} from './test-utils'
|
||||
import { bracket } from 'lib/exampleKcl'
|
||||
import { onboardingPaths } from 'routes/Onboarding/paths'
|
||||
import {
|
||||
@ -347,17 +354,14 @@ test(
|
||||
'Restarting onboarding on desktop takes one attempt',
|
||||
{ tag: '@electron' },
|
||||
async ({ browser: _ }, testInfo) => {
|
||||
test.skip(
|
||||
process.platform === 'win32',
|
||||
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
|
||||
)
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async (dir) => {
|
||||
await fsp.mkdir(`${dir}/router-template-slate`, { recursive: true })
|
||||
const routerTemplateDir = join(dir, 'router-template-slate')
|
||||
await fsp.mkdir(routerTemplateDir, { recursive: true })
|
||||
await fsp.copyFile(
|
||||
'src/wasm-lib/tests/executor/inputs/router-template-slate.kcl',
|
||||
`${dir}/router-template-slate/main.kcl`
|
||||
executorInputPath('router-template-slate.kcl'),
|
||||
join(routerTemplateDir, 'main.kcl')
|
||||
)
|
||||
},
|
||||
})
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { test, expect, Page } from '@playwright/test'
|
||||
import {
|
||||
doExport,
|
||||
executorInputPath,
|
||||
getUtils,
|
||||
isOutOfViewInScrollContainer,
|
||||
Paths,
|
||||
@ -49,17 +50,11 @@ test(
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async (dir) => {
|
||||
await fsp.mkdir(join(dir, 'bracket'), { recursive: true })
|
||||
const bracketDir = join(dir, 'bracket')
|
||||
await fsp.mkdir(bracketDir, { recursive: true })
|
||||
await fsp.copyFile(
|
||||
join(
|
||||
'src',
|
||||
'wasm-lib',
|
||||
'tests',
|
||||
'executor',
|
||||
'inputs',
|
||||
'focusrite_scarlett_mounting_braket.kcl'
|
||||
),
|
||||
join(dir, 'bracket', 'main.kcl')
|
||||
executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
|
||||
join(bracketDir, 'main.kcl')
|
||||
)
|
||||
},
|
||||
})
|
||||
@ -99,14 +94,7 @@ test(
|
||||
folderSetupFn: async (dir) => {
|
||||
await fsp.mkdir(join(dir, 'broken-code'), { recursive: true })
|
||||
await fsp.copyFile(
|
||||
join(
|
||||
'src',
|
||||
'wasm-lib',
|
||||
'tests',
|
||||
'executor',
|
||||
'inputs',
|
||||
'broken-code-test.kcl'
|
||||
),
|
||||
executorInputPath('broken-code-test.kcl'),
|
||||
join(dir, 'broken-code', 'main.kcl')
|
||||
)
|
||||
},
|
||||
@ -143,17 +131,14 @@ test.describe('Can export from electron app', () => {
|
||||
`Can export using ${method}`,
|
||||
{ tag: '@electron' },
|
||||
async ({ browserName }, testInfo) => {
|
||||
test.skip(
|
||||
process.platform === 'win32',
|
||||
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
|
||||
)
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async (dir) => {
|
||||
await fsp.mkdir(`${dir}/bracket`, { recursive: true })
|
||||
const bracketDir = join(dir, 'bracket')
|
||||
await fsp.mkdir(bracketDir, { recursive: true })
|
||||
await fsp.copyFile(
|
||||
'src/wasm-lib/tests/executor/inputs/focusrite_scarlett_mounting_braket.kcl',
|
||||
`${dir}/bracket/main.kcl`
|
||||
executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
|
||||
join(bracketDir, 'main.kcl')
|
||||
)
|
||||
},
|
||||
})
|
||||
@ -469,6 +454,7 @@ test(
|
||||
await electronApp.close()
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
'File in the file pane should open with a single click',
|
||||
{ tag: '@electron' },
|
||||
@ -521,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' },
|
||||
@ -605,6 +654,48 @@ test(
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
'Can load a file with CRLF line endings',
|
||||
{ tag: '@electron' },
|
||||
async ({ browserName }, testInfo) => {
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async (dir) => {
|
||||
const routerTemplateDir = join(dir, 'router-template-slate')
|
||||
await fsp.mkdir(routerTemplateDir, { recursive: true })
|
||||
|
||||
const file = await fsp.readFile(
|
||||
executorInputPath('router-template-slate.kcl'),
|
||||
'utf-8'
|
||||
)
|
||||
// Replace both \r optionally so we don't end up with \r\r\n
|
||||
const fileWithCRLF = file.replace(/\r?\n/g, '\r\n')
|
||||
await fsp.writeFile(
|
||||
join(routerTemplateDir, 'main.kcl'),
|
||||
fileWithCRLF,
|
||||
'utf-8'
|
||||
)
|
||||
},
|
||||
})
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
|
||||
page.on('console', console.log)
|
||||
|
||||
await page.getByText('router-template-slate').click()
|
||||
await expect(page.getByTestId('loading')).toBeAttached()
|
||||
await expect(page.getByTestId('loading')).not.toBeAttached({
|
||||
timeout: 20_000,
|
||||
})
|
||||
|
||||
await expect(u.codeLocator).toContainText('routerDiameter')
|
||||
await expect(u.codeLocator).toContainText('templateGap')
|
||||
await expect(u.codeLocator).toContainText('minClampingDistance')
|
||||
|
||||
await electronApp.close()
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
'Can sort projects on home page',
|
||||
{ tag: '@electron' },
|
||||
@ -1032,10 +1123,6 @@ test(
|
||||
'Search projects on desktop home',
|
||||
{ tag: '@electron' },
|
||||
async ({ browserName: _ }, testInfo) => {
|
||||
test.skip(
|
||||
process.platform === 'win32',
|
||||
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
|
||||
)
|
||||
const projectData = [
|
||||
['basic bracket', 'focusrite_scarlett_mounting_braket.kcl'],
|
||||
['basic-cube', 'basic_fillet_cube_end.kcl'],
|
||||
@ -1050,7 +1137,7 @@ test(
|
||||
for (const [name, file] of projectData) {
|
||||
await fsp.mkdir(join(dir, name), { recursive: true })
|
||||
await fsp.copyFile(
|
||||
join('src', 'wasm-lib', 'tests', 'executor', 'inputs', file),
|
||||
executorInputPath(file),
|
||||
join(dir, name, `main.kcl`)
|
||||
)
|
||||
}
|
||||
@ -1097,14 +1184,11 @@ test(
|
||||
'file pane is scrollable when there are many files',
|
||||
{ tag: '@electron' },
|
||||
async ({ browserName }, testInfo) => {
|
||||
test.skip(
|
||||
process.platform === 'win32',
|
||||
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
|
||||
)
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async (dir) => {
|
||||
await fsp.mkdir(`${dir}/testProject`, { recursive: true })
|
||||
const testDir = join(dir, 'testProject')
|
||||
await fsp.mkdir(testDir, { recursive: true })
|
||||
const fileNames = [
|
||||
'angled_line.kcl',
|
||||
'basic_fillet_cube_close_opposite.kcl',
|
||||
@ -1168,8 +1252,8 @@ test(
|
||||
]
|
||||
for (const fileName of fileNames) {
|
||||
await fsp.copyFile(
|
||||
`src/wasm-lib/tests/executor/inputs/${fileName}`,
|
||||
`${dir}/testProject/${fileName}`
|
||||
executorInputPath(fileName),
|
||||
join(testDir, fileName)
|
||||
)
|
||||
}
|
||||
},
|
||||
@ -1210,19 +1294,16 @@ test(
|
||||
'select all in code editor does not actually select all, just what is visible (regression)',
|
||||
{ tag: '@electron' },
|
||||
async ({ browserName }, testInfo) => {
|
||||
test.skip(
|
||||
process.platform === 'win32',
|
||||
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
|
||||
)
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async (dir) => {
|
||||
// src/wasm-lib/tests/executor/inputs/mike_stress_test.kcl
|
||||
const name = 'mike_stress_test'
|
||||
await fsp.mkdir(`${dir}/${name}`, { recursive: true })
|
||||
const testDir = join(dir, name)
|
||||
await fsp.mkdir(testDir, { recursive: true })
|
||||
await fsp.copyFile(
|
||||
`src/wasm-lib/tests/executor/inputs/${name}.kcl`,
|
||||
`${dir}/${name}/main.kcl`
|
||||
executorInputPath(`${name}.kcl`),
|
||||
join(testDir, 'main.kcl')
|
||||
)
|
||||
},
|
||||
})
|
||||
@ -1320,27 +1401,16 @@ test.describe('Renaming in the file tree', () => {
|
||||
'A file you have open',
|
||||
{ tag: '@electron' },
|
||||
async ({ browser: _ }, testInfo) => {
|
||||
test.skip(
|
||||
process.platform === 'win32',
|
||||
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
|
||||
)
|
||||
const { electronApp, page, dir } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async (dir) => {
|
||||
await fsp.mkdir(join(dir, 'Test Project'), { recursive: true })
|
||||
const exampleDir = join(
|
||||
'src',
|
||||
'wasm-lib',
|
||||
'tests',
|
||||
'executor',
|
||||
'inputs'
|
||||
)
|
||||
await fsp.copyFile(
|
||||
join(exampleDir, 'basic_fillet_cube_end.kcl'),
|
||||
executorInputPath('basic_fillet_cube_end.kcl'),
|
||||
join(dir, 'Test Project', 'main.kcl')
|
||||
)
|
||||
await fsp.copyFile(
|
||||
join(exampleDir, 'cylinder.kcl'),
|
||||
executorInputPath('cylinder.kcl'),
|
||||
join(dir, 'Test Project', 'fileToRename.kcl')
|
||||
)
|
||||
},
|
||||
@ -1425,27 +1495,16 @@ test.describe('Renaming in the file tree', () => {
|
||||
'A file you do not have open',
|
||||
{ tag: '@electron' },
|
||||
async ({ browser: _ }, testInfo) => {
|
||||
test.skip(
|
||||
process.platform === 'win32',
|
||||
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
|
||||
)
|
||||
const { electronApp, page, dir } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async (dir) => {
|
||||
await fsp.mkdir(join(dir, 'Test Project'), { recursive: true })
|
||||
const exampleDir = join(
|
||||
'src',
|
||||
'wasm-lib',
|
||||
'tests',
|
||||
'executor',
|
||||
'inputs'
|
||||
)
|
||||
await fsp.copyFile(
|
||||
join(exampleDir, 'basic_fillet_cube_end.kcl'),
|
||||
executorInputPath('basic_fillet_cube_end.kcl'),
|
||||
join(dir, 'Test Project', 'main.kcl')
|
||||
)
|
||||
await fsp.copyFile(
|
||||
join(exampleDir, 'cylinder.kcl'),
|
||||
executorInputPath('cylinder.kcl'),
|
||||
join(dir, 'Test Project', 'fileToRename.kcl')
|
||||
)
|
||||
},
|
||||
@ -1527,10 +1586,6 @@ test.describe('Renaming in the file tree', () => {
|
||||
`A folder you're not inside`,
|
||||
{ tag: '@electron' },
|
||||
async ({ browser: _ }, testInfo) => {
|
||||
test.skip(
|
||||
process.platform === 'win32',
|
||||
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
|
||||
)
|
||||
const { electronApp, page, dir } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async (dir) => {
|
||||
@ -1538,19 +1593,12 @@ test.describe('Renaming in the file tree', () => {
|
||||
await fsp.mkdir(join(dir, 'Test Project', 'folderToRename'), {
|
||||
recursive: true,
|
||||
})
|
||||
const exampleDir = join(
|
||||
'src',
|
||||
'wasm-lib',
|
||||
'tests',
|
||||
'executor',
|
||||
'inputs'
|
||||
)
|
||||
await fsp.copyFile(
|
||||
join(exampleDir, 'basic_fillet_cube_end.kcl'),
|
||||
executorInputPath('basic_fillet_cube_end.kcl'),
|
||||
join(dir, 'Test Project', 'main.kcl')
|
||||
)
|
||||
await fsp.copyFile(
|
||||
join(exampleDir, 'cylinder.kcl'),
|
||||
executorInputPath('cylinder.kcl'),
|
||||
join(dir, 'Test Project', 'folderToRename', 'someFileWithin.kcl')
|
||||
)
|
||||
},
|
||||
@ -1625,11 +1673,6 @@ test.describe('Renaming in the file tree', () => {
|
||||
`A folder you are inside`,
|
||||
{ tag: '@electron' },
|
||||
async ({ browser: _ }, testInfo) => {
|
||||
test.skip(
|
||||
process.platform === 'win32',
|
||||
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
|
||||
)
|
||||
const exampleDir = join('src', 'wasm-lib', 'tests', 'executor', 'inputs')
|
||||
const { electronApp, page, dir } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async (dir) => {
|
||||
@ -1638,11 +1681,11 @@ test.describe('Renaming in the file tree', () => {
|
||||
recursive: true,
|
||||
})
|
||||
await fsp.copyFile(
|
||||
join(exampleDir, 'basic_fillet_cube_end.kcl'),
|
||||
executorInputPath('basic_fillet_cube_end.kcl'),
|
||||
join(dir, 'Test Project', 'main.kcl')
|
||||
)
|
||||
await fsp.copyFile(
|
||||
join(exampleDir, 'cylinder.kcl'),
|
||||
executorInputPath('cylinder.kcl'),
|
||||
join(dir, 'Test Project', 'folderToRename', 'someFileWithin.kcl')
|
||||
)
|
||||
},
|
||||
@ -1738,21 +1781,18 @@ test.describe('Deleting files from the file pane', () => {
|
||||
`when main.kcl exists, navigate to main.kcl`,
|
||||
{ tag: '@electron' },
|
||||
async ({ browserName }, testInfo) => {
|
||||
test.skip(
|
||||
process.platform === 'win32',
|
||||
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
|
||||
)
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async (dir) => {
|
||||
await fsp.mkdir(`${dir}/testProject`, { recursive: true })
|
||||
const testDir = join(dir, 'testProject')
|
||||
await fsp.mkdir(testDir, { recursive: true })
|
||||
await fsp.copyFile(
|
||||
'src/wasm-lib/tests/executor/inputs/cylinder.kcl',
|
||||
`${dir}/testProject/main.kcl`
|
||||
executorInputPath('cylinder.kcl'),
|
||||
join(testDir, 'main.kcl')
|
||||
)
|
||||
await fsp.copyFile(
|
||||
'src/wasm-lib/tests/executor/inputs/basic_fillet_cube_end.kcl',
|
||||
`${dir}/testProject/fileToDelete.kcl`
|
||||
executorInputPath('basic_fillet_cube_end.kcl'),
|
||||
join(testDir, 'fileToDelete.kcl')
|
||||
)
|
||||
},
|
||||
})
|
||||
|
@ -1,6 +1,13 @@
|
||||
import { test, expect, Page } from '@playwright/test'
|
||||
import { join } from 'path'
|
||||
import * as fsp from 'fs/promises'
|
||||
import { getUtils, setup, setupElectron, tearDown } from './test-utils'
|
||||
import {
|
||||
getUtils,
|
||||
setup,
|
||||
setupElectron,
|
||||
tearDown,
|
||||
executorInputPath,
|
||||
} from './test-utils'
|
||||
import { TEST_CODE_TRIGGER_ENGINE_EXPORT_ERROR } from './storageStates'
|
||||
import { bracket } from 'lib/exampleKcl'
|
||||
|
||||
@ -425,17 +432,14 @@ const sketch001 = startSketchAt([-0, -0])
|
||||
`Network health indicator only appears in modeling view`,
|
||||
{ tag: '@electron' },
|
||||
async ({ browserName: _ }, testInfo) => {
|
||||
test.skip(
|
||||
process.platform === 'win32',
|
||||
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
|
||||
)
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async (dir) => {
|
||||
await fsp.mkdir(`${dir}/bracket`, { recursive: true })
|
||||
const bracketDir = join(dir, 'bracket')
|
||||
await fsp.mkdir(bracketDir, { recursive: true })
|
||||
await fsp.copyFile(
|
||||
'src/wasm-lib/tests/executor/inputs/focusrite_scarlett_mounting_braket.kcl',
|
||||
`${dir}/bracket/main.kcl`
|
||||
executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
|
||||
join(bracketDir, 'main.kcl')
|
||||
)
|
||||
},
|
||||
})
|
||||
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 71 KiB |
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
@ -912,3 +912,7 @@ export async function createProjectAndRenameIt({
|
||||
|
||||
await page.getByLabel('checkmark').last().click()
|
||||
}
|
||||
|
||||
export function executorInputPath(fileName: string): string {
|
||||
return join('src', 'wasm-lib', 'tests', 'executor', 'inputs', fileName)
|
||||
}
|
||||
|
@ -174,168 +174,166 @@ test.describe('Testing Camera Movement', () => {
|
||||
}, [0, -85, -85])
|
||||
})
|
||||
|
||||
// TODO fixme something is wrong with sketch here
|
||||
test.fixme(
|
||||
'Zoom should be consistent when exiting or entering sketches',
|
||||
async ({ page }) => {
|
||||
// start new sketch pan and zoom before exiting, when exiting the sketch should stay in the same place
|
||||
// than zoom and pan outside of sketch mode and enter again and it should not change from where it is
|
||||
// than again for sketching
|
||||
test('Zoom should be consistent when exiting or entering sketches', async ({
|
||||
page,
|
||||
}) => {
|
||||
// start new sketch pan and zoom before exiting, when exiting the sketch should stay in the same place
|
||||
// than zoom and pan outside of sketch mode and enter again and it should not change from where it is
|
||||
// than again for sketching
|
||||
|
||||
test.skip(process.platform !== 'darwin', 'Zoom should be consistent')
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
test.skip(process.platform !== 'darwin', 'Zoom should be consistent')
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
|
||||
await u.waitForAuthSkipAppStart()
|
||||
await u.openDebugPanel()
|
||||
await u.waitForAuthSkipAppStart()
|
||||
await u.openDebugPanel()
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Start Sketch' })
|
||||
).not.toBeDisabled()
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Start Sketch' })
|
||||
).toBeVisible()
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Start Sketch' })
|
||||
).not.toBeDisabled()
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Start Sketch' })
|
||||
).toBeVisible()
|
||||
|
||||
// click on "Start Sketch" button
|
||||
await u.clearCommandLogs()
|
||||
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
||||
// click on "Start Sketch" button
|
||||
await u.clearCommandLogs()
|
||||
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
// select a plane
|
||||
await page.mouse.click(700, 325)
|
||||
|
||||
let code = `const sketch001 = startSketchOn('XY')`
|
||||
await expect(u.codeLocator).toHaveText(code)
|
||||
await u.closeDebugPanel()
|
||||
|
||||
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
|
||||
|
||||
// move the camera slightly
|
||||
await page.keyboard.down('Shift')
|
||||
await page.mouse.move(700, 300)
|
||||
await page.mouse.down({ button: 'right' })
|
||||
await page.mouse.move(800, 200)
|
||||
await page.mouse.up({ button: 'right' })
|
||||
await page.keyboard.up('Shift')
|
||||
|
||||
let y = 350,
|
||||
x = 948
|
||||
|
||||
await u.canvasLocator.click({ position: { x: 783, y } })
|
||||
code += `\n |> startProfileAt([8.12, -12.98], %)`
|
||||
// await expect(u.codeLocator).toHaveText(code)
|
||||
await u.canvasLocator.click({ position: { x, y } })
|
||||
code += `\n |> line([11.18, 0], %)`
|
||||
// await expect(u.codeLocator).toHaveText(code)
|
||||
await u.canvasLocator.click({ position: { x, y: 275 } })
|
||||
code += `\n |> line([0, 6.99], %)`
|
||||
// await expect(u.codeLocator).toHaveText(code)
|
||||
|
||||
// click the line button
|
||||
await page.getByRole('button', { name: 'line Line', exact: true }).click()
|
||||
|
||||
const hoverOverNothing = async () => {
|
||||
// await u.canvasLocator.hover({position: {x: 700, y: 325}})
|
||||
await page.mouse.move(700, 325)
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
// select a plane
|
||||
await page.mouse.click(700, 325)
|
||||
|
||||
let code = `const sketch001 = startSketchOn('XY')`
|
||||
await expect(u.codeLocator).toHaveText(code)
|
||||
await u.closeDebugPanel()
|
||||
|
||||
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
|
||||
|
||||
// move the camera slightly
|
||||
await page.keyboard.down('Shift')
|
||||
await page.mouse.move(700, 300)
|
||||
await page.mouse.down({ button: 'right' })
|
||||
await page.mouse.move(800, 200)
|
||||
await page.mouse.up({ button: 'right' })
|
||||
await page.keyboard.up('Shift')
|
||||
|
||||
let y = 350,
|
||||
x = 948
|
||||
|
||||
await u.canvasLocator.click({ position: { x: 783, y } })
|
||||
code += `\n |> startProfileAt([8.12, -12.98], %)`
|
||||
// await expect(u.codeLocator).toHaveText(code)
|
||||
await u.canvasLocator.click({ position: { x, y } })
|
||||
code += `\n |> line([11.18, 0], %)`
|
||||
// await expect(u.codeLocator).toHaveText(code)
|
||||
await u.canvasLocator.click({ position: { x, y: 275 } })
|
||||
code += `\n |> line([0, 6.99], %)`
|
||||
// await expect(u.codeLocator).toHaveText(code)
|
||||
|
||||
// click the line button
|
||||
await page.getByRole('button', { name: 'line Line', exact: true }).click()
|
||||
|
||||
const hoverOverNothing = async () => {
|
||||
// await u.canvasLocator.hover({position: {x: 700, y: 325}})
|
||||
await page.mouse.move(700, 325)
|
||||
await page.waitForTimeout(100)
|
||||
await expect(page.getByTestId('hover-highlight')).not.toBeVisible({
|
||||
timeout: 10_000,
|
||||
})
|
||||
}
|
||||
|
||||
await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
|
||||
|
||||
await page.waitForTimeout(200)
|
||||
// hover over horizontal line
|
||||
await u.canvasLocator.hover({ position: { x: 800, y } })
|
||||
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
|
||||
timeout: 10_000,
|
||||
})
|
||||
await page.waitForTimeout(200)
|
||||
|
||||
await hoverOverNothing()
|
||||
await page.waitForTimeout(200)
|
||||
// hover over vertical line
|
||||
await u.canvasLocator.hover({ position: { x, y: 325 } })
|
||||
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
|
||||
timeout: 10_000,
|
||||
})
|
||||
|
||||
await hoverOverNothing()
|
||||
|
||||
// click exit sketch
|
||||
await page.getByRole('button', { name: 'Exit Sketch' }).click()
|
||||
await page.waitForTimeout(400)
|
||||
|
||||
await hoverOverNothing()
|
||||
await page.waitForTimeout(200)
|
||||
// hover over horizontal line
|
||||
await page.mouse.move(858, y, { steps: 5 })
|
||||
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
|
||||
timeout: 10_000,
|
||||
})
|
||||
|
||||
await hoverOverNothing()
|
||||
|
||||
// hover over vertical line
|
||||
await page.mouse.move(x, 325)
|
||||
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
|
||||
timeout: 10_000,
|
||||
})
|
||||
|
||||
await hoverOverNothing()
|
||||
|
||||
// hover over vertical line
|
||||
await page.mouse.move(857, y)
|
||||
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
|
||||
timeout: 10_000,
|
||||
})
|
||||
// now click it
|
||||
await page.mouse.click(857, y)
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Edit Sketch' })
|
||||
).toBeVisible()
|
||||
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
||||
|
||||
await page.waitForTimeout(400)
|
||||
|
||||
await hoverOverNothing()
|
||||
x = 975
|
||||
y = 468
|
||||
|
||||
await page.waitForTimeout(100)
|
||||
await page.mouse.move(x, 419, { steps: 5 })
|
||||
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
|
||||
timeout: 10_000,
|
||||
})
|
||||
|
||||
await hoverOverNothing()
|
||||
|
||||
await page.mouse.move(855, y)
|
||||
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
|
||||
timeout: 10_000,
|
||||
})
|
||||
|
||||
await hoverOverNothing()
|
||||
|
||||
await page.getByRole('button', { name: 'Exit Sketch' }).click()
|
||||
await page.waitForTimeout(200)
|
||||
|
||||
await hoverOverNothing()
|
||||
await page.waitForTimeout(200)
|
||||
|
||||
await page.mouse.move(x, 419)
|
||||
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
|
||||
timeout: 10_000,
|
||||
})
|
||||
|
||||
await hoverOverNothing()
|
||||
|
||||
await page.mouse.move(855, y)
|
||||
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
|
||||
await expect(page.getByTestId('hover-highlight')).not.toBeVisible({
|
||||
timeout: 10_000,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
|
||||
|
||||
await page.waitForTimeout(200)
|
||||
// hover over horizontal line
|
||||
await u.canvasLocator.hover({ position: { x: 800, y } })
|
||||
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
|
||||
timeout: 10_000,
|
||||
})
|
||||
await page.waitForTimeout(200)
|
||||
|
||||
await hoverOverNothing()
|
||||
await page.waitForTimeout(200)
|
||||
// hover over vertical line
|
||||
await u.canvasLocator.hover({ position: { x, y: 325 } })
|
||||
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
|
||||
timeout: 10_000,
|
||||
})
|
||||
|
||||
await hoverOverNothing()
|
||||
|
||||
// click exit sketch
|
||||
await page.getByRole('button', { name: 'Exit Sketch' }).click()
|
||||
await page.waitForTimeout(400)
|
||||
|
||||
await hoverOverNothing()
|
||||
await page.waitForTimeout(200)
|
||||
// hover over horizontal line
|
||||
await page.mouse.move(858, y, { steps: 5 })
|
||||
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
|
||||
timeout: 10_000,
|
||||
})
|
||||
|
||||
await hoverOverNothing()
|
||||
|
||||
// hover over vertical line
|
||||
await page.mouse.move(x, 325)
|
||||
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
|
||||
timeout: 10_000,
|
||||
})
|
||||
|
||||
await hoverOverNothing()
|
||||
|
||||
// hover over vertical line
|
||||
await page.mouse.move(857, y)
|
||||
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
|
||||
timeout: 10_000,
|
||||
})
|
||||
// now click it
|
||||
await page.mouse.click(857, y)
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Edit Sketch' })
|
||||
).toBeVisible()
|
||||
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
||||
|
||||
await page.waitForTimeout(400)
|
||||
|
||||
await hoverOverNothing()
|
||||
x = 975
|
||||
y = 468
|
||||
|
||||
await page.waitForTimeout(100)
|
||||
await page.mouse.move(x, 419, { steps: 5 })
|
||||
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
|
||||
timeout: 10_000,
|
||||
})
|
||||
|
||||
await hoverOverNothing()
|
||||
|
||||
await page.mouse.move(855, y)
|
||||
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
|
||||
timeout: 10_000,
|
||||
})
|
||||
|
||||
await hoverOverNothing()
|
||||
|
||||
await page.getByRole('button', { name: 'Exit Sketch' }).click()
|
||||
await page.waitForTimeout(200)
|
||||
|
||||
await hoverOverNothing()
|
||||
await page.waitForTimeout(200)
|
||||
|
||||
await page.mouse.move(x, 419)
|
||||
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
|
||||
timeout: 10_000,
|
||||
})
|
||||
|
||||
await hoverOverNothing()
|
||||
|
||||
await page.mouse.move(855, y)
|
||||
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
|
||||
timeout: 10_000,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -1,6 +1,13 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
import * as fsp from 'fs/promises'
|
||||
import { getUtils, setup, setupElectron, tearDown } from './test-utils'
|
||||
import { join } from 'path'
|
||||
import {
|
||||
getUtils,
|
||||
setup,
|
||||
setupElectron,
|
||||
tearDown,
|
||||
executorInputPath,
|
||||
} from './test-utils'
|
||||
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
|
||||
import { TEST_SETTINGS_KEY, TEST_SETTINGS_CORRUPTED } from './storageStates'
|
||||
import * as TOML from '@iarna/toml'
|
||||
@ -115,6 +122,36 @@ test.describe('Testing settings', () => {
|
||||
).not.toBeChecked()
|
||||
})
|
||||
|
||||
test('Keybindings display the correct hotkey for Command Palette', async ({
|
||||
page,
|
||||
}) => {
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
await test.step('Open keybindings settings', async () => {
|
||||
// Open the settings modal with the browser keyboard shortcut
|
||||
await page.keyboard.press('ControlOrMeta+Shift+,')
|
||||
|
||||
// Go to Keybindings tab.
|
||||
const keybindingsTab = page.getByRole('radio', { name: 'Keybindings' })
|
||||
await keybindingsTab.click()
|
||||
})
|
||||
|
||||
// Go to the hotkey for Command Palette.
|
||||
const commandPalette = page.getByText('Toggle Command Palette')
|
||||
await commandPalette.scrollIntoViewIfNeeded()
|
||||
|
||||
// The heading is above it and should be in view now.
|
||||
const commandPaletteHeading = page.getByRole('heading', {
|
||||
name: 'Command Palette',
|
||||
})
|
||||
// The hotkey is in a kbd element next to the heading.
|
||||
const hotkey = commandPaletteHeading.locator('+ div kbd')
|
||||
const text = process.platform === 'darwin' ? 'Command+K' : 'Control+K'
|
||||
await expect(hotkey).toHaveText(text)
|
||||
})
|
||||
|
||||
test('Project and user settings can be reset', async ({ page }) => {
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
@ -203,10 +240,11 @@ test.describe('Testing settings', () => {
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async (dir) => {
|
||||
await fsp.mkdir(`${dir}/bracket`, { recursive: true })
|
||||
const bracketDir = join(dir, 'bracket')
|
||||
await fsp.mkdir(bracketDir, { recursive: true })
|
||||
await fsp.copyFile(
|
||||
'src/wasm-lib/tests/executor/inputs/focusrite_scarlett_mounting_braket.kcl',
|
||||
`${dir}/bracket/main.kcl`
|
||||
executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
|
||||
join(bracketDir, 'main.kcl')
|
||||
)
|
||||
},
|
||||
})
|
||||
@ -329,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')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -1,5 +1,13 @@
|
||||
import { test, expect, Page } from '@playwright/test'
|
||||
import { getUtils, setup, tearDown } from './test-utils'
|
||||
import {
|
||||
getUtils,
|
||||
setup,
|
||||
tearDown,
|
||||
setupElectron,
|
||||
createProjectAndRenameIt,
|
||||
} from './test-utils'
|
||||
import { join } from 'path'
|
||||
import fs from 'fs'
|
||||
|
||||
test.beforeEach(async ({ context, page }) => {
|
||||
await setup(context, page)
|
||||
@ -683,3 +691,61 @@ 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, dir } = await setupElectron({ testInfo })
|
||||
const fileExists = () =>
|
||||
fs.existsSync(join(dir, 'test-000', 'lego-2x4.kcl'))
|
||||
|
||||
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 })
|
||||
expect(fileExists()).toBeTruthy()
|
||||
})
|
||||
|
||||
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()
|
||||
expect(fileExists()).toBeFalsy()
|
||||
})
|
||||
|
||||
await electronApp.close()
|
||||
}
|
||||
)
|
||||
|
@ -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
@ -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
|
||||
}
|
||||
|
19
package.json
@ -26,7 +26,7 @@
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@headlessui/react": "^1.7.19",
|
||||
"@headlessui/tailwindcss": "^0.2.0",
|
||||
"@kittycad/lib": "^2.0.0",
|
||||
"@kittycad/lib": "^2.0.1",
|
||||
"@lezer/highlight": "^1.2.1",
|
||||
"@lezer/lr": "^1.4.1",
|
||||
"@react-hook/resize-observer": "^2.0.1",
|
||||
@ -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",
|
||||
@ -82,14 +83,13 @@
|
||||
"fmt-check": "prettier --check ./src *.ts *.json *.js ./e2e ./packages",
|
||||
"fetch:wasm": "./get-latest-wasm-bundle.sh",
|
||||
"isomorphic-copy-wasm": "(copy src/wasm-lib/pkg/wasm_lib_bg.wasm public || cp src/wasm-lib/pkg/wasm_lib_bg.wasm public)",
|
||||
"build:wasm-dev": "(cd src/wasm-lib && wasm-pack build --dev --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && yarn isomorphic-copy-wasm && yarn fmt",
|
||||
"build:wasm": "cd src/wasm-lib && wasm-pack build --release --target web --out-dir pkg && cargo test -p kcl-lib export_bindings && cd ../.. && yarn isomorphic-copy-wasm && yarn fmt",
|
||||
"build:wasm-clean": "yarn wasm-prep && yarn build:wasm",
|
||||
"build:wasm-dev": "yarn wasm-prep && (cd src/wasm-lib && wasm-pack build --dev --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && yarn isomorphic-copy-wasm && yarn fmt",
|
||||
"build:wasm": "yarn wasm-prep && cd src/wasm-lib && wasm-pack build --release --target web --out-dir pkg && cargo test -p kcl-lib export_bindings && cd ../.. && yarn isomorphic-copy-wasm && yarn fmt",
|
||||
"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",
|
||||
"wasm-prep": "rimraf src/wasm-lib/pkg && mkdirp src/wasm-lib/pkg && rimraf 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",
|
||||
@ -119,16 +119,18 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||
"@babel/preset-env": "^7.24.3",
|
||||
"@babel/preset-env": "^7.25.4",
|
||||
"@electron-forge/cli": "^7.4.0",
|
||||
"@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 +139,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",
|
||||
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -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 */
|
||||
}
|
||||
|
@ -9,6 +9,8 @@ import useHotkeyWrapper from 'lib/hotkeyWrapper'
|
||||
import { CustomIcon } from 'components/CustomIcon'
|
||||
import Tooltip from 'components/Tooltip'
|
||||
|
||||
export const COMMAND_PALETTE_HOTKEY = 'mod+k'
|
||||
|
||||
export const CommandBar = () => {
|
||||
const { pathname } = useLocation()
|
||||
const { commandBarState, commandBarSend } = useCommandsContext()
|
||||
@ -24,7 +26,7 @@ export const CommandBar = () => {
|
||||
}, [pathname])
|
||||
|
||||
// Hook up keyboard shortcuts
|
||||
useHotkeyWrapper(['mod+k'], () => {
|
||||
useHotkeyWrapper([COMMAND_PALETTE_HOTKEY], () => {
|
||||
if (commandBarState.context.commands.length === 0) return
|
||||
if (commandBarState.matches('Closed')) {
|
||||
commandBarSend({ type: 'Open' })
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import usePlatform from 'hooks/usePlatform'
|
||||
import { hotkeyDisplay } from 'lib/hotkeyWrapper'
|
||||
import { COMMAND_PALETTE_HOTKEY } from './CommandBar/CommandBar'
|
||||
|
||||
export function CommandBarOpenButton() {
|
||||
const { commandBarSend } = useCommandsContext()
|
||||
@ -12,7 +14,7 @@ export function CommandBarOpenButton() {
|
||||
>
|
||||
<span>Commands</span>
|
||||
<kbd className="bg-primary/10 dark:bg-chalkboard-80 dark:group-hover:bg-primary font-mono rounded-sm dark:text-inherit inline-block px-1 border-primary dark:border-chalkboard-90">
|
||||
{platform === 'macos' ? '⌘K' : '^/'}
|
||||
{hotkeyDisplay(COMMAND_PALETTE_HOTKEY, platform)}
|
||||
</kbd>
|
||||
</button>
|
||||
)
|
||||
|
@ -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})`
|
||||
|
@ -9,7 +9,7 @@ import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
|
||||
import { coreDump } from 'lang/wasm'
|
||||
import toast from 'react-hot-toast'
|
||||
import { CoreDumpManager } from 'lib/coredump'
|
||||
import openWindow from 'lib/openWindow'
|
||||
import openWindow, { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
||||
import { NetworkMachineIndicator } from './NetworkMachineIndicator'
|
||||
|
||||
export function LowerRightControls({
|
||||
@ -66,6 +66,9 @@ export function LowerRightControls({
|
||||
{children}
|
||||
<menu className="flex items-center justify-end gap-3 pointer-events-auto">
|
||||
<a
|
||||
onClick={openExternalBrowserIfDesktop(
|
||||
`https://github.com/KittyCAD/modeling-app/releases/tag/v${APP_VERSION}`
|
||||
)}
|
||||
href={`https://github.com/KittyCAD/modeling-app/releases/tag/v${APP_VERSION}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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'
|
||||
|
@ -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 = {
|
||||
|
@ -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} />
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
)
|
||||
}, 5_000)
|
||||
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
|
||||
|
@ -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,
|
||||
@ -284,6 +469,9 @@ export const hasValidFilletSelection = ({
|
||||
if (segmentNode.node.type === 'CallExpression') {
|
||||
const segmentName = segmentNode.node.callee.name
|
||||
if (segmentName in sketchLineHelperMap) {
|
||||
// Add check whether the tag exists at all:
|
||||
if (!(segmentNode.node.arguments.length === 3)) return true
|
||||
// If the tag exists, check if it is already filleted
|
||||
const edges = isTagUsedInFillet({
|
||||
ast,
|
||||
callExp: segmentNode.node,
|
||||
|
@ -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,
|
||||
},
|
||||
},
|
||||
|
@ -60,6 +60,7 @@ export const TEST_SETTINGS_FILE_KEY = 'playwright-test-settings'
|
||||
|
||||
export const DEFAULT_HOST = 'https://api.zoo.dev'
|
||||
export const SETTINGS_FILE_NAME = 'settings.toml'
|
||||
export const TOKEN_FILE_NAME = 'token.txt'
|
||||
export const PROJECT_SETTINGS_FILE_NAME = 'project.toml'
|
||||
export const COOKIE_NAME = '__Secure-next-auth.session-token'
|
||||
|
||||
|
@ -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,
|
||||
@ -15,6 +13,7 @@ import {
|
||||
PROJECT_FOLDER,
|
||||
PROJECT_SETTINGS_FILE_NAME,
|
||||
SETTINGS_FILE_NAME,
|
||||
TOKEN_FILE_NAME,
|
||||
} from './constants'
|
||||
import { DeepPartial } from './types'
|
||||
import { ProjectConfiguration } from 'wasm-lib/kcl/bindings/ProjectConfiguration'
|
||||
@ -398,6 +397,23 @@ const getAppSettingsFilePath = async () => {
|
||||
}
|
||||
return window.electron.path.join(fullPath, SETTINGS_FILE_NAME)
|
||||
}
|
||||
const getTokenFilePath = async () => {
|
||||
const isTestEnv = window.electron.process.env.IS_PLAYWRIGHT === 'true'
|
||||
const testSettingsPath = window.electron.process.env.TEST_SETTINGS_FILE_KEY
|
||||
const appConfig = await window.electron.getPath('appData')
|
||||
const fullPath = isTestEnv
|
||||
? testSettingsPath
|
||||
: window.electron.path.join(appConfig, getAppFolderName())
|
||||
try {
|
||||
await window.electron.stat(fullPath)
|
||||
} catch (e) {
|
||||
// File/path doesn't exist
|
||||
if (e === 'ENOENT') {
|
||||
await window.electron.mkdir(fullPath, { recursive: true })
|
||||
}
|
||||
}
|
||||
return window.electron.path.join(fullPath, TOKEN_FILE_NAME)
|
||||
}
|
||||
|
||||
const getProjectSettingsFilePath = async (projectPath: string) => {
|
||||
try {
|
||||
@ -477,15 +493,31 @@ export const writeAppSettingsFile = async (tomlStr: string) => {
|
||||
return window.electron.writeFile(appSettingsFilePath, tomlStr)
|
||||
}
|
||||
|
||||
let appStateStore: ProjectState | undefined = undefined
|
||||
export const readTokenFile = async () => {
|
||||
let settingsPath = await getTokenFilePath()
|
||||
|
||||
export const getState = async (): Promise<ProjectState | undefined> => {
|
||||
if (window.electron.exists(settingsPath)) {
|
||||
const token: string = await window.electron.readFile(settingsPath)
|
||||
if (!token) return ''
|
||||
|
||||
return token
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
export const writeTokenFile = async (token: string) => {
|
||||
const tokenFilePath = await getTokenFilePath()
|
||||
if (err(token)) return Promise.reject(token)
|
||||
return window.electron.writeFile(tokenFilePath, token)
|
||||
}
|
||||
|
||||
let appStateStore: Project | undefined = undefined
|
||||
|
||||
export const getState = async (): Promise<Project | undefined> => {
|
||||
return Promise.resolve(appStateStore)
|
||||
}
|
||||
|
||||
export const setState = async (
|
||||
state: ProjectState | undefined
|
||||
): Promise<void> => {
|
||||
export const setState = async (state: Project | undefined): Promise<void> => {
|
||||
appStateStore = state
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
98
src/lib/getCurrentProjectFile.test.ts
Normal 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 })
|
||||
}
|
||||
})
|
||||
})
|
116
src/lib/getCurrentProjectFile.ts
Normal 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
|
||||
}
|
35
src/lib/hotkeyWrapper.test.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { hotkeyDisplay } from './hotkeyWrapper'
|
||||
|
||||
describe('hotkeyDisplay', () => {
|
||||
it('displays mod', async () => {
|
||||
expect(hotkeyDisplay('mod+c', 'macos')).toEqual('⌘C')
|
||||
expect(hotkeyDisplay('mod+c', 'windows')).toEqual('Ctrl+C')
|
||||
expect(hotkeyDisplay('mod+c', 'linux')).toEqual('Ctrl+C')
|
||||
})
|
||||
it('displays shift', async () => {
|
||||
expect(hotkeyDisplay('shift+c', 'macos')).toEqual('⬆C')
|
||||
expect(hotkeyDisplay('shift+c', 'windows')).toEqual('Shift+C')
|
||||
expect(hotkeyDisplay('shift+c', 'linux')).toEqual('Shift+C')
|
||||
})
|
||||
it('displays meta', async () => {
|
||||
expect(hotkeyDisplay('meta+c', 'macos')).toEqual('⌘C')
|
||||
expect(hotkeyDisplay('meta+c', 'windows')).toEqual('Win+C')
|
||||
// That's correct. What browsers call meta is actually super.
|
||||
expect(hotkeyDisplay('meta+c', 'linux')).toEqual('Super+C')
|
||||
})
|
||||
it('displays alt', async () => {
|
||||
expect(hotkeyDisplay('alt+c', 'macos')).toEqual('⌥C')
|
||||
expect(hotkeyDisplay('alt+c', 'windows')).toEqual('Alt+C')
|
||||
expect(hotkeyDisplay('alt+c', 'linux')).toEqual('Alt+C')
|
||||
})
|
||||
it('displays ctrl', async () => {
|
||||
expect(hotkeyDisplay('ctrl+c', 'macos')).toEqual('^C')
|
||||
expect(hotkeyDisplay('ctrl+c', 'windows')).toEqual('Ctrl+C')
|
||||
expect(hotkeyDisplay('ctrl+c', 'linux')).toEqual('Ctrl+C')
|
||||
})
|
||||
it('displays multiple modifiers', async () => {
|
||||
expect(hotkeyDisplay('shift+alt+ctrl+c', 'windows')).toEqual(
|
||||
'Shift+Alt+Ctrl+C'
|
||||
)
|
||||
})
|
||||
})
|
@ -1,9 +1,10 @@
|
||||
import { Options, useHotkeys } from 'react-hotkeys-hook'
|
||||
import { useEffect } from 'react'
|
||||
import { codeManager } from './singletons'
|
||||
import { Platform } from './utils'
|
||||
|
||||
// Hotkey wrapper wraps hotkeys for the app (outside of the editor)
|
||||
// With hotkeys inside the editor.
|
||||
// with hotkeys inside the editor.
|
||||
// This way we can have hotkeys defined in one place and not have to worry about
|
||||
// conflicting hotkeys, or them only being implemented for the app but not
|
||||
// inside the editor.
|
||||
@ -37,3 +38,48 @@ function mapHotkeyToCodeMirrorHotkey(hotkey: string): string {
|
||||
.replaceAll('shift', 'Shift')
|
||||
.replaceAll('alt', 'Alt')
|
||||
}
|
||||
|
||||
const LOWER_CASE_LETTER = /[a-z]/
|
||||
const WHITESPACE = /\s+/g
|
||||
|
||||
/**
|
||||
* Convert hotkey to display text.
|
||||
*
|
||||
* TODO: We should handle capitalized single letter hotkeys like K as Shift+K,
|
||||
* but we don't.
|
||||
*/
|
||||
export function hotkeyDisplay(hotkey: string, platform: Platform): string {
|
||||
const isMac = platform === 'macos'
|
||||
const isWindows = platform === 'windows'
|
||||
// Browsers call it metaKey, but that's a misnomer.
|
||||
const meta = isWindows ? 'Win' : 'Super'
|
||||
const outputSeparator = isMac ? '' : '+'
|
||||
const display = hotkey
|
||||
// Capitalize letters. We want Ctrl+K, not Ctrl+k, since Shift should be
|
||||
// shown as a separate modifier.
|
||||
.split('+')
|
||||
.map((word) => {
|
||||
if (word.length === 1 && LOWER_CASE_LETTER.test(word)) {
|
||||
return word.toUpperCase()
|
||||
}
|
||||
return word
|
||||
})
|
||||
.join(outputSeparator)
|
||||
// Collapse multiple spaces into one.
|
||||
.replaceAll(WHITESPACE, ' ')
|
||||
.replaceAll('mod', isMac ? '⌘' : 'Ctrl')
|
||||
.replaceAll('meta', isMac ? '⌘' : meta)
|
||||
// This is technically the wrong arrow for control, but it's more visible
|
||||
// and recognizable. May want to change this in the future.
|
||||
//
|
||||
// The correct arrow is ⌃ "UP ARROWHEAD" Unicode: U+2303
|
||||
.replaceAll('ctrl', isMac ? '^' : 'Ctrl')
|
||||
// This is technically the wrong arrow for shift, but it's more visible and
|
||||
// recognizable. May want to change this in the future.
|
||||
//
|
||||
// The correct arrow is ⇧ "UPWARDS WHITE ARROW" Unicode: U+21E7
|
||||
.replaceAll('shift', isMac ? '⬆' : 'Shift')
|
||||
.replaceAll('alt', isMac ? '⌥' : 'Alt')
|
||||
|
||||
return display
|
||||
}
|
||||
|
46
src/lib/project.d.ts
vendored
Normal 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
|
||||
}
|
@ -61,7 +61,7 @@ export const interactionMap: Record<
|
||||
name: 'toggle-command-palette',
|
||||
sequence: `${PRIMARY}+K`,
|
||||
title: 'Toggle Command Palette',
|
||||
description: 'Always available. Use Ctrl+/ on Windows/Linux.',
|
||||
description: 'Always available.',
|
||||
},
|
||||
],
|
||||
Panes: [
|
||||
|
@ -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'
|
||||
|
||||
|
@ -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
|
||||
|
@ -147,10 +147,39 @@ export function platform(): Platform {
|
||||
case 'sunos':
|
||||
return 'linux'
|
||||
default:
|
||||
console.error('Unknown platform:', platform)
|
||||
console.error('Unknown desktop platform:', platform)
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
// navigator.platform is deprecated, but many browsers still support it, and
|
||||
// it's more accurate than userAgent and userAgentData in Playwright.
|
||||
if (
|
||||
navigator.platform?.indexOf('Mac') === 0 ||
|
||||
navigator.platform?.indexOf('iPhone') === 0 ||
|
||||
navigator.platform?.indexOf('iPad') === 0 ||
|
||||
// Vite tests running in HappyDOM.
|
||||
navigator.platform?.indexOf('Darwin') >= 0
|
||||
) {
|
||||
return 'macos'
|
||||
}
|
||||
if (navigator.platform === 'Windows' || navigator.platform === 'Win32') {
|
||||
return 'windows'
|
||||
}
|
||||
|
||||
// Chrome only, but more accurate than userAgent.
|
||||
let userAgentDataPlatform: unknown
|
||||
if (
|
||||
'userAgentData' in navigator &&
|
||||
navigator.userAgentData &&
|
||||
typeof navigator.userAgentData === 'object' &&
|
||||
'platform' in navigator.userAgentData
|
||||
) {
|
||||
userAgentDataPlatform = navigator.userAgentData.platform
|
||||
if (userAgentDataPlatform === 'macOS') return 'macos'
|
||||
if (userAgentDataPlatform === 'Windows') return 'windows'
|
||||
}
|
||||
|
||||
if (navigator.userAgent.indexOf('Mac') !== -1) {
|
||||
return 'macos'
|
||||
} else if (navigator.userAgent.indexOf('Win') !== -1) {
|
||||
@ -158,7 +187,12 @@ export function platform(): Platform {
|
||||
} else if (navigator.userAgent.indexOf('Linux') !== -1) {
|
||||
return 'linux'
|
||||
}
|
||||
console.error('Unknown platform userAgent:', navigator.userAgent)
|
||||
console.error(
|
||||
'Unknown web platform:',
|
||||
navigator.platform,
|
||||
userAgentDataPlatform,
|
||||
navigator.userAgent
|
||||
)
|
||||
return ''
|
||||
}
|
||||
|
||||
|
@ -8,7 +8,11 @@ import {
|
||||
VITE_KC_SKIP_AUTH,
|
||||
DEV,
|
||||
} from 'env'
|
||||
import { getUser as getUserDesktop } from 'lib/desktop'
|
||||
import {
|
||||
getUser as getUserDesktop,
|
||||
readTokenFile,
|
||||
writeTokenFile,
|
||||
} from 'lib/desktop'
|
||||
import { COOKIE_NAME } from 'lib/constants'
|
||||
|
||||
const SKIP_AUTH = VITE_KC_SKIP_AUTH === 'true' && DEV
|
||||
@ -53,6 +57,7 @@ const persistedToken =
|
||||
|
||||
export const authMachine = createMachine<UserContext, Events>(
|
||||
{
|
||||
/** @xstate-layout N4IgpgJg5mDOIC5QEECuAXAFgOgMabFwGsBJAMwBkB7KGCEgOwGIIqGxsBLBgNyqI75CRALQAbGnRHcA2gAYAuolAAHKrE7pObZSAAeiAIwBmAEzYA7ABYAbAFZTcgBzGbN44adWANCACeiKbGdthypk4AnBFyVs6uQXYAvom+aFh4BMTk1LSQjExgAE6FVIXYKmIAhuhkpQC2GcLikpDSDPJKSCBqGlo6XQYIrk7YETYWctYRxmMWFk6+AUPj2I5OdjZyrnZOFmbJqRg4Ern0zDkABFQYHbo9mtoMuoOGFhHYxlZOhvbOsUGGRaIL4WbBONzWQxWYwWOx2H4HEBpY4tCAAeQwTEuskUd3UD36oEGIlMNlCuzk8Js0TcVisgP8iG2lmcGysb0mW3ByRSIAYVAgcF0yLxvUez0QIms5ImVJpNjpDKWxmw9PGdLh4Te00+iORjSylFRjFFBKeA0QThGQWcexMwWhniBCGiqrepisUVMdlszgieqO2BOdBNXXufXNRKMHtGVuphlJkXs4Wdriso2CCasdgipOidID6WDkAx6FNEYlCAT5jmcjrckMdj2b3GzpsjbBMVMWezDbGPMSQA */
|
||||
id: 'Auth',
|
||||
initial: 'checkIfLoggedIn',
|
||||
states: {
|
||||
@ -85,6 +90,9 @@ export const authMachine = createMachine<UserContext, Events>(
|
||||
on: {
|
||||
'Log out': {
|
||||
target: 'loggedOut',
|
||||
actions: () => {
|
||||
if (isDesktop()) writeTokenFile('')
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -96,7 +104,6 @@ export const authMachine = createMachine<UserContext, Events>(
|
||||
actions: assign({
|
||||
token: (_, event) => {
|
||||
const token = event.token || ''
|
||||
localStorage.setItem(TOKEN_PERSIST_KEY, token)
|
||||
return token
|
||||
},
|
||||
}),
|
||||
@ -120,11 +127,7 @@ export const authMachine = createMachine<UserContext, Events>(
|
||||
)
|
||||
|
||||
async function getUser(context: UserContext) {
|
||||
const token = VITE_KC_DEV_TOKEN
|
||||
? VITE_KC_DEV_TOKEN
|
||||
: context.token && context.token !== ''
|
||||
? context.token
|
||||
: getCookie(COOKIE_NAME) || localStorage?.getItem(TOKEN_PERSIST_KEY)
|
||||
const token = await getAndSyncStoredToken(context)
|
||||
const url = withBaseURL('/user')
|
||||
const headers: { [key: string]: string } = {
|
||||
'Content-Type': 'application/json',
|
||||
@ -189,3 +192,26 @@ function getCookie(cname: string): string | null {
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
async function getAndSyncStoredToken(context: UserContext): Promise<string> {
|
||||
// dev mode
|
||||
if (VITE_KC_DEV_TOKEN) return VITE_KC_DEV_TOKEN
|
||||
|
||||
const token =
|
||||
context.token && context.token !== ''
|
||||
? context.token
|
||||
: getCookie(COOKIE_NAME) || localStorage?.getItem(TOKEN_PERSIST_KEY) || ''
|
||||
if (token) {
|
||||
// has just logged in, update storage
|
||||
localStorage.setItem(TOKEN_PERSIST_KEY, token)
|
||||
isDesktop() && writeTokenFile(token)
|
||||
return token
|
||||
}
|
||||
if (!isDesktop()) return ''
|
||||
const fileToken = isDesktop() ? await readTokenFile() : ''
|
||||
// prefer other above, but file will ensure login persists after app updates
|
||||
if (!fileToken) return ''
|
||||
// has token in file, update localStorage
|
||||
localStorage.setItem(TOKEN_PERSIST_KEY, fileToken)
|
||||
return fileToken
|
||||
}
|
||||
|
@ -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(
|
||||
{
|
||||
|
@ -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(
|
||||
{
|
||||
|
@ -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') {
|
||||
|
144
src/main.ts
@ -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)
|
||||
})
|
||||
}
|
||||
|
@ -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),
|
||||
|
@ -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.
|
||||
|
@ -2,6 +2,8 @@ import usePlatform from 'hooks/usePlatform'
|
||||
import { OnboardingButtons, kbdClasses, useDismiss, useNextClick } from '.'
|
||||
import { onboardingPaths } from 'routes/Onboarding/paths'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
import { hotkeyDisplay } from 'lib/hotkeyWrapper'
|
||||
import { COMMAND_PALETTE_HOTKEY } from 'components/CommandBar/CommandBar'
|
||||
|
||||
export default function CmdK() {
|
||||
const { context } = useModelingContext()
|
||||
@ -20,15 +22,9 @@ export default function CmdK() {
|
||||
<h2 className="text-2xl font-bold">Command Bar</h2>
|
||||
<p className="my-4">
|
||||
Press{' '}
|
||||
{platformName === 'macos' ? (
|
||||
<>
|
||||
<kbd className={kbdClasses}>⌘K</kbd>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<kbd className={kbdClasses}>Ctrl + /</kbd>
|
||||
</>
|
||||
)}{' '}
|
||||
<kbd className={kbdClasses}>
|
||||
{hotkeyDisplay(COMMAND_PALETTE_HOTKEY, platformName)}
|
||||
</kbd>{' '}
|
||||
to open the command bar. Try changing your theme with it.
|
||||
</p>
|
||||
<p className="my-4">
|
||||
|
166
src/wasm-lib/Cargo.lock
generated
@ -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",
|
||||
]
|
||||
|
||||
@ -724,7 +672,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "derive-docs"
|
||||
version = "0.1.24"
|
||||
version = "0.1.25"
|
||||
dependencies = [
|
||||
"Inflector",
|
||||
"anyhow",
|
||||
@ -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.7"
|
||||
version = "0.2.11"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"approx",
|
||||
@ -1464,12 +1412,12 @@ dependencies = [
|
||||
"pretty_assertions",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.75",
|
||||
"syn 2.0.76",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kcl-test-server"
|
||||
version = "0.1.8"
|
||||
version = "0.1.9"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"hyper",
|
||||
@ -1482,9 +1430,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kittycad"
|
||||
version = "0.3.16"
|
||||
version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a9621df809fa105ae28b01586c52ce46561e166702babb50e5bcc5097a8ce62"
|
||||
checksum = "fbb7c076d64ad00a29ae900108707d1bbb583944d4b2d005e1eca9914a18c7c2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@ -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]]
|
||||
@ -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]]
|
||||
@ -2096,9 +2043,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.36"
|
||||
version = "1.0.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
|
||||
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
@ -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]]
|
||||
|
@ -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"] }
|
||||
@ -71,7 +70,7 @@ members = [
|
||||
|
||||
[workspace.dependencies]
|
||||
http = "0.2.12"
|
||||
kittycad = { version = "0.3.16", default-features = false, features = ["js", "requests"] }
|
||||
kittycad = { version = "0.3.17", default-features = false, features = ["js", "requests"] }
|
||||
kittycad-modeling-session = "0.1.4"
|
||||
|
||||
[[test]]
|
||||
|
@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "derive-docs"
|
||||
description = "A tool for generating documentation from Rust derive macros"
|
||||
version = "0.1.24"
|
||||
version = "0.1.25"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/KittyCAD/modeling-app"
|
||||
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "kcl-test-server"
|
||||
description = "A test server for KCL"
|
||||
version = "0.1.8"
|
||||
version = "0.1.9"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
|
||||
@ -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"] }
|
||||
|
@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "kcl-lib"
|
||||
description = "KittyCAD Language implementation and tools"
|
||||
version = "0.2.7"
|
||||
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.
|
||||
|