Compare commits

..

6 Commits

301 changed files with 6534 additions and 22315 deletions

Binary file not shown.

View File

@ -1,3 +1,3 @@
[codespell]
ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,absolutey,atleast,ue,afterall
skip: **/target,node_modules,build,**/Cargo.lock,./docs/kcl/*.md,.yarn.lock,**/yarn.lock,./openapi/*.json,./src/lib/machine-api.d.ts
skip: **/target,node_modules,build,**/Cargo.lock,./docs/kcl/*.md,.yarn.lock,**/yarn.lock

View File

@ -1,4 +1,3 @@
src/wasm-lib/*
src/lib/engine-utils/engine.js
*.typegen.ts
packages/codemirror-lsp-client/dist/*

View File

@ -25,7 +25,6 @@ jobs:
runs-on: ubuntu-22.04 # seperate job on Ubuntu for easy string manipulations (compared to Windows)
outputs:
version: ${{ steps.export_version.outputs.version }}
notes: ${{ steps.export_version.outputs.notes }}
steps:
- uses: actions/checkout@v4
@ -52,22 +51,12 @@ jobs:
run: |
VERSION=$(date +'%-y.%-m.%-d') yarn bump-jsons
# TODO: see if we need to inject updater nightly URL here https://dl.zoo.dev/releases/modeling-app/nightly/last_update.json
- name: Generate release notes
env:
NOTES: ${{ github.event_name == 'release' && github.event.release.body || format('Non-release build, commit {0}', github.sha) }}
run: |
echo "$NOTES" > release-notes.md
cat release-notes.md
- uses: actions/upload-artifact@v3
with:
name: prepared-files
path: |
package.json
src/wasm-lib/pkg/wasm_lib*
release-notes.md
- id: export_version
run: echo "version=`cat package.json | jq -r '.version'`" >> "$GITHUB_OUTPUT"
@ -84,13 +73,10 @@ jobs:
path: |
electron-builder.yml
- id: export_notes
run: echo "notes=`cat release-notes.md'`" >> "$GITHUB_OUTPUT"
- name: Prepare electron-builder.yml file for updater test
if: ${{ env.CUT_RELEASE_PR == 'true' }}
run: |
yq -i '.publish[0].url = "https://dl.zoo.dev/releases/modeling-app/updater-test-release-notes"' electron-builder.yml
yq -i '.publish[0].url = "https://dl.zoo.dev/releases/modeling-app/updater-test"' electron-builder.yml
- uses: actions/upload-artifact@v3
if: ${{ env.CUT_RELEASE_PR == 'true' }}
@ -105,13 +91,7 @@ jobs:
strategy:
fail-fast: false
matrix:
include:
- os: macos-14
platform: mac
- os: windows-2022
platform: win
- os: ubuntu-22.04
platform: linux
os: [macos-14, windows-2022, ubuntu-22.04]
runs-on: ${{ matrix.os }}
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
@ -138,7 +118,6 @@ jobs:
cp prepared-files/src/wasm-lib/pkg/wasm_lib_bg.wasm public
mkdir src/wasm-lib/pkg
cp prepared-files/src/wasm-lib/pkg/wasm_lib* src/wasm-lib/pkg
cp prepared-files/release-notes.md release-notes.md
- uses: actions/download-artifact@v3
if: ${{ github.event_name == 'schedule' }}
@ -194,29 +173,17 @@ jobs:
- uses: actions/upload-artifact@v3
with:
name: out-arm64-${{ matrix.platform }}
# first two will pick both Zoo Modeling App-$VERSION-arm64-win.exe and Zoo Modeling App-$VERSION-win.exe
path: |
out/*-${{ env.VERSION_NO_V }}-win.*
out/*-${{ env.VERSION_NO_V }}-arm64-win.*
out/*-arm64-mac.*
out/*-arm64-linux.*
- uses: actions/upload-artifact@v3
with:
name: out-x64-${{ matrix.platform }}
path: |
out/*-x64-win.*
out/*-x64-mac.*
out/*-x86_64-linux.*
- uses: actions/upload-artifact@v3
if: ${{ env.BUILD_RELEASE == 'true' }}
with:
name: out-yml
name: out-arm64-${{ matrix.os }}
path: |
out/Zoo*arm64*.*
out/latest*.yml
- uses: actions/upload-artifact@v3
with:
name: out-x64-${{ matrix.os }}
path: |
out/Zoo*x*64*.*
# TODO: add the 'Build for Mac TestFlight (nightly)' stage back
- uses: actions/download-artifact@v3
@ -236,20 +203,16 @@ jobs:
- uses: actions/upload-artifact@v3
if: ${{ env.CUT_RELEASE_PR == 'true' }}
with:
name: updater-test-arm64-${{ matrix.platform }}
name: updater-test-arm64-${{ matrix.os }}
path: |
out/*-arm64-win.exe
out/*-arm64-mac.dmg
out/*-arm64-linux.AppImage
out/Zoo*arm64*.*
- uses: actions/upload-artifact@v3
if: ${{ env.CUT_RELEASE_PR == 'true' }}
with:
name: updater-test-x64-${{ matrix.platform }}
name: updater-test-x64-${{ matrix.os }}
path: |
out/*-x64-win.exe
out/*-x64-mac.dmg
out/*-x86_64-linux.AppImage
out/Zoo*x64*.*
publish-apps-release:
@ -262,7 +225,7 @@ jobs:
VERSION_NO_V: ${{ needs.prepare-files.outputs.version }}
VERSION: ${{ github.event_name == 'schedule' && needs.prepare-files.outputs.version || format('v{0}', needs.prepare-files.outputs.version) }}
PUB_DATE: ${{ github.event_name == 'release' && github.event.release.created_at || github.event.repository.updated_at }}
NOTES: ${{ needs.prepare-files.outputs.notes }}
NOTES: ${{ github.event_name == 'release' && github.event.release.body || format('Non-release build, commit {0}', github.sha) }}
BUCKET_DIR: ${{ github.event_name == 'schedule' && 'dl.kittycad.io/releases/modeling-app/nightly' || 'dl.kittycad.io/releases/modeling-app' }}
WEBSITE_DIR: ${{ github.event_name == 'schedule' && 'dl.zoo.dev/releases/modeling-app/nightly' || 'dl.zoo.dev/releases/modeling-app' }}
URL_CODED_NAME: ${{ github.event_name == 'schedule' && 'Zoo%20Modeling%20App%20%28Nightly%29' || 'Zoo%20Modeling%20App' }}
@ -271,37 +234,32 @@ jobs:
- uses: actions/download-artifact@v3
with:
name: out-arm64-win
name: out-arm64-windows-2022
path: out
- uses: actions/download-artifact@v3
with:
name: out-x64-win
name: out-x64-windows-2022
path: out
- uses: actions/download-artifact@v3
with:
name: out-arm64-mac
name: out-arm64-macos-14
path: out
- uses: actions/download-artifact@v3
with:
name: out-x64-mac
name: out-x64-macos-14
path: out
- uses: actions/download-artifact@v3
with:
name: out-arm64-linux
name: out-arm64-ubuntu-22.04
path: out
- uses: actions/download-artifact@v3
with:
name: out-x64-linux
path: out
- uses: actions/download-artifact@v3
with:
name: out-yml
name: out-x64-ubuntu-22.04
path: out
- name: Generate the download static endpoint

View File

@ -37,6 +37,10 @@ jobs:
node-version-file: '.nvmrc'
cache: 'yarn'
- run: yarn install
- uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- run: yarn build:wasm
yarn-tsc:
@ -66,6 +70,10 @@ jobs:
node-version-file: '.nvmrc'
cache: 'yarn'
- run: yarn install
- uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- run: yarn lint
python-codespell:
@ -93,6 +101,11 @@ jobs:
cache: 'yarn'
- run: yarn install
- uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- run: yarn build:wasm
- run: yarn simpleserver:bg

4
.gitignore vendored
View File

@ -66,7 +66,3 @@ venv
# electron
out/
# engine wasm utils
src/lib/engine-utils/engine.wasm
src/lib/engine-utils/engine.js

View File

@ -2,7 +2,7 @@
## Zoo Modeling App
download at [zoo.dev/modeling-app/download](https://zoo.dev/modeling-app/download)
live at [app.zoo.dev](https://app.zoo.dev/)
A CAD application from the future, brought to you by the [Zoo team](https://zoo.dev).
@ -57,7 +57,7 @@ yarn install
followed by:
```
yarn build:wasm
yarn build:wasm-dev
```
or if you have the gh cli installed
@ -66,15 +66,15 @@ or if you have the gh cli installed
./get-latest-wasm-bundle.sh # this will download the latest main wasm bundle
```
That will build the WASM binary and put in the `public` dir (though gitignored).
That will build the WASM binary and put in the `public` dir (though gitignored)
Finally, to run the web app only, run:
finally, to run the web app only, run:
```
yarn start
```
If you're not an KittyCAD employee you won't be able to access the dev environment, you should copy everything from `.env.production` to `.env.development` to make it point to production instead, then when you navigate to `localhost:3000` the easiest way to sign in is to paste `localStorage.setItem('TOKEN_PERSIST_KEY', "your-token-from-https://zoo.dev/account/api-tokens")` replacing the with a real token from https://zoo.dev/account/api-tokens of course, then navigate to localhost:3000 again. Note that navigating to `localhost:3000/signin` removes your token so you will need to set the token again.
If you're not an KittyCAD employee you won't be able to access the dev environment, you should copy everything from `.env.production` to `.env.development` to make it point to production instead, then when you navigate to `localhost:3000` the easiest way to sign in is to paste `localStorage.setItem('TOKEN_PERSIST_KEY', "your-token-from-https://zoo.dev/account/api-tokens")` replacing the with a real token from https://zoo.dev/account/api-tokens ofcourse, then navigate to localhost:3000 again. Note that navigating to localhost:3000/signin removes your token so you will need to set the token again.
### Development environment variables
@ -91,13 +91,13 @@ Third-Party Cookies".
## Desktop
To spin up the desktop app, `yarn install` and `yarn build:wasm` need to have been done before hand then
To spin up the desktop app, `yarn install` and `yarn build:wasm-dev` need to have been done before hand then
```
yarn tron:start
yarn electron:start
```
This will start the application and hot-reload on changes.
This will start the application and hot-reload on changed.
Devtools can be opened with the usual Cmd/Ctrl-Shift-I.
@ -128,18 +128,7 @@ Before you submit a contribution PR to this repo, please ensure that:
## Release a new version
#### 1. Bump the versions by running `./make-release.sh`
The `./make-release.sh` script has git commands to pull main but to be sure you can run the following git commands to have a fresh `main` locally.
```
git branch -D main
git checkout main
git pull origin
./make-release.sh
# Copy within the back ticks and paste the stdout of the change log
git push --set-upstream origin <branch name created from ./make-release.sh>
```
#### 1. Bump the versions by running `./make-release.sh` and create a Cut Release PR
That will create the branch with the updated json files for you:
- run `./make-release.sh` or `./make-release.sh patch` for a patch update;
@ -148,50 +137,28 @@ That will create the branch with the updated json files for you:
After it runs you should just need the push the branch and open a PR.
#### 2. Create a Cut Release PR
When you open the PR copy the change log from the output of the `./make-release.sh` script into the description of the PR.
**Important:** Pull request title needs to be prefixed with `Cut release v` to build in release mode and a few other things to test in the best context possible, the intent would be for instance to have `Cut release v1.2.3` for the `v1.2.3` release candidate.
**Important:** It needs to be prefixed with `Cut release v` to build in release mode and a few other things to test in the best context possible, the intent would be for instance to have `Cut release v1.2.3` for the `v1.2.3` release candidate.
The PR may then serve as a place to discuss the human-readable changelog and extra QA. The `make-release.sh` tool suggests a changelog for you too to be used as PR description, just make sure to delete lines that are not user facing.
#### 3. Manually test artifacts from the Cut Release PR
#### 2. Smoke test artifacts from the Cut Release PR
##### Release builds
The release builds can be find under the `artifact` zip, at the very bottom of the `ci` action page for each commit on this branch.
The release builds can be found under the `out-{platform}` zip, at the very bottom of the `build-publish-apps` summary page for each commit on this branch.
We don't have a strict process, but click around and check for anything obvious, posting results as comments in the Cut Release PR.
Manually test against this [list](https://github.com/KittyCAD/modeling-app/issues/3588) across Windows, MacOS, Linux and posting results as comments in the Cut Release PR.
The other `ci` output in Cut Release PRs is `updater-test`, because we don't have a way to test this fully automated, we have a semi-automated process. Download updater-test zip file, install the app, run it, expect an updater prompt to a dummy v0.99.99, install it and check that the app comes back at that version (on both macOS and Windows).
##### Updater-test builds
The other `build-publish-apps` output in Cut Release PRs is `updater-test-{platform}`. As we don't have a way to test this fully automatically, we have a semi-automated process. For macOS, Windows, and Linux, download the corresponding updater-test artifact file, install the app, run it, expect an updater prompt to a dummy v0.255.255, install it and check that the app comes back at that version.
The only difference with these builds is that they point to a different update location on the release bucket, with this dummy v0.255.255 always available. This helps ensuring that the version we release will be able to update to the next one available.
If the prompt doesn't show up, start the app in command line to grab the electron-updater logs. This is likely an issue with the current build that needs addressing (or the updater-test location in the storage bucket).
```
# Windows (PowerShell)
& 'C:\Program Files\Zoo Modeling App\Zoo Modeling App.exe'
# macOS
/Applications/Zoo\ Modeling\ App.app/Contents/MacOS/Zoo\ Modeling\ App
# Linux
./Zoo Modeling App-{version}-{arch}-linux.AppImage
```
#### 4. Merge the Cut Release PR
#### 3. Merge the Cut Release PR
This will kick the `create-release` action, that creates a _Draft_ release out of this Cut Release PR merge after less than a minute, with the new version as title and Cut Release PR as description.
#### 5. Publish the release
#### 4. Publish the release
Head over to https://github.com/KittyCAD/modeling-app/releases, the draft release corresponding to the merged Cut Release PR should show up at the top as _Draft_. Click on it, verify the content, and hit _Publish_.
#### 6. Profit
#### 5. Profit
A new Action kicks in at https://github.com/KittyCAD/modeling-app/actions, which can be found under `release` event filter.
@ -352,16 +319,7 @@ Which will run our suite of [Vitest unit](https://vitest.dev/) and [React Testin
```bash
cd src/wasm-lib
KITTYCAD_API_TOKEN=XXX cargo test -- --test-threads=1
```
Where `XXX` is an API token from the production engine (NOT the dev environment).
We recommend using [nextest](https://nexte.st/) to run the Rust tests (its faster and is used in CI). Once installed, run the tests using
```
cd src/wasm-lib
KITTYCAD_API_TOKEN=XXX cargo run nextest
cargo test
```
### Mapping CI CD jobs to local commands

File diff suppressed because one or more lines are too long

View File

@ -36,7 +36,7 @@ exampleSketch = startSketchOn('XZ')
|> close(%)
|> patternCircular2d({
center: [0, 0],
instances: 13,
repetitions: 12,
arcDegrees: 360,
rotateDuplicates: true
}, %)

View File

@ -35,7 +35,7 @@ example = extrude(-5, exampleSketch)
|> patternCircular3d({
axis: [1, -1, 0],
center: [10, -20, 0],
instances: 11,
repetitions: 10,
arcDegrees: 360,
rotateDuplicates: true
}, %)

View File

@ -32,7 +32,7 @@ exampleSketch = startSketchOn('XZ')
|> circle({ center: [0, 0], radius: 1 }, %)
|> patternLinear2d({
axis: [1, 0],
instances: 7,
repetitions: 6,
distance: 4
}, %)

View File

@ -38,7 +38,7 @@ exampleSketch = startSketchOn('XZ')
example = extrude(1, exampleSketch)
|> patternLinear3d({
axis: [1, 0, 1],
instances: 7,
repetitions: 6,
distance: 6
}, %)
```

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -82,78 +82,6 @@ Raise a number to a power.
----
Are two numbers equal?
**enum:** `==`
----
Are two numbers not equal?
**enum:** `!=`
----
Is left greater than right
**enum:** `>`
----
Is left greater than or equal to right
**enum:** `>=`
----
Is left less than right
**enum:** `<`
----
Is left less than or equal to right
**enum:** `<=`
----

View File

@ -18,27 +18,6 @@ layout: manual
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `ImportStatement`| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `items` |`[` [`ImportItem`](/docs/kcl/types/ImportItem) `]`| | No |
| `path` |`string`| | No |
| `raw_path` |`string`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
@ -66,7 +45,6 @@ layout: manual
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `declarations` |`[` [`VariableDeclarator`](/docs/kcl/types/VariableDeclarator) `]`| | No |
| `visibility` |[`ItemVisibility`](/docs/kcl/types/ItemVisibility)| | No |
| `kind` |[`VariableKind`](/docs/kcl/types/VariableKind)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |

View File

@ -16,7 +16,7 @@ Data for a circular pattern on a 2D sketch.
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `instances` |[`Uint`](/docs/kcl/types/Uint)| The number of total instances. Must be greater than or equal to 1. This includes the original entity. For example, if instances is 2, there will be two copies -- the original, and one new copy. If instances is 1, this has no effect. | No |
| `repetitions` |[`Uint`](/docs/kcl/types/Uint)| The number of repetitions. Must be greater than 0. This excludes the original entity. For example, if `repetitions` is 1, the original entity will be copied once. | No |
| `center` |`[number, number]`| The center about which to make the pattern. This is a 2D vector. | No |
| `arcDegrees` |`number`| The arc angle (in degrees) to place the repetitions. Must be greater than 0. | No |
| `rotateDuplicates` |`boolean`| Whether or not to rotate the duplicates as they are copied. | No |

View File

@ -16,7 +16,7 @@ Data for a circular pattern on a 3D model.
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `instances` |[`Uint`](/docs/kcl/types/Uint)| The number of total instances. Must be greater than or equal to 1. This includes the original entity. For example, if instances is 2, there will be two copies -- the original, and one new copy. If instances is 1, this has no effect. | No |
| `repetitions` |[`Uint`](/docs/kcl/types/Uint)| The number of repetitions. Must be greater than 0. This excludes the original entity. For example, if `repetitions` is 1, the original entity will be copied once. | No |
| `axis` |`[number, number, number]`| The axis around which to make the pattern. This is a 3D vector. | No |
| `center` |`[number, number, number]`| The center about which to make the pattern. This is a 3D vector. | No |
| `arcDegrees` |`number`| The arc angle (in degrees) to place the repetitions. Must be greater than 0. | No |

View File

@ -197,27 +197,6 @@ An expression can be evaluated to yield a single KCL value.
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `ArrayRangeExpression`| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `startElement` |[`Expr`](/docs/kcl/types/Expr)| An expression can be evaluated to yield a single KCL value. | No |
| `endElement` |[`Expr`](/docs/kcl/types/Expr)| An expression can be evaluated to yield a single KCL value. | No |
| `endInclusive` |`boolean`| Is the `end_element` included in the range? | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |

View File

@ -1,24 +0,0 @@
---
title: "ImportItem"
excerpt: ""
layout: manual
---
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `name` |[`Identifier`](/docs/kcl/types/Identifier)| Name of the item to import. | No |
| `alias` |[`Identifier`](/docs/kcl/types/Identifier)| Rename the item using an identifier after "as". | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |

View File

@ -1,16 +0,0 @@
---
title: "ItemVisibility"
excerpt: ""
layout: manual
---
**enum:** `default`, `export`

View File

@ -1,10 +1,10 @@
---
title: "KclValue"
excerpt: "Any KCL value."
excerpt: "A memory item."
layout: manual
---
Any KCL value.
A memory item.
@ -80,7 +80,7 @@ A plane.
|----------|------|-------------|----------|
| `type` |enum: `Plane`| | No |
| `id` |`string`| The id of the plane. | No |
| `value` |[`PlaneType`](/docs/kcl/types/PlaneType)| Any KCL value. | No |
| `value` |[`PlaneType`](/docs/kcl/types/PlaneType)| A memory item. | No |
| `origin` |[`Point3d`](/docs/kcl/types/Point3d)| Origin of the plane. | No |
| `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the planes X axis be? | No |
| `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the planes Y axis be? | No |
@ -183,8 +183,8 @@ Data for an imported geometry.
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Function`| | No |
| `expression` |[`FunctionExpression`](/docs/kcl/types/FunctionExpression)| Any KCL value. | No |
| `memory` |[`ProgramMemory`](/docs/kcl/types/ProgramMemory)| Any KCL value. | No |
| `expression` |[`FunctionExpression`](/docs/kcl/types/FunctionExpression)| A memory item. | No |
| `memory` |[`ProgramMemory`](/docs/kcl/types/ProgramMemory)| A memory item. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |

View File

@ -16,7 +16,7 @@ Data for a linear pattern on a 2D sketch.
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `instances` |[`Uint`](/docs/kcl/types/Uint)| The number of total instances. Must be greater than or equal to 1. This includes the original entity. For example, if instances is 2, there will be two copies -- the original, and one new copy. If instances is 1, this has no effect. | No |
| `repetitions` |[`Uint`](/docs/kcl/types/Uint)| The number of repetitions. Must be greater than 0. This excludes the original entity. For example, if `repetitions` is 1, the original entity will be copied once. | No |
| `distance` |`number`| The distance between each repetition. This can also be referred to as spacing. | No |
| `axis` |`[number, number]`| The axis of the pattern. This is a 2D vector. | No |

View File

@ -16,7 +16,7 @@ Data for a linear pattern on a 3D model.
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `instances` |[`Uint`](/docs/kcl/types/Uint)| The number of total instances. Must be greater than or equal to 1. This includes the original entity. For example, if instances is 2, there will be two copies -- the original, and one new copy. If instances is 1, this has no effect. | No |
| `repetitions` |[`Uint`](/docs/kcl/types/Uint)| The number of repetitions. Must be greater than 0. This excludes the original entity. For example, if `repetitions` is 1, the original entity will be copied once. | No |
| `distance` |`number`| The distance between each repetition. This can also be referred to as spacing. | No |
| `axis` |`[number, number, number]`| The axis of the pattern. | No |

View File

@ -313,45 +313,3 @@ test(
await electronApp.close()
}
)
test(
'external change of file contents are reflected in editor',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
const PROJECT_DIR_NAME = 'lee-was-here'
const {
electronApp,
page,
dir: projectsDir,
} = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
const aProjectDir = join(dir, PROJECT_DIR_NAME)
await fsp.mkdir(aProjectDir, { recursive: true })
},
})
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await test.step('Open the project', async () => {
await expect(page.getByText(PROJECT_DIR_NAME)).toBeVisible()
await page.getByText(PROJECT_DIR_NAME).click()
await u.waitForPageLoad()
})
await u.openFilePanel()
await u.openKclCodePanel()
await test.step('Write to file externally and check for changed content', async () => {
const content = 'ha he ho ho ha blap scap be dap'
await fsp.writeFile(
join(projectsDir, PROJECT_DIR_NAME, 'main.kcl'),
content
)
await u.editorTextMatches(content)
})
await electronApp.close()
}
)

View File

@ -1,80 +0,0 @@
import { test, expect } from '@playwright/test'
import { getUtils, setup, tearDown } from './test-utils'
test.beforeEach(async ({ context, page }, testInfo) => {
await setup(context, page, testInfo)
})
test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
function countNewlines(input: string): number {
let count = 0
for (const char of input) {
if (char === '\n') {
count++
}
}
return count
}
test.describe('Debug pane', () => {
test('Artifact IDs in the artifact graph are stable across code edits', async ({
page,
context,
}) => {
const code = `sketch001 = startSketchOn('XZ')
|> startProfileAt([0, 0], %)
|> line([1, 1], %)
`
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
const tree = page.getByTestId('debug-feature-tree')
const segment = tree.locator('li', {
hasText: 'segIds:',
hasNotText: 'paths:',
})
await test.step('Test setup', async () => {
await u.waitForAuthSkipAppStart()
await u.openKclCodePanel()
await u.openDebugPanel()
// Set the code in the code editor.
await u.codeLocator.click()
await page.keyboard.type(code, { delay: 0 })
// Scroll to the feature tree.
await tree.scrollIntoViewIfNeeded()
// Expand the feature tree.
await tree.getByText('Feature Tree').click()
// Just expanded the details, making the element taller, so scroll again.
await tree.getByText('Plane').first().scrollIntoViewIfNeeded()
})
// Extract the artifact IDs from the debug feature tree.
const initialSegmentIds = await segment.innerText({ timeout: 5_000 })
// The artifact ID should include a UUID.
expect(initialSegmentIds).toMatch(
/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/
)
await test.step('Move cursor to the bottom of the code editor', async () => {
// Focus on the code editor.
await u.codeLocator.click()
// Make sure the cursor is at the end of the code.
const lines = countNewlines(code) + 1
for (let i = 0; i < lines; i++) {
await page.keyboard.press('ArrowDown')
}
})
await test.step('Enter a comment', async () => {
await page.keyboard.type('|> line([2, 2], %)', { delay: 0 })
// Wait for keyboard input debounce and updated artifact graph.
await page.waitForTimeout(1000)
})
const newSegmentIds = await segment.innerText()
// Strip off the closing bracket.
const initialIds = initialSegmentIds.slice(0, initialSegmentIds.length - 1)
expect(newSegmentIds.slice(0, initialIds.length)).toEqual(initialIds)
})
})

View File

@ -104,7 +104,7 @@ test(
},
{ timeout: 15_000 }
)
.toBeGreaterThan(300_000)
.toBe(431341)
// clean up output.gltf
await fsp.rm('output.gltf')
@ -179,7 +179,7 @@ test(
},
{ timeout: 15_000 }
)
.toBeGreaterThan(100_000)
.toBe(102040)
// clean up output.gltf
await fsp.rm('output.gltf')

View File

@ -1,16 +1,6 @@
import { test, expect } from '@playwright/test'
import fsp from 'fs/promises'
import { uuidv4 } from 'lib/utils'
import {
darkModeBgColor,
darkModePlaneColorXZ,
executorInputPath,
getUtils,
setup,
setupElectron,
tearDown,
} from './test-utils'
import { join } from 'path'
import { getUtils, setup, tearDown } from './test-utils'
test.beforeEach(async ({ context, page }, testInfo) => {
await setup(context, page, testInfo)
@ -984,84 +974,4 @@ test.describe('Editor tests', () => {
|> close(%)
|> extrude(5, %)`)
})
test(
`Can use the import stdlib function on a local OBJ file`,
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
const bracketDir = join(dir, 'cube')
await fsp.mkdir(bracketDir, { recursive: true })
await fsp.copyFile(
executorInputPath('cube.obj'),
join(bracketDir, 'cube.obj')
)
await fsp.writeFile(join(bracketDir, 'main.kcl'), '')
},
})
const viewportSize = { width: 1200, height: 500 }
await page.setViewportSize(viewportSize)
// Locators and constants
const u = await getUtils(page)
const projectLink = page.getByRole('link', { name: 'cube' })
const gizmo = page.locator('[aria-label*=gizmo]')
const resetCameraButton = page.getByRole('button', { name: 'Reset view' })
const locationToHavColor = async (
position: { x: number; y: number },
color: [number, number, number]
) => {
return u.getGreatestPixDiff(position, color)
}
const notTheOrigin = {
x: viewportSize.width * 0.55,
y: viewportSize.height * 0.3,
}
const origin = { x: viewportSize.width / 2, y: viewportSize.height / 2 }
const errorIndicators = page.locator('.cm-lint-marker-error')
await test.step(`Open the empty file, see the default planes`, async () => {
await projectLink.click()
await u.waitForPageLoad()
await expect
.poll(
async () => locationToHavColor(notTheOrigin, darkModePlaneColorXZ),
{
timeout: 5000,
message: 'XZ plane color is visible',
}
)
.toBeLessThan(15)
})
await test.step(`Write the import function line`, async () => {
await u.codeLocator.fill(`import('cube.obj')`)
await page.waitForTimeout(800)
})
await test.step(`Reset the camera before checking`, async () => {
await u.doAndWaitForCmd(async () => {
await gizmo.click({ button: 'right' })
await resetCameraButton.click()
}, 'zoom_to_fit')
})
await test.step(`Verify that we see the imported geometry and no errors`, async () => {
await expect(errorIndicators).toHaveCount(0)
await expect
.poll(async () => locationToHavColor(origin, darkModePlaneColorXZ), {
timeout: 3000,
message: 'Plane color should not be visible',
})
.toBeGreaterThan(15)
await expect
.poll(async () => locationToHavColor(origin, darkModeBgColor), {
timeout: 3000,
message: 'Background color should not be visible',
})
.toBeGreaterThan(15)
})
await electronApp.close()
}
)
})

View File

@ -136,9 +136,6 @@ test.describe('when using the file tree to', () => {
)
await pasteCodeInEditor(kclCube)
// TODO: We have a timeout of 1s between edits to write to disk. If you reload the page too quickly it won't write to disk.
await tronApp.page.waitForTimeout(2000)
await renameFile(fromFile, toFile)
await tronApp.page.reload()
@ -225,11 +222,9 @@ test.describe('when using the file tree to', () => {
)
await pasteCodeInEditor(kclCube)
// TODO: We have a timeout of 1s between edits to write to disk. If you reload the page too quickly it won't write to disk.
await tronApp.page.waitForTimeout(2000)
const kcl1 = 'main.kcl'
const kcl2 = '2.kcl'
await createNewFileAndSelect(kcl2)
const kclCylinder = await fsp.readFile(
'src/wasm-lib/tests/executor/inputs/cylinder.kcl',
@ -237,9 +232,6 @@ test.describe('when using the file tree to', () => {
)
await pasteCodeInEditor(kclCylinder)
// TODO: We have a timeout of 1s between edits to write to disk. If you reload the page too quickly it won't write to disk.
await tronApp.page.waitForTimeout(2000)
await renameFile(kcl2, kcl1)
await test.step(`Postcondition: ${kcl1} still has the original content`, async () => {
@ -968,171 +960,4 @@ _test.describe('Deleting items from the file pane', () => {
'TODO - delete folder we are in, with no main.kcl',
async () => {}
)
// Copied from tests above.
_test(
`external deletion of project navigates back home`,
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
const TEST_PROJECT_NAME = 'Test Project'
const {
electronApp,
page,
dir: projectsDirName,
} = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
await fsp.mkdir(join(dir, TEST_PROJECT_NAME), { recursive: true })
await fsp.mkdir(join(dir, TEST_PROJECT_NAME, 'folderToDelete'), {
recursive: true,
})
await fsp.copyFile(
executorInputPath('basic_fillet_cube_end.kcl'),
join(dir, TEST_PROJECT_NAME, 'main.kcl')
)
await fsp.copyFile(
executorInputPath('cylinder.kcl'),
join(dir, TEST_PROJECT_NAME, 'folderToDelete', 'someFileWithin.kcl')
)
},
})
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
// Constants and locators
const projectCard = page.getByText(TEST_PROJECT_NAME)
const projectMenuButton = page.getByTestId('project-sidebar-toggle')
const folderToDelete = page.getByRole('button', {
name: 'folderToDelete',
})
const fileWithinFolder = page.getByRole('listitem').filter({
has: page.getByRole('button', { name: 'someFileWithin.kcl' }),
})
await _test.step(
'Open project and navigate into folderToDelete',
async () => {
await projectCard.click()
await u.waitForPageLoad()
await _expect(projectMenuButton).toContainText('main.kcl')
await u.closeKclCodePanel()
await u.openFilePanel()
await folderToDelete.click()
await _expect(fileWithinFolder).toBeVisible()
await fileWithinFolder.click()
await _expect(projectMenuButton).toContainText('someFileWithin.kcl')
}
)
// Point of divergence. Delete the project folder and see if it goes back
// to the home view.
await _test.step(
'Delete projectsDirName/<project-name> externally',
async () => {
await fsp.rm(join(projectsDirName, TEST_PROJECT_NAME), {
recursive: true,
force: true,
})
}
)
await _test.step('Check the app is back on the home view', async () => {
const projectsDirLink = page.getByText('Loaded from')
await _expect(projectsDirLink).toBeVisible()
})
await electronApp.close()
}
)
// Similar to the above
_test(
`external deletion of file in sub-directory updates the file tree and recreates it on code editor typing`,
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
const TEST_PROJECT_NAME = 'Test Project'
const {
electronApp,
page,
dir: projectsDirName,
} = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
await fsp.mkdir(join(dir, TEST_PROJECT_NAME), { recursive: true })
await fsp.mkdir(join(dir, TEST_PROJECT_NAME, 'folderToDelete'), {
recursive: true,
})
await fsp.copyFile(
executorInputPath('basic_fillet_cube_end.kcl'),
join(dir, TEST_PROJECT_NAME, 'main.kcl')
)
await fsp.copyFile(
executorInputPath('cylinder.kcl'),
join(dir, TEST_PROJECT_NAME, 'folderToDelete', 'someFileWithin.kcl')
)
},
})
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
// Constants and locators
const projectCard = page.getByText(TEST_PROJECT_NAME)
const projectMenuButton = page.getByTestId('project-sidebar-toggle')
const folderToDelete = page.getByRole('button', {
name: 'folderToDelete',
})
const fileWithinFolder = page.getByRole('listitem').filter({
has: page.getByRole('button', { name: 'someFileWithin.kcl' }),
})
await _test.step(
'Open project and navigate into folderToDelete',
async () => {
await projectCard.click()
await u.waitForPageLoad()
await _expect(projectMenuButton).toContainText('main.kcl')
await u.openFilePanel()
await folderToDelete.click()
await _expect(fileWithinFolder).toBeVisible()
await fileWithinFolder.click()
await _expect(projectMenuButton).toContainText('someFileWithin.kcl')
}
)
await _test.step(
'Delete projectsDirName/<project-name> externally',
async () => {
await fsp.rm(
join(
projectsDirName,
TEST_PROJECT_NAME,
'folderToDelete',
'someFileWithin.kcl'
)
)
}
)
await _test.step('Check the file is gone in the file tree', async () => {
await _expect(
page.getByTestId('file-pane-scroll-container')
).not.toContainText('someFileWithin.kcl')
})
await _test.step(
'Check the file is back in the file tree after typing in code editor',
async () => {
await u.pasteCodeInEditor('hello = 1')
await _expect(
page.getByTestId('file-pane-scroll-container')
).toContainText('someFileWithin.kcl')
}
)
await electronApp.close()
}
)
})

View File

@ -13,13 +13,6 @@ type mouseParams = {
pixelDiff: number
}
type SceneSerialised = {
camera: {
position: [number, number, number]
target: [number, number, number]
}
}
export class SceneFixture {
public page: Page
@ -29,22 +22,6 @@ export class SceneFixture {
this.page = page
this.reConstruct(page)
}
private _serialiseScene = async (): Promise<SceneSerialised> => {
const camera = await this.getCameraInfo()
return {
camera,
}
}
expectState = async (expected: SceneSerialised) => {
return expect
.poll(() => this._serialiseScene(), {
message: `Expected scene state to match`,
})
.toEqual(expected)
}
reConstruct = (page: Page) => {
this.page = page
@ -54,7 +31,7 @@ export class SceneFixture {
makeMouseHelpers = (
x: number,
y: number,
{ steps }: { steps: number } = { steps: 20 }
{ steps }: { steps: number } = { steps: 5000 }
) =>
[
(clickParams?: mouseParams) => {
@ -110,36 +87,6 @@ export class SceneFixture {
)
await closeDebugPanel(this.page)
}
/** Forces a refresh of the camera position and target displayed
* in the debug panel and then returns the values of the fields
*/
async getCameraInfo() {
await openAndClearDebugPanel(this.page)
await sendCustomCmd(this.page, {
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
})
await this.waitForExecutionDone()
const position = await Promise.all([
this.page.getByTestId('cam-x-position').inputValue().then(Number),
this.page.getByTestId('cam-y-position').inputValue().then(Number),
this.page.getByTestId('cam-z-position').inputValue().then(Number),
])
const target = await Promise.all([
this.page.getByTestId('cam-x-target').inputValue().then(Number),
this.page.getByTestId('cam-y-target').inputValue().then(Number),
this.page.getByTestId('cam-z-target').inputValue().then(Number),
])
await closeDebugPanel(this.page)
return {
position,
target,
}
}
waitForExecutionDone = async () => {
await expect(this.exeIndicator).toBeVisible()
}
@ -167,17 +114,4 @@ export class SceneFixture {
)
})
}
get gizmo() {
return this.page.locator('[aria-label*=gizmo]')
}
async clickGizmoMenuItem(name: string) {
await this.gizmo.click({ button: 'right' })
const buttonToTest = this.page.getByRole('button', {
name: name,
})
await expect(buttonToTest).toBeVisible()
await buttonToTest.click()
}
}

View File

@ -1,4 +1,4 @@
import { test, expect } from '@playwright/test'
import { test, expect, Page } from '@playwright/test'
import {
doExport,
executorInputPath,
@ -255,7 +255,7 @@ test.describe('Can export from electron app', () => {
},
{ timeout: 15_000 }
)
.toBeGreaterThan(300_000)
.toBe(431341)
// clean up output.gltf
await fsp.rm('output.gltf')
@ -618,30 +618,31 @@ test(
'Deleting projects, can delete individual project, can still create projects after deleting all',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
const projectData = [
['router-template-slate', 'cylinder.kcl'],
['bracket', 'focusrite_scarlett_mounting_braket.kcl'],
['lego', 'lego.kcl'],
]
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
// Do these serially to ensure the order is correct
for (const [name, file] of projectData) {
await fsp.mkdir(join(dir, name), { recursive: true })
await fsp.copyFile(
executorInputPath(file),
join(dir, name, `main.kcl`)
)
// Wait 1s between each project to ensure the order is correct
await new Promise((r) => setTimeout(r, 1_000))
}
},
})
await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log)
const createProjectAndRenameItTest = async ({
name,
page,
}: {
name: string
page: Page
}) => {
await test.step(`Create and rename project ${name}`, async () => {
await createProjectAndRenameIt({ name, page })
})
}
// we need to create the folders so that the order is correct
// creating them ahead of time with fs tools means they all have the same timestamp
await createProjectAndRenameItTest({ name: 'router-template-slate', page })
await createProjectAndRenameItTest({ name: 'bracket', page })
await createProjectAndRenameItTest({ name: 'lego', page })
await test.step('delete the middle project, i.e. the bracket project', async () => {
const project = page.getByText('bracket')
@ -743,26 +744,8 @@ test(
'Can sort projects on home page',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
const projectData = [
['router-template-slate', 'cylinder.kcl'],
['bracket', 'focusrite_scarlett_mounting_braket.kcl'],
['lego', 'lego.kcl'],
]
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
// Do these serially to ensure the order is correct
for (const [name, file] of projectData) {
await fsp.mkdir(join(dir, name), { recursive: true })
await fsp.copyFile(
executorInputPath(file),
join(dir, name, `main.kcl`)
)
// Wait 1s between each project to ensure the order is correct
await new Promise((r) => setTimeout(r, 1_000))
}
},
})
await page.setViewportSize({ width: 1200, height: 500 })
@ -770,6 +753,24 @@ test(
page.on('console', console.log)
const createProjectAndRenameItTest = async ({
name,
page,
}: {
name: string
page: Page
}) => {
await test.step(`Create and rename project ${name}`, async () => {
await createProjectAndRenameIt({ name, page })
})
}
// we need to create the folders so that the order is correct
// creating them ahead of time with fs tools means they all have the same timestamp
await createProjectAndRenameItTest({ name: 'router-template-slate', page })
await createProjectAndRenameItTest({ name: 'bracket', page })
await createProjectAndRenameItTest({ name: 'lego', page })
await test.step('should be shorted by modified initially', async () => {
const lastModifiedButton = page.getByRole('button', {
name: 'Last Modified',
@ -851,7 +852,7 @@ test(
}
)
test.fixme(
test(
'When the project folder is empty, user can create new project and open it.',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
@ -861,12 +862,6 @@ test.fixme(
page.on('console', console.log)
// Locators and constants
const gizmo = page.locator('[aria-label*=gizmo]')
const resetCameraButton = page.getByRole('button', { name: 'Reset view' })
const pointOnModel = { x: 660, y: 250 }
const expectedStartCamZPosition = 15633.47
// expect to see text "No Projects found"
await expect(page.getByText('No Projects found')).toBeVisible()
@ -879,7 +874,16 @@ test.fixme(
await page.getByText('project-000').click()
await u.waitForPageLoad()
await expect(page.getByTestId('loading')).toBeAttached()
await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
})
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeEnabled({
timeout: 20_000,
})
await page.locator('.cm-content').fill(`sketch001 = startSketchOn('XZ')
|> startProfileAt([-87.4, 282.92], %)
@ -889,28 +893,8 @@ test.fixme(
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
extrude001 = extrude(200, sketch001)`)
await page.waitForTimeout(800)
async function getCameraZValue() {
return page
.getByTestId('cam-z-position')
.inputValue()
.then((value) => parseFloat(value))
}
await test.step(`Reset camera`, async () => {
await u.openDebugPanel()
await u.clearCommandLogs()
await u.doAndWaitForCmd(async () => {
await gizmo.click({ button: 'right' })
await resetCameraButton.click()
}, 'zoom_to_fit')
await expect
.poll(getCameraZValue, {
message: 'Camera Z should be at expected position after reset',
})
.toEqual(expectedStartCamZPosition)
})
const pointOnModel = { x: 660, y: 250 }
// gray at this pixel means the stream has loaded in the most
// user way we can verify it (pixel color)
@ -918,7 +902,7 @@ extrude001 = extrude(200, sketch001)`)
.poll(() => u.getGreatestPixDiff(pointOnModel, [143, 143, 143]), {
timeout: 10_000,
})
.toBeLessThan(30)
.toBeLessThan(15)
await expect(async () => {
await page.mouse.move(0, 0, { steps: 5 })

View File

@ -471,7 +471,7 @@ test(
await page.mouse.move(startXPx + PUR * 30, 500 - PUR * 20, { steps: 10 })
await page.waitForTimeout(1000)
await page.waitForTimeout(300)
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
@ -521,6 +521,7 @@ test(
const startXPx = 600
// Equip the rectangle tool
await page.getByRole('button', { name: 'line Line', exact: true }).click()
await page
.getByRole('button', { name: 'rectangle Corner rectangle', exact: true })
.click()
@ -528,7 +529,6 @@ test(
// Draw the rectangle
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 30)
await page.mouse.move(startXPx + PUR * 10, 500 - PUR * 10, { steps: 5 })
await page.waitForTimeout(800)
// Ensure the draft rectangle looks the same as it usually does
await expect(page).toHaveScreenshot({
@ -670,7 +670,6 @@ test.describe(
// screen shot should show the sketch
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
mask: [page.getByTestId('model-state-indicator')],
})
// exit sketch
@ -688,7 +687,6 @@ test.describe(
// second screen shot should look almost identical, i.e. scale should be the same.
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
mask: [page.getByTestId('model-state-indicator')],
})
})
@ -896,7 +894,7 @@ test(
// Wait for the second extrusion to appear
// TODO: Find a way to truly know that the objects have finished
// rendering, because an execution-done message is not sufficient.
await page.waitForTimeout(2000)
await page.waitForTimeout(1000)
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
@ -940,7 +938,7 @@ test(
// Wait for the second extrusion to appear
// TODO: Find a way to truly know that the objects have finished
// rendering, because an execution-done message is not sufficient.
await page.waitForTimeout(2000)
await page.waitForTimeout(1000)
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

@ -47,14 +47,6 @@ export const commonPoints = {
num2: 14.44,
}
/** A semi-reliable color to check the default XZ plane on
* in dark mode in the default camera position
*/
export const darkModePlaneColorXZ: [number, number, number] = [50, 50, 99]
/** A semi-reliable color to check the default dark mode bg color against */
export const darkModeBgColor: [number, number, number] = [27, 27, 27]
export const editorSelector = '[role="textbox"][data-language="kcl"]'
type PaneId = 'variables' | 'code' | 'files' | 'logs'
@ -471,9 +463,6 @@ export async function getUtils(page: Page, test_?: typeof test) {
return test_?.step(
`Create and select project with text "${hasText}"`,
async () => {
// Without this, we get unreliable project creation. It's probably
// due to a race between the FS being read and clicking doing something.
await page.waitForTimeout(100)
await page.getByTestId('home-new-file').click()
const projectLinksPost = page.getByTestId('project-link')
await projectLinksPost.filter({ hasText }).click()
@ -503,11 +492,6 @@ export async function getUtils(page: Page, test_?: typeof test) {
createNewFile: async (name: string) => {
return test?.step(`Create a file named ${name}`, async () => {
// If the application is in the middle of connecting a stream
// then creating a new file won't work in the end.
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
await page.getByTestId('create-file-button').click()
await page.getByTestId('file-rename-field').fill(name)
await page.keyboard.press('Enter')
@ -890,8 +874,8 @@ export async function setupElectron({
appSettings
? { settings: appSettings }
: {
...TEST_SETTINGS,
settings: {
...TEST_SETTINGS,
app: {
...TEST_SETTINGS.app,
projectDirectory: projectDirName,

View File

@ -1,18 +1,18 @@
import { _test, _expect } from './playwright-deprecated'
import { test } from './fixtures/fixtureSetup'
import { test, expect } from '@playwright/test'
import { getUtils, setup, tearDown } from './test-utils'
import { uuidv4 } from 'lib/utils'
import { TEST_CODE_GIZMO } from './storageStates'
_test.beforeEach(async ({ context, page }, testInfo) => {
test.beforeEach(async ({ context, page }, testInfo) => {
await setup(context, page, testInfo)
})
_test.afterEach(async ({ page }, testInfo) => {
test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
_test.describe('Testing Gizmo', () => {
test.describe('Testing Gizmo', () => {
const cases = [
{
testDescription: 'top view',
@ -57,7 +57,7 @@ _test.describe('Testing Gizmo', () => {
expectedCameraTarget,
testDescription,
} of cases) {
_test(`check ${testDescription}`, async ({ page, browserName }) => {
test(`check ${testDescription}`, async ({ page, browserName }) => {
const u = await getUtils(page)
await page.addInitScript((TEST_CODE_GIZMO) => {
localStorage.setItem('persistCode', TEST_CODE_GIZMO)
@ -117,30 +117,30 @@ _test.describe('Testing Gizmo', () => {
await Promise.all([
// position
_expect(page.getByTestId('cam-x-position')).toHaveValue(
expect(page.getByTestId('cam-x-position')).toHaveValue(
expectedCameraPosition.x.toString()
),
_expect(page.getByTestId('cam-y-position')).toHaveValue(
expect(page.getByTestId('cam-y-position')).toHaveValue(
expectedCameraPosition.y.toString()
),
_expect(page.getByTestId('cam-z-position')).toHaveValue(
expect(page.getByTestId('cam-z-position')).toHaveValue(
expectedCameraPosition.z.toString()
),
// target
_expect(page.getByTestId('cam-x-target')).toHaveValue(
expect(page.getByTestId('cam-x-target')).toHaveValue(
expectedCameraTarget.x.toString()
),
_expect(page.getByTestId('cam-y-target')).toHaveValue(
expect(page.getByTestId('cam-y-target')).toHaveValue(
expectedCameraTarget.y.toString()
),
_expect(page.getByTestId('cam-z-target')).toHaveValue(
expect(page.getByTestId('cam-z-target')).toHaveValue(
expectedCameraTarget.z.toString()
),
])
})
}
_test('Context menu and popover menu', async ({ page }) => {
test('Context menu and popover menu', async ({ page }) => {
const testCase = {
testDescription: 'Right view',
expectedCameraPosition: { x: 5660.02, y: -152, z: 26 },
@ -196,7 +196,7 @@ _test.describe('Testing Gizmo', () => {
const buttonToTest = page.getByRole('button', {
name: testCase.testDescription,
})
await _expect(buttonToTest).toBeVisible()
await expect(buttonToTest).toBeVisible()
await buttonToTest.click()
// Now assert we've moved to the correct view
@ -215,23 +215,23 @@ _test.describe('Testing Gizmo', () => {
await Promise.all([
// position
_expect(page.getByTestId('cam-x-position')).toHaveValue(
expect(page.getByTestId('cam-x-position')).toHaveValue(
testCase.expectedCameraPosition.x.toString()
),
_expect(page.getByTestId('cam-y-position')).toHaveValue(
expect(page.getByTestId('cam-y-position')).toHaveValue(
testCase.expectedCameraPosition.y.toString()
),
_expect(page.getByTestId('cam-z-position')).toHaveValue(
expect(page.getByTestId('cam-z-position')).toHaveValue(
testCase.expectedCameraPosition.z.toString()
),
// target
_expect(page.getByTestId('cam-x-target')).toHaveValue(
expect(page.getByTestId('cam-x-target')).toHaveValue(
testCase.expectedCameraTarget.x.toString()
),
_expect(page.getByTestId('cam-y-target')).toHaveValue(
expect(page.getByTestId('cam-y-target')).toHaveValue(
testCase.expectedCameraTarget.y.toString()
),
_expect(page.getByTestId('cam-z-target')).toHaveValue(
expect(page.getByTestId('cam-z-target')).toHaveValue(
testCase.expectedCameraTarget.z.toString()
),
])
@ -242,60 +242,8 @@ _test.describe('Testing Gizmo', () => {
const gizmoPopoverButton = page.getByRole('button', {
name: 'view settings',
})
await _expect(gizmoPopoverButton).toBeVisible()
await expect(gizmoPopoverButton).toBeVisible()
await gizmoPopoverButton.click()
await _expect(buttonToTest).toBeVisible()
})
})
test.describe(`Testing gizmo, fixture-based`, () => {
test('Center on selection from menu', async ({
app,
cmdBar,
editor,
toolbar,
scene,
}) => {
test.skip(
process.platform === 'win32',
'Fails on windows in CI, can not be replicated locally on windows.'
)
await test.step(`Setup`, async () => {
const file = await app.getInputFile('test-circle-extrude.kcl')
await app.initialise(file)
await scene.expectState({
camera: {
position: [4982.21, -23865.37, 13810.64],
target: [4982.21, 0, 2737.1],
},
})
})
const [clickCircle, moveToCircle] = scene.makeMouseHelpers(582, 217)
await test.step(`Select an edge of this circle`, async () => {
const circleSnippet =
'circle({ center: [318.33, 168.1], radius: 182.8 }, %)'
await moveToCircle()
await clickCircle()
await editor.expectState({
activeLines: [circleSnippet.slice(-5)],
highlightedCode: circleSnippet,
diagnostics: [],
})
})
await test.step(`Center on selection from menu`, async () => {
await scene.clickGizmoMenuItem('Center view on selection')
})
await test.step(`Verify the camera moved`, async () => {
await scene.expectState({
camera: {
position: [0, -23865.37, 11073.54],
target: [0, 0, 0],
},
})
})
await expect(buttonToTest).toBeVisible()
})
})

View File

@ -1208,12 +1208,6 @@ extrude001 = extrude(50, sketch001)
test('Deselecting line tool should mean nothing happens on click', async ({
page,
}) => {
/**
* If the line tool is clicked when the state is 'No Points' it will exit Sketch mode.
* This is the same exact workflow as pressing ESC.
*
* To continue to test this workflow, we now enter sketch mode and place a single point before exiting the line tool.
*/
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
@ -1234,7 +1228,6 @@ extrude001 = extrude(50, sketch001)
200
)
// Clicks the XZ Plane in the page
await page.mouse.click(700, 200)
await expect(page.locator('.cm-content')).toHaveText(
@ -1243,11 +1236,6 @@ extrude001 = extrude(50, sketch001)
await page.waitForTimeout(600)
// Place a point because the line tool will exit if no points are pressed
await page.mouse.click(650, 200)
await page.waitForTimeout(600)
// Code before exiting the tool
let previousCodeContent = await page.locator('.cm-content').innerText()
// deselect the line tool by clicking it

View File

@ -9,7 +9,6 @@ import {
executorInputPath,
} from './test-utils'
import { SaveSettingsPayload, SettingsLevel } from 'lib/settings/settingsTypes'
import { SETTINGS_FILE_NAME, PROJECT_SETTINGS_FILE_NAME } from 'lib/constants'
import {
TEST_SETTINGS_KEY,
TEST_SETTINGS_CORRUPTED,
@ -344,7 +343,7 @@ test.describe('Testing settings', () => {
// Selectors and constants
const errorHeading = page.getByRole('heading', {
name: 'An unexpected error occurred',
name: 'An unextected error occurred',
})
const projectDirLink = page.getByText('Loaded from')
@ -373,7 +372,7 @@ test.describe('Testing settings', () => {
// Selectors and constants
const errorHeading = page.getByRole('heading', {
name: 'An unexpected error occurred',
name: 'An unextected error occurred',
})
const projectDirLink = page.getByText('Loaded from')
@ -385,118 +384,6 @@ test.describe('Testing settings', () => {
}
)
// It was much easier to test the logo color than the background stream color.
test(
'user settings reload on external change, on project and modeling view',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
const {
electronApp,
page,
dir: projectDirName,
} = await setupElectron({
testInfo,
appSettings: {
app: {
// Doesn't matter what you set it to. It will
// default to 264.5
themeColor: '0',
},
},
})
await page.setViewportSize({ width: 1200, height: 500 })
const logoLink = page.getByTestId('app-logo')
const projectDirLink = page.getByText('Loaded from')
await test.step('Wait for project view', async () => {
await expect(projectDirLink).toBeVisible()
await expect(logoLink).toHaveCSS('--primary-hue', '264.5')
})
const changeColor = async (color: string) => {
const tempSettingsFilePath = join(projectDirName, SETTINGS_FILE_NAME)
let tomlStr = await fsp.readFile(tempSettingsFilePath, 'utf-8')
tomlStr = tomlStr.replace(/(themeColor = ")[0-9]+(")/, `$1${color}$2`)
await fsp.writeFile(tempSettingsFilePath, tomlStr)
}
await test.step('Check color of logo changed', async () => {
await changeColor('99')
await expect(logoLink).toHaveCSS('--primary-hue', '99')
})
await test.step('Check color of logo changed when in modeling view', async () => {
await page.getByRole('button', { name: 'New project' }).click()
await page.getByTestId('project-link').first().click()
await page.getByRole('button', { name: 'Dismiss' }).click()
await changeColor('58')
await expect(logoLink).toHaveCSS('--primary-hue', '58')
})
await test.step('Check going back to projects view still changes the color', async () => {
await logoLink.click()
await expect(projectDirLink).toBeVisible()
await changeColor('21')
await expect(logoLink).toHaveCSS('--primary-hue', '21')
})
await electronApp.close()
}
)
test(
'project settings reload on external change',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
const {
electronApp,
page,
dir: projectDirName,
} = await setupElectron({
testInfo,
})
await page.setViewportSize({ width: 1200, height: 500 })
const logoLink = page.getByTestId('app-logo')
const projectDirLink = page.getByText('Loaded from')
await test.step('Wait for project view', async () => {
await expect(projectDirLink).toBeVisible()
})
const projectLinks = page.getByTestId('project-link')
const oldCount = await projectLinks.count()
await page.getByRole('button', { name: 'New project' }).click()
await expect(projectLinks).toHaveCount(oldCount + 1)
await projectLinks.filter({ hasText: 'project-000' }).first().click()
const changeColorFs = async (color: string) => {
const tempSettingsFilePath = join(
projectDirName,
'project-000',
PROJECT_SETTINGS_FILE_NAME
)
await fsp.writeFile(
tempSettingsFilePath,
`[settings.app]\nthemeColor = "${color}"`
)
}
await test.step('Check the color is first starting as we expect', async () => {
await expect(logoLink).toHaveCSS('--primary-hue', '264.5')
})
await test.step('Check color of logo changed', async () => {
await changeColorFs('99')
await expect(logoLink).toHaveCSS('--primary-hue', '99')
})
await electronApp.close()
}
)
test(
`Closing settings modal should go back to the original file being viewed`,
{ tag: '@electron' },

View File

@ -32,10 +32,10 @@ win:
arch:
- x64
- arm64
# - target: msi
# arch:
# - x64
# - arm64
- target: msi
arch:
- x64
- arm64
signingHashAlgorithms:
- sha256
sign: "./sign-win.js"
@ -47,9 +47,9 @@ win:
mimeType: text/vnd.zoo.kcl
description: Zoo KCL File
role: Editor
# msi:
# oneClick: false
# perMachine: true
msi:
oneClick: false
perMachine: true
nsis:
oneClick: false
perMachine: true
@ -73,5 +73,3 @@ publish:
- provider: generic
url: https://dl.zoo.dev/releases/modeling-app
channel: latest
releaseInfo:
releaseNotesFile: release-notes.md

14
interface.d.ts vendored
View File

@ -20,11 +20,11 @@ export interface IElectronAPI {
version: typeof process.env.version
watchFileOn: (
path: string,
key: string,
callback: (eventType: string, path: string) => void
) => void
readFile: typeof fs.readFile
watchFileOff: (path: string, key: string) => void
watchFileOff: (path: string) => void
watchFileObliterate: () => void
readFile: (path: string) => ReturnType<fs.readFile>
writeFile: (
path: string,
data: string | Uint8Array
@ -68,15 +68,11 @@ export interface IElectronAPI {
}
}
kittycad: (access: string, args: any) => any
listMachines: (machineApiIp: string) => Promise<MachinesListing>
listMachines: () => Promise<MachinesListing>
getMachineApiIp: () => Promise<string | null>
onUpdateDownloadStart: (
callback: (value: { version: string }) => void
) => Electron.IpcRenderer
onUpdateDownloaded: (
callback: (value: { version: string; releaseNotes: string }) => void
callback: (value: string) => void
) => Electron.IpcRenderer
onUpdateError: (callback: (value: { error: Error }) => void) => Electron
appRestart: () => void
}

View File

@ -70,7 +70,7 @@ echo ""
echo "Suggested changelog:"
echo "\`\`\`"
echo "## What's Changed"
git log $(git describe --tags --match="v[0-9]*" --abbrev=0)..HEAD --oneline --pretty=format:%s | grep -v Bump | grep -v 'Cut release v' | awk '{print "* "toupper(substr($0,0,1))substr($0,2)}'
git log $(git describe --tags --abbrev=0)..HEAD --oneline --pretty=format:%s | grep -v Bump | grep -v 'Cut release v' | awk '{print "* "toupper(substr($0,0,1))substr($0,2)}'
echo ""
echo "**Full Changelog**: https://github.com/KittyCAD/modeling-app/compare/${latest_tag}...${new_version}"
echo "\`\`\`"

View File

@ -36,297 +36,38 @@
"description": "Extra machine-specific information regarding a connected machine.",
"oneOf": [
{
"additionalProperties": false,
"properties": {
"type": {
"enum": [
"moonraker"
],
"type": "string"
"Moonraker": {
"type": "object"
}
},
"required": [
"type"
"Moonraker"
],
"type": "object"
},
{
"additionalProperties": false,
"properties": {
"type": {
"enum": [
"usb"
],
"type": "string"
"Usb": {
"type": "object"
}
},
"required": [
"type"
"Usb"
],
"type": "object"
},
{
"additionalProperties": false,
"properties": {
"current_stage": {
"allOf": [
{
"$ref": "#/components/schemas/Stage"
}
],
"description": "The current stage of the machine as defined by Bambu which can include errors, etc.",
"nullable": true
},
"nozzle_diameter": {
"allOf": [
{
"$ref": "#/components/schemas/NozzleDiameter"
}
],
"description": "The nozzle diameter of the machine."
},
"type": {
"enum": [
"bambu"
],
"type": "string"
"Bambu": {
"type": "object"
}
},
"required": [
"nozzle_diameter",
"type"
],
"type": "object"
}
]
},
"FdmHardwareConfiguration": {
"description": "Configuration for a FDM-based printer.",
"properties": {
"filaments": {
"description": "The filaments the printer has access to.",
"items": {
"$ref": "#/components/schemas/Filament"
},
"type": "array"
},
"nozzle_diameter": {
"description": "Diameter of the extrusion nozzle, in mm.",
"format": "double",
"type": "number"
}
},
"required": [
"filaments",
"nozzle_diameter"
],
"type": "object"
},
"Filament": {
"description": "Information about the filament being used in a FDM printer.",
"properties": {
"color": {
"description": "The color (as hex without the `#`) of the filament, this is likely specific to the manufacturer.",
"maxLength": 6,
"minLength": 6,
"nullable": true,
"type": "string"
},
"material": {
"allOf": [
{
"$ref": "#/components/schemas/FilamentMaterial"
}
],
"description": "The material that the filament is made of."
},
"name": {
"description": "The name of the filament, this is likely specfic to the manufacturer.",
"nullable": true,
"type": "string"
}
},
"required": [
"material"
],
"type": "object"
},
"FilamentMaterial": {
"description": "The material that the filament is made of.",
"oneOf": [
{
"description": "Polylactic acid based plastics",
"properties": {
"type": {
"enum": [
"pla"
],
"type": "string"
}
},
"required": [
"type"
],
"type": "object"
},
{
"description": "Pla support",
"properties": {
"type": {
"enum": [
"pla_support"
],
"type": "string"
}
},
"required": [
"type"
],
"type": "object"
},
{
"description": "acrylonitrile butadiene styrene based plastics",
"properties": {
"type": {
"enum": [
"abs"
],
"type": "string"
}
},
"required": [
"type"
],
"type": "object"
},
{
"description": "polyethylene terephthalate glycol based plastics",
"properties": {
"type": {
"enum": [
"petg"
],
"type": "string"
}
},
"required": [
"type"
],
"type": "object"
},
{
"description": "unsuprisingly, nylon based",
"properties": {
"type": {
"enum": [
"nylon"
],
"type": "string"
}
},
"required": [
"type"
],
"type": "object"
},
{
"description": "thermoplastic polyurethane based urethane material",
"properties": {
"type": {
"enum": [
"tpu"
],
"type": "string"
}
},
"required": [
"type"
],
"type": "object"
},
{
"description": "polyvinyl alcohol based material",
"properties": {
"type": {
"enum": [
"pva"
],
"type": "string"
}
},
"required": [
"type"
],
"type": "object"
},
{
"description": "high impact polystyrene based material",
"properties": {
"type": {
"enum": [
"hips"
],
"type": "string"
}
},
"required": [
"type"
],
"type": "object"
},
{
"description": "composite material with stuff in other stuff, something like PLA mixed with carbon fiber, kevlar, or fiberglass",
"properties": {
"type": {
"enum": [
"composite"
],
"type": "string"
}
},
"required": [
"type"
],
"type": "object"
}
]
},
"HardwareConfiguration": {
"description": "The hardware configuration of a machine.",
"oneOf": [
{
"description": "No configuration is possible. This isn't the same conceptually as an `Option<HardwareConfiguration>`, because this indicates we positively know there is no possible configuration changes that are possible with this method of manufcture.",
"properties": {
"type": {
"enum": [
"none"
],
"type": "string"
}
},
"required": [
"type"
],
"type": "object"
},
{
"description": "Hardware configuration specific to FDM based printers",
"properties": {
"config": {
"allOf": [
{
"$ref": "#/components/schemas/FdmHardwareConfiguration"
}
],
"description": "The configuration for the FDM printer."
},
"type": {
"enum": [
"fdm"
],
"type": "string"
}
},
"required": [
"config",
"type"
"Bambu"
],
"type": "object"
}
@ -344,14 +85,6 @@
"description": "Additional, per-machine information which is specific to the underlying machine type.",
"nullable": true
},
"hardware_configuration": {
"allOf": [
{
"$ref": "#/components/schemas/HardwareConfiguration"
}
],
"description": "Information about how the Machine is currently configured."
},
"id": {
"description": "Machine Identifier (ID) for the specific Machine.",
"type": "string"
@ -381,12 +114,6 @@
"description": "Maximum part size that can be manufactured by this device. This may be some sort of theoretical upper bound, getting close to this limit seems like maybe a bad idea.\n\nThis may be `None` if the maximum size is not knowable by the Machine API.\n\nWhat \"close\" means is up to you!",
"nullable": true
},
"progress": {
"description": "Progress of the current print, if printing.",
"format": "double",
"nullable": true,
"type": "number"
},
"state": {
"allOf": [
{
@ -397,7 +124,6 @@
}
},
"required": [
"hardware_configuration",
"id",
"machine_type",
"make_model",
@ -431,111 +157,57 @@
"oneOf": [
{
"description": "If a print state can not be resolved at this time, an Unknown may be returned.",
"properties": {
"state": {
"enum": [
"unknown"
],
"type": "string"
}
},
"required": [
"state"
"enum": [
"Unknown"
],
"type": "object"
"type": "string"
},
{
"description": "Idle, and ready for another job.",
"properties": {
"state": {
"enum": [
"idle"
],
"type": "string"
}
},
"required": [
"state"
"enum": [
"Idle"
],
"type": "object"
"type": "string"
},
{
"description": "Running a job -- 3D printing or CNC-ing a part.",
"properties": {
"state": {
"enum": [
"running"
],
"type": "string"
}
},
"required": [
"state"
"enum": [
"Running"
],
"type": "object"
"type": "string"
},
{
"description": "Machine is currently offline or unreachable.",
"properties": {
"state": {
"enum": [
"offline"
],
"type": "string"
}
},
"required": [
"state"
"enum": [
"Offline"
],
"type": "object"
"type": "string"
},
{
"description": "Job is underway but halted, waiting for some action to take place.",
"properties": {
"state": {
"enum": [
"paused"
],
"type": "string"
}
},
"required": [
"state"
"enum": [
"Paused"
],
"type": "object"
"type": "string"
},
{
"description": "Job is finished, but waiting manual action to move back to Idle.",
"properties": {
"state": {
"enum": [
"complete"
],
"type": "string"
}
},
"required": [
"state"
"enum": [
"Complete"
],
"type": "object"
"type": "string"
},
{
"additionalProperties": false,
"description": "The printer has failed and is in an unknown state that may require manual attention to resolve. The inner value is a human readable description of what specifically has failed.",
"properties": {
"message": {
"description": "A human-readable message describing the failure.",
"Failed": {
"nullable": true,
"type": "string"
},
"state": {
"enum": [
"failed"
],
"type": "string"
}
},
"required": [
"state"
"Failed"
],
"type": "object"
}
@ -547,54 +219,21 @@
{
"description": "Use light to cure a resin to build up layers.",
"enum": [
"stereolithography"
"Stereolithography"
],
"type": "string"
},
{
"description": "Fused Deposition Modeling, layers of melted plastic.",
"enum": [
"fused_deposition"
"FusedDeposition"
],
"type": "string"
},
{
"description": "\"Computer numerical control\" - machine that grinds away material from a hunk of material to construct a part.",
"enum": [
"cnc"
],
"type": "string"
}
]
},
"NozzleDiameter": {
"description": "A nozzle diameter.",
"oneOf": [
{
"description": "0.2mm.",
"enum": [
"0.2"
],
"type": "string"
},
{
"description": "0.4mm.",
"enum": [
"0.4"
],
"type": "string"
},
{
"description": "0.6mm.",
"enum": [
"0.6"
],
"type": "string"
},
{
"description": "0.8mm.",
"enum": [
"0.8"
"Cnc"
],
"type": "string"
}
@ -645,15 +284,6 @@
"machine_id": {
"description": "The machine id to print to.",
"type": "string"
},
"slicer_configuration": {
"allOf": [
{
"$ref": "#/components/schemas/SlicerConfiguration"
}
],
"description": "Requested design-specific slicer configurations.",
"nullable": true
}
},
"required": [
@ -662,283 +292,6 @@
],
"type": "object"
},
"SlicerConfiguration": {
"description": "The slicer configuration is a set of parameters that are passed to the slicer to control how the gcode is generated.",
"properties": {
"filament_idx": {
"description": "The filament to use for the print.",
"format": "uint",
"minimum": 0,
"nullable": true,
"type": "integer"
}
},
"type": "object"
},
"Stage": {
"description": "The print stage. These come from: https://github.com/SoftFever/OrcaSlicer/blob/431978baf17961df90f0d01871b0ad1d839d7f5d/src/slic3r/GUI/DeviceManager.cpp#L78",
"oneOf": [
{
"description": "Nothing.",
"enum": [
"nothing"
],
"type": "string"
},
{
"description": "Empty.",
"enum": [
"empty"
],
"type": "string"
},
{
"description": "Auto bed leveling.",
"enum": [
"auto_bed_leveling"
],
"type": "string"
},
{
"description": "Heatbed preheating.",
"enum": [
"heatbed_preheating"
],
"type": "string"
},
{
"description": "Sweeping XY mech mode.",
"enum": [
"sweeping_xy_mech_mode"
],
"type": "string"
},
{
"description": "Changing filament.",
"enum": [
"changing_filament"
],
"type": "string"
},
{
"description": "M400 pause.",
"enum": [
"m400_pause"
],
"type": "string"
},
{
"description": "Paused due to filament runout.",
"enum": [
"paused_due_to_filament_runout"
],
"type": "string"
},
{
"description": "Heating hotend.",
"enum": [
"heating_hotend"
],
"type": "string"
},
{
"description": "Calibrating extrusion.",
"enum": [
"calibrating_extrusion"
],
"type": "string"
},
{
"description": "Scanning bed surface.",
"enum": [
"scanning_bed_surface"
],
"type": "string"
},
{
"description": "Inspecting first layer.",
"enum": [
"inspecting_first_layer"
],
"type": "string"
},
{
"description": "Identifying build plate type.",
"enum": [
"identifying_build_plate_type"
],
"type": "string"
},
{
"description": "Calibrating micro lidar.",
"enum": [
"calibrating_micro_lidar"
],
"type": "string"
},
{
"description": "Homing toolhead.",
"enum": [
"homing_toolhead"
],
"type": "string"
},
{
"description": "Cleaning nozzle tip.",
"enum": [
"cleaning_nozzle_tip"
],
"type": "string"
},
{
"description": "Checking extruder temperature.",
"enum": [
"checking_extruder_temperature"
],
"type": "string"
},
{
"description": "Printing was paused by the user.",
"enum": [
"printing_was_paused_by_the_user"
],
"type": "string"
},
{
"description": "Pause of front cover falling.",
"enum": [
"pause_of_front_cover_falling"
],
"type": "string"
},
{
"description": "Calibrating micro lidar.",
"enum": [
"calibrating_micro_lidar2"
],
"type": "string"
},
{
"description": "Calibrating extrusion flow.",
"enum": [
"calibrating_extrusion_flow"
],
"type": "string"
},
{
"description": "Paused due to nozzle temperature malfunction.",
"enum": [
"paused_due_to_nozzle_temperature_malfunction"
],
"type": "string"
},
{
"description": "Paused due to heat bed temperature malfunction.",
"enum": [
"paused_due_to_heat_bed_temperature_malfunction"
],
"type": "string"
},
{
"description": "Filament unloading.",
"enum": [
"filament_unloading"
],
"type": "string"
},
{
"description": "Skip step pause.",
"enum": [
"skip_step_pause"
],
"type": "string"
},
{
"description": "Filament loading.",
"enum": [
"filament_loading"
],
"type": "string"
},
{
"description": "Motor noise calibration.",
"enum": [
"motor_noise_calibration"
],
"type": "string"
},
{
"description": "Paused due to AMS lost.",
"enum": [
"paused_due_to_ams_lost"
],
"type": "string"
},
{
"description": "Paused due to low speed of the heat break fan.",
"enum": [
"paused_due_to_low_speed_of_the_heat_break_fan"
],
"type": "string"
},
{
"description": "Paused due to chamber temperature control error.",
"enum": [
"paused_due_to_chamber_temperature_control_error"
],
"type": "string"
},
{
"description": "Cooling chamber.",
"enum": [
"cooling_chamber"
],
"type": "string"
},
{
"description": "Paused by the Gcode inserted by the user.",
"enum": [
"paused_by_the_gcode_inserted_by_the_user"
],
"type": "string"
},
{
"description": "Motor noise showoff.",
"enum": [
"motor_noise_showoff"
],
"type": "string"
},
{
"description": "Nozzle filament covered detected pause.",
"enum": [
"nozzle_filament_covered_detected_pause"
],
"type": "string"
},
{
"description": "Cutter error pause.",
"enum": [
"cutter_error_pause"
],
"type": "string"
},
{
"description": "First layer error pause.",
"enum": [
"first_layer_error_pause"
],
"type": "string"
},
{
"description": "Nozzle clog pause.",
"enum": [
"nozzle_clog_pause"
],
"type": "string"
}
]
},
"Volume": {
"description": "Set of three values to represent the extent of a 3-D Volume. This contains the width, depth, and height values, generally used to represent some maximum or minimum.\n\nAll measurements are in millimeters.",
"properties": {
@ -1072,34 +425,6 @@
]
}
},
"/metrics": {
"get": {
"operationId": "get_metrics",
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"title": "String",
"type": "string"
}
}
},
"description": "successful operation"
},
"4XX": {
"$ref": "#/components/responses/Error"
},
"5XX": {
"$ref": "#/components/responses/Error"
}
},
"summary": "List available machines and their statuses",
"tags": [
"hidden"
]
}
},
"/ping": {
"get": {
"operationId": "ping",
@ -1167,13 +492,6 @@
}
},
"tags": [
{
"description": "Hidden API endpoints that should not show up in the docs.",
"externalDocs": {
"url": "https://docs.zoo.dev/api/machines"
},
"name": "hidden"
},
{
"description": "Utilities for making parts and discovering machines.",
"externalDocs": {

View File

@ -1,6 +1,6 @@
{
"name": "zoo-modeling-app",
"version": "0.26.0",
"version": "0.25.5",
"private": true,
"productName": "Zoo Modeling App",
"author": {
@ -26,7 +26,7 @@
"@fortawesome/react-fontawesome": "^0.2.0",
"@headlessui/react": "^1.7.19",
"@headlessui/tailwindcss": "^0.2.0",
"@kittycad/lib": "2.0.7",
"@kittycad/lib": "^2.0.1",
"@lezer/highlight": "^1.2.1",
"@lezer/lr": "^1.4.1",
"@react-hook/resize-observer": "^2.0.1",
@ -36,7 +36,6 @@
"@xstate/inspect": "^0.8.0",
"@xstate/react": "^4.1.1",
"bonjour-service": "^1.2.1",
"chokidar": "^4.0.1",
"codemirror": "^6.0.1",
"decamelize": "^6.0.0",
"electron-squirrel-startup": "^1.0.1",

View File

@ -64,27 +64,6 @@ export type ReactCameraProperties =
const lastCmdDelay = 50
class CameraRateLimiter {
lastSend?: Date = undefined
rateLimitMs: number = 16 //60 FPS
send = (f: () => void) => {
let now = new Date()
if (
this.lastSend === undefined ||
now.getTime() - this.lastSend.getTime() > this.rateLimitMs
) {
f()
this.lastSend = now
}
}
reset = () => {
this.lastSend = undefined
}
}
export class CameraControls {
engineCommandManager: EngineCommandManager
syncDirection: 'clientToEngine' | 'engineToClient' = 'engineToClient'
@ -98,8 +77,9 @@ export class CameraControls {
enableRotate = true
enablePan = true
enableZoom = true
moveSender: CameraRateLimiter = new CameraRateLimiter()
zoomSender: CameraRateLimiter = new CameraRateLimiter()
zoomDataFromLastFrame?: number = undefined
// holds coordinates, and interaction
moveDataFromLastFrame?: [number, number, string] = undefined
lastPerspectiveFov: number = 45
pendingZoom: number | null = null
pendingRotation: Vector2 | null = null
@ -191,36 +171,6 @@ export class CameraControls {
}
}
doMove = (interaction: any, coordinates: any) => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
type: 'camera_drag_move',
interaction: interaction,
window: {
x: coordinates[0],
y: coordinates[1],
},
},
cmd_id: uuidv4(),
})
}
doZoom = (zoom: number) => {
this.handleStart()
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
type: 'default_camera_zoom',
magnitude: (-1 * zoom) / window.devicePixelRatio,
},
cmd_id: uuidv4(),
})
this.handleEnd()
}
constructor(
isOrtho = false,
domElement: HTMLCanvasElement,
@ -308,6 +258,49 @@ export class CameraControls {
this.onCameraChange()
}
// Our stream is never more than 60fps.
// We can get away with capping our "virtual fps" to 60 then.
const FPS_VIRTUAL = 60
const doZoom = () => {
if (this.zoomDataFromLastFrame !== undefined) {
this.handleStart()
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
type: 'default_camera_zoom',
magnitude:
(-1 * this.zoomDataFromLastFrame) / window.devicePixelRatio,
},
cmd_id: uuidv4(),
})
this.handleEnd()
}
this.zoomDataFromLastFrame = undefined
}
setInterval(doZoom, 1000 / FPS_VIRTUAL)
const doMove = () => {
if (this.moveDataFromLastFrame !== undefined) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
type: 'camera_drag_move',
interaction: this.moveDataFromLastFrame[2] as any,
window: {
x: this.moveDataFromLastFrame[0],
y: this.moveDataFromLastFrame[1],
},
},
cmd_id: uuidv4(),
})
}
this.moveDataFromLastFrame = undefined
}
setInterval(doMove, 1000 / FPS_VIRTUAL)
setTimeout(() => {
this.engineCommandManager.subscribeTo({
event: 'camera_drag_end',
@ -393,9 +386,7 @@ export class CameraControls {
if (interaction === 'none') return
if (this.syncDirection === 'engineToClient') {
this.moveSender.send(() => {
this.doMove(interaction, [event.clientX, event.clientY])
})
this.moveDataFromLastFrame = [event.clientX, event.clientY, interaction]
return
}
@ -468,9 +459,7 @@ export class CameraControls {
if (this.syncDirection === 'engineToClient') {
if (interaction === 'zoom') {
this.zoomSender.send(() => {
this.doZoom(event.deltaY)
})
this.zoomDataFromLastFrame = event.deltaY
} else {
// This case will get handled when we add pan and rotate using Apple trackpad.
console.error(
@ -904,7 +893,6 @@ export class CameraControls {
type: 'zoom_to_fit',
object_ids: [], // leave empty to zoom to all objects
padding: 0.2, // padding around the objects
animated: false, // don't animate the zoom for now
},
})
}

View File

@ -408,7 +408,6 @@ export async function deleteSegment({
const testExecute = await executeAst({
ast: modifiedAst,
idGenerator: kclManager.execState.idGenerator,
useFakeExecutor: true,
engineCommandManager: engineCommandManager,
})

View File

@ -391,14 +391,12 @@ export class SceneEntities {
const { truncatedAst, programMemoryOverride, variableDeclarationName } =
prepared
const { execState } = await executeAst({
const { programMemory } = await executeAst({
ast: truncatedAst,
useFakeExecutor: true,
engineCommandManager: this.engineCommandManager,
programMemoryOverride,
idGenerator: kclManager.execState.idGenerator,
})
const programMemory = execState.memory
const sketch = sketchFromPathToNode({
pathToNode: sketchPathToNode,
ast: maybeModdedAst,
@ -803,14 +801,12 @@ export class SceneEntities {
updateRectangleSketch(sketchInit, x, y, tags[0])
}
const { execState } = await executeAst({
const { programMemory } = await executeAst({
ast: truncatedAst,
useFakeExecutor: true,
engineCommandManager: this.engineCommandManager,
programMemoryOverride,
idGenerator: kclManager.execState.idGenerator,
})
const programMemory = execState.memory
this.sceneProgramMemory = programMemory
const sketch = sketchFromKclValue(
programMemory.get(variableDeclarationName),
@ -852,14 +848,12 @@ export class SceneEntities {
await kclManager.executeAstMock(_ast)
sceneInfra.modelingSend({ type: 'Finish rectangle' })
const { execState } = await executeAst({
const { programMemory } = await executeAst({
ast: _ast,
useFakeExecutor: true,
engineCommandManager: this.engineCommandManager,
programMemoryOverride,
idGenerator: kclManager.execState.idGenerator,
})
const programMemory = execState.memory
// Prepare to update the THREEjs scene
this.sceneProgramMemory = programMemory
@ -971,14 +965,12 @@ export class SceneEntities {
modded = moddedResult.modifiedAst
}
const { execState } = await executeAst({
const { programMemory } = await executeAst({
ast: modded,
useFakeExecutor: true,
engineCommandManager: this.engineCommandManager,
programMemoryOverride,
idGenerator: kclManager.execState.idGenerator,
})
const programMemory = execState.memory
this.sceneProgramMemory = programMemory
const sketch = sketchFromKclValue(
programMemory.get(variableDeclarationName),
@ -1325,14 +1317,12 @@ export class SceneEntities {
// don't want to mod the user's code yet as they have't committed to the change yet
// plus this would be the truncated ast being recast, it would be wrong
codeManager.updateCodeEditor(code)
const { execState } = await executeAst({
const { programMemory } = await executeAst({
ast: truncatedAst,
useFakeExecutor: true,
engineCommandManager: this.engineCommandManager,
programMemoryOverride,
idGenerator: kclManager.execState.idGenerator,
})
const programMemory = execState.memory
this.sceneProgramMemory = programMemory
const maybeSketch = programMemory.get(variableDeclarationName)

View File

@ -157,7 +157,7 @@ export function useCalc({
engineCommandManager,
useFakeExecutor: true,
programMemoryOverride: kclManager.programMemory.clone(),
}).then(({ execState }) => {
}).then(({ programMemory }) => {
const resultDeclaration = ast.body.find(
(a) =>
a.type === 'VariableDeclaration' &&
@ -166,7 +166,7 @@ export function useCalc({
const init =
resultDeclaration?.type === 'VariableDeclaration' &&
resultDeclaration?.declarations?.[0]?.init
const result = execState.memory?.get('__result__')?.value
const result = programMemory?.get('__result__')?.value
setCalcResult(typeof result === 'number' ? String(result) : 'NAN')
init && setValueNode(init)
})

View File

@ -135,9 +135,7 @@ function CommandArgOptionInput({
<Combobox.Input
id="option-input"
ref={inputRef}
onChange={(event) =>
!event.target.disabled && setQuery(event.target.value)
}
onChange={(event) => setQuery(event.target.value)}
className="flex-grow px-2 py-1 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80 !bg-transparent focus:outline-none"
onKeyDown={(event) => {
if (event.metaKey && event.key === 'k')
@ -177,18 +175,9 @@ function CommandArgOptionInput({
<Combobox.Option
key={option.name}
value={option}
disabled={option.disabled}
className="flex items-center gap-2 px-4 py-1 first:mt-2 last:mb-2 ui-active:bg-primary/10 dark:ui-active:bg-chalkboard-90"
>
<p
className={`flex-grow ${
(option.disabled &&
'text-chalkboard-70 dark:text-chalkboard-50 cursor-not-allowed') ||
''
}`}
>
{option.name}
</p>
<p className="flex-grow">{option.name} </p>
{option.value === currentOption?.value && (
<small className="text-chalkboard-70 dark:text-chalkboard-50">
current

View File

@ -91,7 +91,7 @@ function CommandBarSelectionInput({
<form id="arg-form" onSubmit={handleSubmit}>
<label
className={
'relative flex flex-col mx-4 my-4 ' +
'relative flex items-center mx-4 my-4 ' +
(!hasSubmitted || canSubmitSelection || 'text-destroy-50')
}
>
@ -100,18 +100,13 @@ function CommandBarSelectionInput({
: `Please select ${
arg.multiple ? 'one or more ' : 'one '
}${getSemanticSelectionType(arg.selectionTypes).join(' or ')}`}
{arg.warningMessage && (
<p className="text-warn-80 bg-warn-10 px-2 py-1 rounded-sm mt-3 mr-2 -mb-2 w-full text-sm cursor-default">
{arg.warningMessage}
</p>
)}
<input
id="selection"
name="selection"
ref={inputRef}
required
placeholder="Select an entity with your mouse"
className="absolute inset-0 w-full h-full opacity-0 cursor-default"
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
onKeyDown={(event) => {
if (event.key === 'Backspace') {
stepBack()

View File

@ -1,111 +0,0 @@
import { isArray, isNonNullable } from 'lib/utils'
import { useRef, useState } from 'react'
type Primitive = string | number | bigint | boolean | symbol | null | undefined
export type GenericObj = {
type?: string
[key: string]: GenericObj | Primitive | Array<GenericObj | Primitive>
}
/**
* Display an array of objects or primitives for debug purposes. Nullable values
* are displayed so that relative indexes are preserved.
*/
export function DebugDisplayArray({
arr,
filterKeys,
}: {
arr: Array<GenericObj | Primitive>
filterKeys: string[]
}) {
return (
<>
{arr.map((obj, index) => {
return (
<div className="my-2" key={index}>
{obj && typeof obj === 'object' ? (
<DebugDisplayObj obj={obj} filterKeys={filterKeys} />
) : isNonNullable(obj) ? (
<span>{obj.toString()}</span>
) : (
<span>{obj}</span>
)}
</div>
)
})}
</>
)
}
/**
* Display an object as a tree for debug purposes. Nullable values are omitted.
* The only other property treated specially is the type property, which is
* assumed to be a string.
*/
export function DebugDisplayObj({
obj,
filterKeys,
}: {
obj: GenericObj
filterKeys: string[]
}) {
const ref = useRef<HTMLPreElement>(null)
const hasCursor = false
const [isCollapsed, setIsCollapsed] = useState(false)
return (
<pre
ref={ref}
className={`ml-2 border-l border-violet-600 pl-1 ${
hasCursor ? 'bg-violet-100/80 dark:bg-violet-100/25' : ''
}`}
>
{isCollapsed ? (
<button
className="m-0 p-0 border-0"
onClick={() => setIsCollapsed(false)}
>
{'>'}type: {obj.type}
</button>
) : (
<span className="flex">
<button
className="m-0 p-0 border-0 mb-auto"
onClick={() => setIsCollapsed(true)}
>
{'⬇️'}
</button>
<ul className="inline-block">
{Object.entries(obj).map(([key, value]) => {
if (filterKeys.includes(key)) {
return null
} else if (isArray(value)) {
return (
<li key={key}>
{`${key}: [`}
<DebugDisplayArray arr={value} filterKeys={filterKeys} />
{']'}
</li>
)
} else if (typeof value === 'object' && value !== null) {
return (
<li key={key}>
{key}:
<DebugDisplayObj obj={value} filterKeys={filterKeys} />
</li>
)
} else if (isNonNullable(value)) {
return (
<li key={key}>
{key}: {value.toString()}
</li>
)
}
return null
})}
</ul>
</span>
)}
</pre>
)
}

View File

@ -1,45 +0,0 @@
import { useMemo } from 'react'
import { engineCommandManager } from 'lib/singletons'
import {
ArtifactGraph,
expandPlane,
PlaneArtifactRich,
} from 'lang/std/artifactGraph'
import { DebugDisplayArray, GenericObj } from './DebugDisplayObj'
export function DebugFeatureTree() {
const featureTree = useMemo(() => {
return computeTree(engineCommandManager.artifactGraph)
}, [engineCommandManager.artifactGraph])
const filterKeys: string[] = ['__meta', 'codeRef', 'pathToNode']
return (
<details data-testid="debug-feature-tree" className="relative">
<summary>Feature Tree</summary>
{featureTree.length > 0 ? (
<pre className="text-xs">
<DebugDisplayArray arr={featureTree} filterKeys={filterKeys} />
</pre>
) : (
<p>(Empty)</p>
)}
</details>
)
}
function computeTree(artifactGraph: ArtifactGraph): GenericObj[] {
let items: GenericObj[] = []
const planes: PlaneArtifactRich[] = []
for (const artifact of artifactGraph.values()) {
if (artifact.type === 'plane') {
planes.push(expandPlane(artifact, artifactGraph))
}
}
const extraRichPlanes: GenericObj[] = planes.map((plane) => {
return plane as any as GenericObj
})
items = items.concat(extraRichPlanes)
return items
}

View File

@ -2,7 +2,7 @@ import type { IndexLoaderData } from 'lib/types'
import { PATHS } from 'lib/paths'
import { ActionButton } from './ActionButton'
import Tooltip from './Tooltip'
import { Dispatch, useCallback, useRef, useState } from 'react'
import { Dispatch, useCallback, useEffect, useRef, useState } from 'react'
import { useNavigate, useRouteLoaderData } from 'react-router-dom'
import { Disclosure } from '@headlessui/react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
@ -13,6 +13,7 @@ import { sortProject } from 'lib/desktopFS'
import { FILE_EXT } from 'lib/constants'
import { CustomIcon } from './CustomIcon'
import { codeManager, kclManager } from 'lib/singletons'
import { useDocumentHasFocus } from 'hooks/useDocumentHasFocus'
import { useLspContext } from './LspProvider'
import useHotkeyWrapper from 'lib/hotkeyWrapper'
import { useModelingContext } from 'hooks/useModelingContext'
@ -20,8 +21,6 @@ import { DeleteConfirmationDialog } from './ProjectCard/DeleteProjectDialog'
import { ContextMenu, ContextMenuItem } from './ContextMenu'
import usePlatform from 'hooks/usePlatform'
import { FileEntry } from 'lib/project'
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
import { normalizeLineEndings } from 'lib/codeEditor'
function getIndentationCSS(level: number) {
return `calc(1rem * ${level + 1})`
@ -132,23 +131,6 @@ const FileTreeItem = ({
const isCurrentFile = fileOrDir.path === currentFile?.path
const itemRef = useRef(null)
// Since every file or directory gets its own FileTreeItem, we can do this.
// Because subtrees only render when they are opened, that means this
// only listens when they open. Because this acts like a useEffect, when
// the ReactNodes are destroyed, so is this listener :)
useFileSystemWatcher(
async (eventType, path) => {
// Don't try to read a file that was removed.
if (isCurrentFile && eventType !== 'unlink') {
let code = await window.electron.readFile(path, { encoding: 'utf-8' })
code = normalizeLineEndings(code)
codeManager.updateCodeStateEditor(code)
}
fileSend({ type: 'Refresh' })
},
[fileOrDir.path]
)
const isRenaming = fileContext.itemsBeingRenamed.includes(fileOrDir.path)
const removeCurrentItemFromRenaming = useCallback(
() =>
@ -172,13 +154,6 @@ const FileTreeItem = ({
})
}, [fileContext.itemsBeingRenamed, fileOrDir.path, fileSend])
const clickDirectory = () => {
fileSend({
type: 'Set selected directory',
directory: fileOrDir,
})
}
function handleKeyUp(e: React.KeyboardEvent<HTMLButtonElement>) {
if (e.metaKey && e.key === 'Backspace') {
// Open confirmation dialog
@ -267,8 +242,18 @@ const FileTreeItem = ({
}
style={{ paddingInlineStart: getIndentationCSS(level) }}
onClick={(e) => e.currentTarget.focus()}
onClickCapture={clickDirectory}
onFocusCapture={clickDirectory}
onClickCapture={(e) =>
fileSend({
type: 'Set selected directory',
directory: fileOrDir,
})
}
onFocusCapture={(e) =>
fileSend({
type: 'Set selected directory',
directory: fileOrDir,
})
}
onKeyDown={(e) => e.key === 'Enter' && e.preventDefault()}
onKeyUp={handleKeyUp}
>
@ -484,36 +469,27 @@ export const FileTreeInner = ({
const loaderData = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
const { send: fileSend, context: fileContext } = useFileContext()
const { send: modelingSend } = useModelingContext()
const documentHasFocus = useDocumentHasFocus()
// Refresh the file tree when there are changes.
useFileSystemWatcher(
async (eventType, path) => {
// Our other watcher races with this watcher on the current file changes,
// so we need to stop this one from reacting at all, otherwise Bad Things
// Happen™.
const isCurrentFile = loaderData.file?.path === path
const hasChanged = eventType === 'change'
if (isCurrentFile && hasChanged) return
fileSend({ type: 'Refresh' })
},
[loaderData?.project?.path, fileContext.selectedDirectory.path].filter(
(x: string | undefined) => x !== undefined
)
)
const clickDirectory = () => {
fileSend({
type: 'Set selected directory',
directory: fileContext.project,
})
}
// Refresh the file tree when the document gets focus
useEffect(() => {
fileSend({ type: 'Refresh' })
}, [documentHasFocus])
return (
<div
className="overflow-auto pb-12 absolute inset-0"
data-testid="file-pane-scroll-container"
>
<ul className="m-0 p-0 text-sm" onClickCapture={clickDirectory}>
<ul
className="m-0 p-0 text-sm"
onClickCapture={(e) => {
fileSend({
type: 'Set selected directory',
directory: fileContext.project,
})
}}
>
{sortProject(fileContext.project?.children || []).map((fileOrDir) => (
<FileTreeItem
project={fileContext.project}

View File

@ -28,7 +28,6 @@ import {
import { Popover } from '@headlessui/react'
import { CustomIcon } from './CustomIcon'
import { reportRejection } from 'lib/trap'
import { useModelingContext } from 'hooks/useModelingContext'
const CANVAS_SIZE = 80
const FRUSTUM_SIZE = 0.5
@ -63,7 +62,6 @@ export default function Gizmo() {
const raycasterIntersect = useRef<Intersection<Object3D> | null>(null)
const cameraPassiveUpdateTimer = useRef(0)
const raycasterPassiveUpdateTimer = useRef(0)
const { send: modelingSend } = useModelingContext()
const menuItems = useMemo(
() => [
...Object.entries(axisNamesSemantic).map(([axisName, axisSemantic]) => (
@ -78,7 +76,6 @@ export default function Gizmo() {
{axisSemantic} view
</ContextMenuItem>
)),
<ContextMenuDivider />,
<ContextMenuItem
onClick={() => {
sceneInfra.camControls.resetCameraPosition().catch(reportRejection)
@ -86,13 +83,6 @@ export default function Gizmo() {
>
Reset view
</ContextMenuItem>,
<ContextMenuItem
onClick={() => {
modelingSend({ type: 'Center camera on selection' })
}}
>
Center view on selection
</ContextMenuItem>,
<ContextMenuDivider />,
<ContextMenuItemRefresh />,
],

View File

@ -69,7 +69,7 @@ import { exportFromEngine } from 'lib/exportFromEngine'
import { Models } from '@kittycad/lib/dist/types/src'
import toast from 'react-hot-toast'
import { EditorSelection, Transaction } from '@codemirror/state'
import { useLoaderData, useNavigate, useSearchParams } from 'react-router-dom'
import { useNavigate, useSearchParams } from 'react-router-dom'
import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls'
import { getVarNameModal } from 'hooks/useToolbarGuards'
import { err, reportRejection, trap } from 'lib/trap'
@ -83,8 +83,6 @@ import {
} from 'lang/std/engineConnection'
import { submitAndAwaitTextToKcl } from 'lib/textToCad'
import { useFileContext } from 'hooks/useFileContext'
import { uuidv4 } from 'lib/utils'
import { IndexLoaderData } from 'lib/types'
type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T>
@ -117,7 +115,6 @@ export const ModelingMachineProvider = ({
} = useSettingsAuthContext()
const navigate = useNavigate()
const { context, send: fileMachineSend } = useFileContext()
const { file } = useLoaderData() as IndexLoaderData
const token = auth?.context?.token
const streamRef = useRef<HTMLDivElement>(null)
const persistedContext = useMemo(() => getPersistedContext(), [])
@ -151,13 +148,6 @@ export const ModelingMachineProvider = ({
},
'sketch exit execute': ({ context: { store } }) => {
;(async () => {
// When cancelling the sketch mode we should disable sketch mode within the engine.
await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: { type: 'sketch_mode_disable' },
})
sceneInfra.camControls.syncDirection = 'clientToEngine'
if (cameraProjection.current === 'perspective') {
@ -253,17 +243,6 @@ export const ModelingMachineProvider = ({
return {}
},
}),
'Center camera on selection': () => {
engineCommandManager
.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_center_to_selection',
},
})
.catch(reportRejection)
},
'Set sketchDetails': assign(({ context: { sketchDetails }, event }) => {
if (event.type !== 'Delete segment') return {}
if (!sketchDetails) return {}
@ -411,15 +390,12 @@ export const ModelingMachineProvider = ({
Make: ({ event }) => {
if (event.type !== 'Make') return
// Check if we already have an export intent.
if (engineCommandManager.exportInfo) {
if (engineCommandManager.exportIntent) {
toast.error('Already exporting')
return
}
// Set the export intent.
engineCommandManager.exportInfo = {
intent: ExportIntent.Make,
name: file?.name || '',
}
engineCommandManager.exportIntent = ExportIntent.Make
// Set the current machine.
machineManager.currentMachine = event.data.machine
@ -448,16 +424,12 @@ export const ModelingMachineProvider = ({
},
'Engine export': ({ event }) => {
if (event.type !== 'Export') return
if (engineCommandManager.exportInfo) {
if (engineCommandManager.exportIntent) {
toast.error('Already exporting')
return
}
// Set the export intent.
engineCommandManager.exportInfo = {
intent: ExportIntent.Save,
// This never gets used its only for make.
name: '',
}
engineCommandManager.exportIntent = ExportIntent.Save
const format = {
...event.data,
@ -1065,11 +1037,6 @@ export const ModelingMachineProvider = ({
modelingSend({ type: 'Delete selection' })
})
// Allow ctrl+alt+c to center to selection
useHotkeys(['mod + alt + c'], () => {
modelingSend({ type: 'Center camera on selection' })
})
useStateMachineCommands({
machineId: 'modeling',
state: modelingState,

View File

@ -1,4 +1,3 @@
import { DebugFeatureTree } from 'components/DebugFeatureTree'
import { AstExplorer } from '../../AstExplorer'
import { EngineCommands } from '../../EngineCommands'
import { CamDebugSettings } from 'clientSideScene/ClientSideSceneComp'
@ -13,7 +12,6 @@ export const DebugPane = () => {
<EngineCommands />
<CamDebugSettings />
<AstExplorer />
<DebugFeatureTree />
</div>
</section>
)

View File

@ -29,8 +29,8 @@ describe('processMemory', () => {
|> lineTo([2.15, 4.32], %)
// |> rx(90, %)`
const ast = parse(code)
const execState = await enginelessExecutor(ast, ProgramMemory.empty())
const output = processMemory(execState.memory)
const programMemory = await enginelessExecutor(ast, ProgramMemory.empty())
const output = processMemory(programMemory)
expect(output.myVar).toEqual(5)
expect(output.otherVar).toEqual(3)
expect(output).toEqual({

View File

@ -11,7 +11,6 @@ export const NetworkMachineIndicator = ({
}) => {
const machineCount = machineManager.machineCount()
const reason = machineManager.noMachinesReason()
const machines = machineManager.machines
return isDesktop() ? (
<Popover className="relative">
@ -47,34 +46,20 @@ export const NetworkMachineIndicator = ({
</div>
{machineCount > 0 && (
<ul className="divide-y divide-chalkboard-20 dark:divide-chalkboard-80">
{machines.map((machine) => {
return (
<li key={machine.id} className={'px-2 py-4 gap-1 last:mb-0 '}>
<p className="">{machine.id.toUpperCase()}</p>
<p className="text-chalkboard-60 dark:text-chalkboard-50 text-xs">
{machine.make_model.model}
{Object.entries(machineManager.machines).map(
([hostname, machine]) => (
<li key={hostname} className={'px-2 py-4 gap-1 last:mb-0 '}>
<p className="">
{machine.make_model.model ||
machine.make_model.manufacturer ||
'Unknown Machine'}
</p>
{machine.extra &&
machine.extra.type === 'bambu' &&
machine.extra.nozzle_diameter && (
<p className="text-chalkboard-60 dark:text-chalkboard-50 text-xs">
Nozzle Diameter: {machine.extra.nozzle_diameter}
</p>
)}
<p className="text-chalkboard-60 dark:text-chalkboard-50 text-xs">
{`Status: ${machine.state.state
.charAt(0)
.toUpperCase()}${machine.state.state.slice(1)}`}
{machine.state.state === 'failed' && machine.state.message
? ` (${machine.state.message})`
: ''}
{machine.state.state === 'running' && machine.progress
? ` (${Math.round(machine.progress)}%)`
: ''}
Hostname {hostname}
</p>
</li>
)
})}
)}
</ul>
)}
</Popover.Panel>

View File

@ -1,10 +1,9 @@
import { trap } from 'lib/trap'
import { useMachine } from '@xstate/react'
import { useNavigate, useRouteLoaderData, useLocation } from 'react-router-dom'
import { PATHS } from 'lib/paths'
import { authMachine, TOKEN_PERSIST_KEY } from '../machines/authMachine'
import withBaseUrl from '../lib/withBaseURL'
import React, { createContext, useEffect, useState } from 'react'
import React, { createContext, useEffect } from 'react'
import useStateMachineCommands from '../hooks/useStateMachineCommands'
import { settingsMachine } from 'machines/settingsMachine'
import { toast } from 'react-hot-toast'
@ -16,6 +15,7 @@ import {
} from 'lib/theme'
import decamelize from 'decamelize'
import { Actor, AnyStateMachine, ContextFrom, Prop, StateFrom } from 'xstate'
import { isDesktop } from 'lib/isDesktop'
import { authCommandBarConfig } from 'lib/commandBarConfigs/authCommandConfig'
import {
kclManager,
@ -33,14 +33,8 @@ import {
import { useCommandsContext } from 'hooks/useCommandsContext'
import { Command } from 'lib/commandTypes'
import { BaseUnit } from 'lib/settings/settingsTypes'
import {
saveSettings,
loadAndValidateSettings,
} from 'lib/settings/settingsUtils'
import { saveSettings } from 'lib/settings/settingsUtils'
import { reportRejection } from 'lib/trap'
import { getAppSettingsFilePath } from 'lib/desktop'
import { isDesktop } from 'lib/isDesktop'
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T>
@ -105,9 +99,6 @@ export const SettingsAuthProviderBase = ({
const location = useLocation()
const navigate = useNavigate()
const { commandBarSend } = useCommandsContext()
const [settingsPath, setSettingsPath] = useState<string | undefined>(
undefined
)
const [settingsState, settingsSend, settingsActor] = useMachine(
settingsMachine.provide({
@ -200,11 +191,7 @@ export const SettingsAuthProviderBase = ({
console.error('Error executing AST after settings change', e)
}
},
persistSettings: ({ context, event }) => {
// Without this, when a user changes the file, it'd
// create a detection loop with the file-system watcher.
if (event.doNotPersist) return
persistSettings: ({ context }) => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
saveSettings(context, loadedProject?.project?.path)
},
@ -214,38 +201,6 @@ export const SettingsAuthProviderBase = ({
)
settingsStateRef = settingsState.context
useEffect(() => {
if (!isDesktop()) return
getAppSettingsFilePath().then(setSettingsPath).catch(trap)
}, [])
useFileSystemWatcher(
async () => {
// If there is a projectPath but it no longer exists it means
// it was exterally removed. If we let the code past this condition
// execute it will recreate the directory due to code in
// loadAndValidateSettings trying to recreate files. I do not
// wish to change the behavior in case anything else uses it.
// Go home.
if (loadedProject?.project?.path) {
if (!window.electron.exists(loadedProject?.project?.path)) {
navigate(PATHS.HOME)
return
}
}
const data = await loadAndValidateSettings(loadedProject?.project?.path)
settingsSend({
type: 'Set all settings',
settings: data.settings,
doNotPersist: true,
})
},
[settingsPath, loadedProject?.project?.path].filter(
(x: string | undefined) => x !== undefined
)
)
// Add settings commands to the command bar
// They're treated slightly differently than other commands
// Because their state machine doesn't have a meaningful .nextEvents,

View File

@ -1,153 +0,0 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { vi } from 'vitest'
import { ToastUpdate } from './ToastUpdate'
describe('ToastUpdate tests', () => {
const testData = {
version: '0.255.255',
files: [
{
url: 'Zoo Modeling App-0.255.255-x64-mac.zip',
sha512:
'VJb0qlrqNr+rVx3QLATz+B28dtHw3osQb5/+UUmQUIMuF9t0i8dTKOVL/2lyJSmLJVw2/SGDB4Ud6VlTPJ6oFw==',
size: 141277345,
},
{
url: 'Zoo Modeling App-0.255.255-arm64-mac.zip',
sha512:
'b+ugdg7A4LhYYJaFkPRxh1RvmGGMlPJJj7inkLg9PwRtCnR9ePMlktj2VRciXF1iLh59XW4bLc4dK1dFQHMULA==',
size: 135278259,
},
{
url: 'Zoo Modeling App-0.255.255-x64-mac.dmg',
sha512:
'gCUqww05yj8OYwPiTq6bo5GbkpngSbXGtenmDD7+kUm0UyVK8WD3dMAfQJtGNG5HY23aHCHe9myE2W4mbZGmiQ==',
size: 146004232,
},
{
url: 'Zoo Modeling App-0.255.255-arm64-mac.dmg',
sha512:
'ND871ayf81F1ZT+iWVLYTc2jdf/Py6KThuxX2QFWz14ebmIbJPL07lNtxQOexOFiuk0MwRhlCy1RzOSG1b9bmw==',
size: 140021522,
},
],
path: 'Zoo Modeling App-0.255.255-x64-mac.zip',
sha512:
'VJb0qlrqNr+rVx3QLATz+B28dtHw3osQb5/+UUmQUIMuF9t0i8dTKOVL/2lyJSmLJVw2/SGDB4Ud6VlTPJ6oFw==',
releaseNotes:
'## Some markdown release notes\n\n- This is a list item\n- This is another list item\n\n```javascript\nconsole.log("Hello, world!")\n```\n',
releaseDate: '2024-10-09T11:57:59.133Z',
} as const
test('Happy path: renders the toast with good data', () => {
const onRestart = vi.fn()
const onDismiss = vi.fn()
render(
<ToastUpdate
onRestart={onRestart}
onDismiss={onDismiss}
version={testData.version}
releaseNotes={testData.releaseNotes}
/>
)
// Locators and other constants
const versionText = screen.getByTestId('update-version')
const restartButton = screen.getByRole('button', { name: /restart/i })
const dismissButton = screen.getByRole('button', { name: /got it/i })
const releaseNotes = screen.getByTestId('release-notes')
expect(versionText).toBeVisible()
expect(versionText).toHaveTextContent(testData.version)
expect(restartButton).toBeEnabled()
fireEvent.click(restartButton)
expect(onRestart.mock.calls).toHaveLength(1)
expect(dismissButton).toBeEnabled()
fireEvent.click(dismissButton)
expect(onDismiss.mock.calls).toHaveLength(1)
// I cannot for the life of me seem to get @testing-library/react
// to properly handle click events or visibility checks on the details element.
// So I'm only checking that the content is in the document.
expect(releaseNotes).toBeInTheDocument()
expect(releaseNotes).toHaveTextContent('Release notes')
const releaseNotesListItems = screen.getAllByRole('listitem')
expect(releaseNotesListItems.map((el) => el.textContent)).toEqual([
'This is a list item',
'This is another list item',
])
})
test('Happy path: renders the breaking changes notice', () => {
const releaseNotesWithBreakingChanges = `
## Some markdown release notes
- This is a list item
- This is another list item with a breaking change
- This is a list item
`
const onRestart = vi.fn()
const onDismiss = vi.fn()
render(
<ToastUpdate
onRestart={onRestart}
onDismiss={onDismiss}
version={testData.version}
releaseNotes={releaseNotesWithBreakingChanges}
/>
)
// Locators and other constants
const releaseNotes = screen.getByText('Release notes', {
selector: 'summary',
})
const listItemContents = screen
.getAllByRole('listitem')
.map((el) => el.textContent)
// I cannot for the life of me seem to get @testing-library/react
// to properly handle click events or visibility checks on the details element.
// So I'm only checking that the content is in the document.
expect(releaseNotes).toBeInTheDocument()
expect(listItemContents).toEqual([
'This is a list item',
'This is another list item with a breaking change',
'This is a list item',
])
})
test('Missing release notes: renders the toast without release notes', () => {
const onRestart = vi.fn()
const onDismiss = vi.fn()
render(
<ToastUpdate
onRestart={onRestart}
onDismiss={onDismiss}
version={testData.version}
releaseNotes={''}
/>
)
// Locators and other constants
const versionText = screen.getByTestId('update-version')
const restartButton = screen.getByRole('button', { name: /restart/i })
const dismissButton = screen.getByRole('button', { name: /got it/i })
const releaseNotes = screen.queryByText(/release notes/i, {
selector: 'details > summary',
})
const releaseNotesListItem = screen.queryByRole('listitem', {
name: /this is a list item/i,
})
expect(versionText).toBeVisible()
expect(versionText).toHaveTextContent(testData.version)
expect(releaseNotes).not.toBeInTheDocument()
expect(releaseNotesListItem).not.toBeInTheDocument()
expect(restartButton).toBeEnabled()
expect(dismissButton).toBeEnabled()
})
})

View File

@ -1,23 +1,14 @@
import toast from 'react-hot-toast'
import { ActionButton } from './ActionButton'
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
import { Marked } from '@ts-stack/markdown'
export function ToastUpdate({
version,
releaseNotes,
onRestart,
onDismiss,
}: {
version: string
releaseNotes?: string
onRestart: () => void
onDismiss: () => void
}) {
const containsBreakingChanges = releaseNotes
?.toLocaleLowerCase()
.includes('breaking')
return (
<div className="inset-0 z-50 grid place-content-center rounded bg-chalkboard-110/50 shadow-md">
<div className="max-w-3xl min-w-[35rem] p-8 rounded bg-chalkboard-10 dark:bg-chalkboard-90">
@ -28,7 +19,7 @@ export function ToastUpdate({
>
v{version}
</span>
<p className="ml-4 text-md text-bold">
<span className="ml-4 text-md text-bold">
A new update has downloaded and will be available next time you
start the app. You can view the release notes{' '}
<a
@ -41,39 +32,15 @@ export function ToastUpdate({
>
here on GitHub.
</a>
</p>
</span>
</div>
{releaseNotes && (
<details
className="my-4 border border-chalkboard-30 dark:border-chalkboard-60 rounded"
open={containsBreakingChanges}
data-testid="release-notes"
>
<summary className="p-2 select-none cursor-pointer">
Release notes
{containsBreakingChanges && (
<strong className="text-destroy-50"> (Breaking changes)</strong>
)}
</summary>
<div
className="parsed-markdown py-2 px-4 mt-2 border-t border-chalkboard-30 dark:border-chalkboard-60 max-h-60 overflow-y-auto"
dangerouslySetInnerHTML={{
__html: Marked.parse(releaseNotes, {
gfm: true,
breaks: true,
sanitize: true,
}),
}}
></div>
</details>
)}
<div className="flex justify-between gap-8">
<ActionButton
Element="button"
iconStart={{
icon: 'arrowRotateRight',
}}
name="restart"
name="Restart app now"
onClick={onRestart}
>
Restart app now
@ -83,10 +50,9 @@ export function ToastUpdate({
iconStart={{
icon: 'checkmark',
}}
name="dismiss"
name="Got it"
onClick={() => {
toast.dismiss()
onDismiss()
}}
>
Got it

View File

@ -1,9 +1,6 @@
import { styleTags, tags as t } from '@lezer/highlight'
export const kclHighlight = styleTags({
'import export': t.moduleKeyword,
ImportItemAs: t.definitionKeyword,
ImportFrom: t.moduleKeyword,
'fn var let const': t.definitionKeyword,
'if else': t.controlKeyword,
return: t.controlKeyword,
@ -11,7 +8,7 @@ export const kclHighlight = styleTags({
nil: t.null,
'AddOp MultOp ExpOp': t.arithmeticOperator,
BangOp: t.logicOperator,
CompOp: t.compareOperator,
CompOp: t.logicOperator,
'Equals Arrow': t.definitionOperator,
PipeOperator: t.controlOperator,
String: t.string,

View File

@ -15,9 +15,8 @@
}
statement[@isGroup=Statement] {
ImportStatement { kw<"import"> ImportItems ImportFrom String } |
FunctionDeclaration { kw<"export">? kw<"fn"> VariableDefinition Equals ParamList Arrow Body } |
VariableDeclaration { kw<"export">? (kw<"var"> | kw<"let"> | kw<"const">)? VariableDefinition Equals expression } |
FunctionDeclaration { kw<"fn"> VariableDefinition Equals ParamList Arrow Body } |
VariableDeclaration { (kw<"var"> | kw<"let"> | kw<"const">)? VariableDefinition Equals expression } |
ReturnStatement { kw<"return"> expression } |
ExpressionStatement { expression }
}
@ -26,9 +25,6 @@ ParamList { "(" commaSep<Parameter { VariableDefinition "?"? (":" type)? }> ")"
Body { "{" statement* "}" }
ImportItems { commaSep1NoTrailingComma<ImportItem> }
ImportItem { identifier (ImportItemAs identifier)? }
expression[@isGroup=Expression] {
String |
Number |
@ -78,8 +74,6 @@ kw<term> { @specialize[@name={term}]<identifier, term> }
commaSep<term> { (term ("," term)*)? ","? }
commaSep1NoTrailingComma<term> { term ("," term)* }
@tokens {
String[isolate] { "'" ("\\" _ | !['\\])* "'" | '"' ("\\" _ | !["\\])* '"' }
@ -90,7 +84,7 @@ commaSep1NoTrailingComma<term> { term ("," term)* }
MultOp { "/" | "*" | "\\" }
ExpOp { "^" }
BangOp { "!" }
CompOp { "==" | "!=" | "<=" | ">=" | "<" | ">" }
CompOp { $[<>] "="? | "!=" | "==" }
Equals { "=" }
Arrow { "=>" }
PipeOperator { "|>" }
@ -112,9 +106,6 @@ commaSep1NoTrailingComma<term> { term ("," term)* }
Shebang { "#!" ![\n]* }
ImportItemAs { "as" }
ImportFrom { "from" }
"(" ")"
"{" "}"
"[" "]"

View File

@ -1,5 +1,4 @@
import { isDesktop } from 'lib/isDesktop'
import { reportRejection } from 'lib/trap'
import { useEffect, useState, useRef } from 'react'
type Path = string
@ -12,51 +11,33 @@ type Path = string
// watcher.addListener(() => { ... }).
export const useFileSystemWatcher = (
callback: (eventType: string, path: Path) => Promise<void>,
paths: Path[]
callback: (path: Path) => void,
dependencyArray: Path[]
): void => {
// Used to track this instance of useFileSystemWatcher.
// Assign to ref so it doesn't change between renders.
const key = useRef(Math.random().toString())
const [output, setOutput] = useState<
{ eventType: string; path: string } | undefined
>(undefined)
// Used to track if paths list changes.
const [pathsTracked, setPathsTracked] = useState<Path[]>([])
// Track a ref to the callback. This is how we get the callback updated
// across the NodeJS<->Browser boundary.
const callbackRef = useRef<{ fn: (path: Path) => void }>({
fn: (_path) => {},
})
useEffect(() => {
if (!output) return
callback(output.eventType, output.path).catch(reportRejection)
}, [output])
callbackRef.current.fn = callback
}, [callback])
// Used to track if dependencyArrray changes.
const [dependencyArrayTracked, setDependencyArrayTracked] = useState<Path[]>(
[]
)
// On component teardown obliterate all watchers.
useEffect(() => {
// The hook is useless on web.
if (!isDesktop()) return
const cbWatcher = (eventType: string, path: string) => {
setOutput({ eventType, path })
}
for (let path of pathsTracked) {
// Because functions don't retain refs between NodeJS-Browser I need to
// pass an identifying key so we can later remove it.
// A way to think of the function call is:
// "For this path, add a new handler with this key"
// "There can be many keys (functions) per path"
// Again if refs were preserved, we wouldn't need to do this. Keys
// gives us uniqueness.
window.electron.watchFileOn(path, key.current, cbWatcher)
}
return () => {
for (let path of pathsTracked) {
window.electron.watchFileOff(path, key.current)
}
window.electron.watchFileObliterate()
}
}, [pathsTracked])
}, [])
function difference<T>(l1: T[], l2: T[]): [T[], T[]] {
return [
@ -65,8 +46,6 @@ export const useFileSystemWatcher = (
]
}
const hasDiff = difference(paths, pathsTracked)[0].length !== 0
// Removing 1 watcher at a time is only possible because in a filesystem,
// a path is unique (there can never be two paths with the same name).
// Otherwise we would have to obliterate() the whole list and reconstruct it.
@ -74,10 +53,19 @@ export const useFileSystemWatcher = (
// The hook is useless on web.
if (!isDesktop()) return
if (!hasDiff) return
const [, pathsRemaining] = difference(pathsTracked, paths)
const [pathsAdded] = difference(paths, pathsTracked)
setPathsTracked(pathsRemaining.concat(pathsAdded))
}, [hasDiff])
const [pathsRemoved, pathsRemaining] = difference(
dependencyArrayTracked,
dependencyArray
)
for (let path of pathsRemoved) {
window.electron.watchFileOff(path)
}
const [pathsAdded] = difference(dependencyArray, dependencyArrayTracked)
for (let path of pathsAdded) {
window.electron.watchFileOn(path, (_eventType: string, path: Path) =>
callbackRef.current.fn(path)
)
}
setDependencyArrayTracked(pathsRemaining.concat(pathsAdded))
}, [difference(dependencyArray, dependencyArrayTracked)[0].length !== 0])
}

View File

@ -293,24 +293,6 @@ code {
which lets you use them with @apply in your CSS, and get
autocomplete in classNames in your JSX.
*/
.parsed-markdown ul,
.parsed-markdown ol {
@apply list-outside pl-4 lg:pl-8 my-2;
}
.parsed-markdown ul li {
@apply list-disc;
}
.parsed-markdown li p {
@apply inline;
}
.parsed-markdown code {
@apply px-1 py-0.5 rounded-sm;
@apply bg-chalkboard-20 text-chalkboard-80;
@apply dark:bg-chalkboard-80 dark:text-chalkboard-30;
}
}
#code-mirror-override .cm-scroller,

View File

@ -8,7 +8,6 @@ import ModalContainer from 'react-modal-promise'
import { isDesktop } from 'lib/isDesktop'
import { AppStreamProvider } from 'AppState'
import { ToastUpdate } from 'components/ToastUpdate'
import { AUTO_UPDATER_TOAST_ID } from 'lib/constants'
// uncomment for xstate inspector
// import { DEV } from 'env'
@ -54,35 +53,17 @@ root.render(
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals()
if (isDesktop()) {
// Listen for update download progress to begin
// to show a loading toast.
window.electron.onUpdateDownloadStart(() => {
const message = `Downloading app update...`
console.log(message)
toast.loading(message, { id: AUTO_UPDATER_TOAST_ID })
})
// Listen for update download errors to show
// an error toast and clear the loading toast.
window.electron.onUpdateError(({ error }) => {
console.error(error)
toast.error('An error occurred while downloading the update.', {
id: AUTO_UPDATER_TOAST_ID,
})
})
window.electron.onUpdateDownloaded(({ version, releaseNotes }) => {
isDesktop() &&
window.electron.onUpdateDownloaded((version: string) => {
const message = `A new update (${version}) was downloaded and will be available next time you open the app.`
console.log(message)
toast.custom(
ToastUpdate({
version,
releaseNotes,
onRestart: () => {
window.electron.appRestart()
},
onDismiss: () => {},
}),
{ duration: 30000, id: AUTO_UPDATER_TOAST_ID }
{ duration: 30000 }
)
})
}

View File

@ -8,8 +8,6 @@ import { EXECUTE_AST_INTERRUPT_ERROR_MESSAGE } from 'lib/constants'
import {
CallExpression,
emptyExecState,
ExecState,
initPromise,
parse,
PathToNode,
@ -40,9 +38,10 @@ export class KclManager {
nonCodeMeta: {
nonCodeNodes: {},
start: [],
digest: null,
},
digest: null,
}
private _execState: ExecState = emptyExecState()
private _programMemory: ProgramMemory = ProgramMemory.empty()
lastSuccessfulProgramMemory: ProgramMemory = ProgramMemory.empty()
private _logs: string[] = []
@ -73,21 +72,11 @@ export class KclManager {
get programMemory() {
return this._programMemory
}
// This is private because callers should be setting the entire execState.
private set programMemory(programMemory) {
set programMemory(programMemory) {
this._programMemory = programMemory
this._programMemoryCallBack(programMemory)
}
set execState(execState) {
this._execState = execState
this.programMemory = execState.memory
}
get execState() {
return this._execState
}
get logs() {
return this._logs
}
@ -206,7 +195,9 @@ export class KclManager {
nonCodeMeta: {
nonCodeNodes: {},
start: [],
digest: null,
},
digest: null,
}
}
@ -262,9 +253,8 @@ export class KclManager {
// Make sure we clear before starting again. End session will do this.
this.engineCommandManager?.endSession()
await this.ensureWasmInit()
const { logs, errors, execState, isInterrupted } = await executeAst({
const { logs, errors, programMemory, isInterrupted } = await executeAst({
ast,
idGenerator: this.execState.idGenerator,
engineCommandManager: this.engineCommandManager,
})
@ -274,7 +264,7 @@ export class KclManager {
this.lints = await lintAst({ ast: ast })
sceneInfra.modelingSend({ type: 'code edit during sketch' })
defaultSelectionFilter(execState.memory, this.engineCommandManager)
defaultSelectionFilter(programMemory, this.engineCommandManager)
if (args.zoomToFit) {
let zoomObjectId: string | undefined = ''
@ -292,7 +282,6 @@ export class KclManager {
type: 'zoom_to_fit',
object_ids: zoomObjectId ? [zoomObjectId] : [], // leave empty to zoom to all objects
padding: 0.1, // padding around the objects
animated: false, // don't animate the zoom for now
},
})
}
@ -305,20 +294,12 @@ export class KclManager {
this._cancelTokens.delete(currentExecutionId)
return
}
// Exit sketch mode if the AST is empty
if (this._isAstEmpty(ast)) {
await this.disableSketchMode()
}
this.logs = logs
// Do not add the errors since the program was interrupted and the error is not a real KCL error
this.addKclErrors(isInterrupted ? [] : errors)
// Reset the next ID index so that we reuse the previous IDs next time.
execState.idGenerator.nextId = 0
this.execState = execState
this.programMemory = programMemory
if (!errors.length) {
this.lastSuccessfulProgramMemory = execState.memory
this.lastSuccessfulProgramMemory = programMemory
}
this.ast = { ...ast }
this._executeCallback()
@ -356,19 +337,17 @@ export class KclManager {
await codeManager.writeToFile()
this._ast = { ...newAst }
const { logs, errors, execState } = await executeAst({
const { logs, errors, programMemory } = await executeAst({
ast: newAst,
idGenerator: this.execState.idGenerator,
engineCommandManager: this.engineCommandManager,
useFakeExecutor: true,
})
this._logs = logs
this._kclErrors = errors
this._execState = execState
this._programMemory = execState.memory
this._programMemory = programMemory
if (!errors.length) {
this.lastSuccessfulProgramMemory = execState.memory
this.lastSuccessfulProgramMemory = programMemory
}
if (updates !== 'artifactRanges') return
@ -573,24 +552,6 @@ export class KclManager {
defaultSelectionFilter() {
defaultSelectionFilter(this.programMemory, this.engineCommandManager)
}
/**
* We can send a single command of 'enable_sketch_mode' or send this in a batched request.
* When there is no code in the KCL editor we should be sending 'sketch_mode_disable' since any previous half finished
* code could leave the state of the application in sketch mode on the engine side.
*/
async disableSketchMode() {
await this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: { type: 'sketch_mode_disable' },
})
}
// Determines if there is no KCL code which means it is executing a blank KCL file
_isAstEmpty(ast: Program) {
return ast.start === 0 && ast.end === 0 && ast.body.length === 0
}
}
function defaultSelectionFilter(

File diff suppressed because it is too large Load Diff

View File

@ -14,9 +14,9 @@ const mySketch001 = startSketchOn('XY')
|> lineTo([-1.59, -1.54], %)
|> lineTo([0.46, -5.82], %)
// |> rx(45, %)`
const execState = await enginelessExecutor(parse(code))
const programMemory = await enginelessExecutor(parse(code))
// @ts-ignore
const sketch001 = execState.memory.get('mySketch001')
const sketch001 = programMemory?.get('mySketch001')
expect(sketch001).toEqual({
type: 'UserVal',
__meta: [{ sourceRange: [46, 71] }],
@ -68,9 +68,9 @@ const mySketch001 = startSketchOn('XY')
|> lineTo([0.46, -5.82], %)
// |> rx(45, %)
|> extrude(2, %)`
const execState = await enginelessExecutor(parse(code))
const programMemory = await enginelessExecutor(parse(code))
// @ts-ignore
const sketch001 = execState.memory.get('mySketch001')
const sketch001 = programMemory?.get('mySketch001')
expect(sketch001).toEqual({
type: 'Solid',
id: expect.any(String),
@ -148,10 +148,9 @@ const sk2 = startSketchOn('XY')
|> extrude(2, %)
`
const execState = await enginelessExecutor(parse(code))
const programMemory = execState.memory
const programMemory = await enginelessExecutor(parse(code))
// @ts-ignore
const geos = [programMemory.get('theExtrude'), programMemory.get('sk2')]
const geos = [programMemory?.get('theExtrude'), programMemory?.get('sk2')]
expect(geos).toEqual([
{
type: 'Solid',
@ -172,6 +171,7 @@ const sk2 = startSketchOn('XY')
start: 114,
type: 'TagDeclarator',
value: 'p',
digest: null,
},
id: expect.any(String),
sourceRange: [95, 117],
@ -222,6 +222,7 @@ const sk2 = startSketchOn('XY')
start: 114,
type: 'TagDeclarator',
value: 'p',
digest: null,
},
__geoMeta: {
id: expect.any(String),
@ -264,6 +265,7 @@ const sk2 = startSketchOn('XY')
start: 417,
type: 'TagDeclarator',
value: 'o',
digest: null,
},
id: expect.any(String),
sourceRange: [399, 420],
@ -314,6 +316,7 @@ const sk2 = startSketchOn('XY')
start: 417,
type: 'TagDeclarator',
value: 'o',
digest: null,
},
__geoMeta: {
id: expect.any(String),

View File

@ -18,7 +18,6 @@ export default class CodeManager {
#updateState: (arg: string) => void = () => {}
private _currentFilePath: string | null = null
private _hotkeys: { [key: string]: () => void } = {}
private timeoutWriter: ReturnType<typeof setTimeout> | undefined = undefined
constructor() {
if (isDesktop()) {
@ -116,11 +115,7 @@ export default class CodeManager {
async writeToFile() {
if (isDesktop()) {
// Only write our buffer contents to file once per second. Any faster
// and file-system watchers which read, will receive empty data during
// writes.
clearTimeout(this.timeoutWriter)
this.timeoutWriter = setTimeout(() => {
setTimeout(() => {
// Wait one event loop to give a chance for params to be set
// Save the file to disk
this._currentFilePath &&
@ -131,7 +126,7 @@ export default class CodeManager {
console.error('error saving file', err)
toast.error('Error saving file, please check file permissions')
})
}, 1000)
})
} else {
safeLSSetItem(PERSIST_CODE_KEY, this.code)
}

View File

@ -73,6 +73,7 @@ const newVar = myVar + 1`
start: 89,
type: 'TagDeclarator',
value: 'myPath',
digest: null,
},
},
{
@ -98,6 +99,7 @@ const newVar = myVar + 1`
start: 143,
type: 'TagDeclarator',
value: 'rightPath',
digest: null,
},
},
])
@ -199,6 +201,7 @@ const newVar = myVar + 1`
start: 109,
type: 'TagDeclarator',
value: 'myPath',
digest: null,
},
},
{
@ -440,6 +443,6 @@ async function exe(
) {
const ast = parse(code)
const execState = await enginelessExecutor(ast, programMemory)
return execState.memory
const result = await enginelessExecutor(ast, programMemory)
return result
}

View File

@ -4,14 +4,11 @@ import {
ProgramMemory,
programMemoryInit,
kclLint,
emptyExecState,
ExecState,
} from 'lang/wasm'
import { enginelessExecutor } from 'lib/testHelpers'
import { EngineCommandManager } from 'lang/std/engineConnection'
import { KCLError } from 'lang/errors'
import { Diagnostic } from '@codemirror/lint'
import { IdGenerator } from 'wasm-lib/kcl/bindings/IdGenerator'
export type ToolTip =
| 'lineTo'
@ -50,18 +47,16 @@ export async function executeAst({
engineCommandManager,
useFakeExecutor = false,
programMemoryOverride,
idGenerator,
}: {
ast: Program
engineCommandManager: EngineCommandManager
useFakeExecutor?: boolean
programMemoryOverride?: ProgramMemory
idGenerator?: IdGenerator
isInterrupted?: boolean
}): Promise<{
logs: string[]
errors: KCLError[]
execState: ExecState
programMemory: ProgramMemory
isInterrupted: boolean
}> {
try {
@ -70,21 +65,15 @@ export async function executeAst({
// eslint-disable-next-line @typescript-eslint/no-floating-promises
engineCommandManager.startNewSession()
}
const execState = await (useFakeExecutor
const programMemory = await (useFakeExecutor
? enginelessExecutor(ast, programMemoryOverride || programMemoryInit())
: _executor(
ast,
programMemoryInit(),
idGenerator,
engineCommandManager,
false
))
: _executor(ast, programMemoryInit(), engineCommandManager, false))
await engineCommandManager.waitForAllCommands()
return {
logs: [],
errors: [],
execState,
programMemory,
isInterrupted: false,
}
} catch (e: any) {
@ -100,7 +89,7 @@ export async function executeAst({
return {
errors: [e],
logs: [],
execState: emptyExecState(),
programMemory: ProgramMemory.empty(),
isInterrupted,
}
} else {
@ -108,7 +97,7 @@ export async function executeAst({
return {
logs: [e],
errors: [],
execState: emptyExecState(),
programMemory: ProgramMemory.empty(),
isInterrupted,
}
}

View File

@ -100,15 +100,15 @@ describe('Testing findUniqueName', () => {
it('should find a unique name', () => {
const result = findUniqueName(
JSON.stringify([
{ type: 'Identifier', name: 'yo01', start: 0, end: 0 },
{ type: 'Identifier', name: 'yo02', start: 0, end: 0 },
{ type: 'Identifier', name: 'yo03', start: 0, end: 0 },
{ type: 'Identifier', name: 'yo04', start: 0, end: 0 },
{ type: 'Identifier', name: 'yo05', start: 0, end: 0 },
{ type: 'Identifier', name: 'yo06', start: 0, end: 0 },
{ type: 'Identifier', name: 'yo07', start: 0, end: 0 },
{ type: 'Identifier', name: 'yo08', start: 0, end: 0 },
{ type: 'Identifier', name: 'yo09', start: 0, end: 0 },
{ type: 'Identifier', name: 'yo01', start: 0, end: 0, digest: null },
{ type: 'Identifier', name: 'yo02', start: 0, end: 0, digest: null },
{ type: 'Identifier', name: 'yo03', start: 0, end: 0, digest: null },
{ type: 'Identifier', name: 'yo04', start: 0, end: 0, digest: null },
{ type: 'Identifier', name: 'yo05', start: 0, end: 0, digest: null },
{ type: 'Identifier', name: 'yo06', start: 0, end: 0, digest: null },
{ type: 'Identifier', name: 'yo07', start: 0, end: 0, digest: null },
{ type: 'Identifier', name: 'yo08', start: 0, end: 0, digest: null },
{ type: 'Identifier', name: 'yo09', start: 0, end: 0, digest: null },
] satisfies Identifier[]),
'yo',
2
@ -123,7 +123,8 @@ describe('Testing addSketchTo', () => {
body: [],
start: 0,
end: 0,
nonCodeMeta: { nonCodeNodes: {}, start: [] },
nonCodeMeta: { nonCodeNodes: {}, start: [], digest: null },
digest: null,
},
'yz'
)
@ -219,11 +220,11 @@ yo2 = hmm([identifierGuy + 5])`
it('should move a binary expression into a new variable', async () => {
const ast = parse(code)
if (err(ast)) throw ast
const execState = await enginelessExecutor(ast)
const programMemory = await enginelessExecutor(ast)
const startIndex = code.indexOf('100 + 100') + 1
const { modifiedAst } = moveValueIntoNewVariable(
ast,
execState.memory,
programMemory,
[startIndex, startIndex],
'newVar'
)
@ -234,11 +235,11 @@ yo2 = hmm([identifierGuy + 5])`
it('should move a value into a new variable', async () => {
const ast = parse(code)
if (err(ast)) throw ast
const execState = await enginelessExecutor(ast)
const programMemory = await enginelessExecutor(ast)
const startIndex = code.indexOf('2.8') + 1
const { modifiedAst } = moveValueIntoNewVariable(
ast,
execState.memory,
programMemory,
[startIndex, startIndex],
'newVar'
)
@ -249,11 +250,11 @@ yo2 = hmm([identifierGuy + 5])`
it('should move a callExpression into a new variable', async () => {
const ast = parse(code)
if (err(ast)) throw ast
const execState = await enginelessExecutor(ast)
const programMemory = await enginelessExecutor(ast)
const startIndex = code.indexOf('def(')
const { modifiedAst } = moveValueIntoNewVariable(
ast,
execState.memory,
programMemory,
[startIndex, startIndex],
'newVar'
)
@ -264,11 +265,11 @@ yo2 = hmm([identifierGuy + 5])`
it('should move a binary expression with call expression into a new variable', async () => {
const ast = parse(code)
if (err(ast)) throw ast
const execState = await enginelessExecutor(ast)
const programMemory = await enginelessExecutor(ast)
const startIndex = code.indexOf('jkl(') + 1
const { modifiedAst } = moveValueIntoNewVariable(
ast,
execState.memory,
programMemory,
[startIndex, startIndex],
'newVar'
)
@ -279,11 +280,11 @@ yo2 = hmm([identifierGuy + 5])`
it('should move a identifier into a new variable', async () => {
const ast = parse(code)
if (err(ast)) throw ast
const execState = await enginelessExecutor(ast)
const programMemory = await enginelessExecutor(ast)
const startIndex = code.indexOf('identifierGuy +') + 1
const { modifiedAst } = moveValueIntoNewVariable(
ast,
execState.memory,
programMemory,
[startIndex, startIndex],
'newVar'
)
@ -464,7 +465,7 @@ describe('Testing deleteSegmentFromPipeExpression', () => {
|> line([306.21, 198.87], %)`
const ast = parse(code)
if (err(ast)) throw ast
const execState = await enginelessExecutor(ast)
const programMemory = await enginelessExecutor(ast)
const lineOfInterest = 'line([306.21, 198.85], %, $a)'
const range: [number, number] = [
code.indexOf(lineOfInterest),
@ -474,7 +475,7 @@ describe('Testing deleteSegmentFromPipeExpression', () => {
const modifiedAst = deleteSegmentFromPipeExpression(
[],
ast,
execState.memory,
programMemory,
code,
pathToNode
)
@ -542,7 +543,7 @@ ${!replace1 ? ` |> ${line}\n` : ''} |> angledLine([-65, ${
const code = makeCode(line)
const ast = parse(code)
if (err(ast)) throw ast
const execState = await enginelessExecutor(ast)
const programMemory = await enginelessExecutor(ast)
const lineOfInterest = line
const range: [number, number] = [
code.indexOf(lineOfInterest),
@ -553,7 +554,7 @@ ${!replace1 ? ` |> ${line}\n` : ''} |> angledLine([-65, ${
const modifiedAst = deleteSegmentFromPipeExpression(
dependentSegments,
ast,
execState.memory,
programMemory,
code,
pathToNode
)
@ -631,7 +632,7 @@ describe('Testing removeSingleConstraintInfo', () => {
const ast = parse(code)
if (err(ast)) throw ast
const execState = await enginelessExecutor(ast)
const programMemory = await enginelessExecutor(ast)
const lineOfInterest = expectedFinish.split('(')[0] + '('
const range: [number, number] = [
code.indexOf(lineOfInterest) + 1,
@ -660,7 +661,7 @@ describe('Testing removeSingleConstraintInfo', () => {
pathToNode,
argPosition,
ast,
execState.memory
programMemory
)
if (!mod) return new Error('mod is undefined')
const recastCode = recast(mod.modifiedAst)
@ -685,7 +686,7 @@ describe('Testing removeSingleConstraintInfo', () => {
const ast = parse(code)
if (err(ast)) throw ast
const execState = await enginelessExecutor(ast)
const programMemory = await enginelessExecutor(ast)
const lineOfInterest = expectedFinish.split('(')[0] + '('
const range: [number, number] = [
code.indexOf(lineOfInterest) + 1,
@ -710,7 +711,7 @@ describe('Testing removeSingleConstraintInfo', () => {
pathToNode,
argPosition,
ast,
execState.memory
programMemory
)
if (!mod) return new Error('mod is undefined')
const recastCode = recast(mod.modifiedAst)
@ -881,7 +882,7 @@ sketch002 = startSketchOn({
// const lineOfInterest = 'line([-2.94, 2.7], %)'
const ast = parse(codeBefore)
if (err(ast)) throw ast
const execState = await enginelessExecutor(ast)
const programMemory = await enginelessExecutor(ast)
// deleteFromSelection
const range: [number, number] = [
@ -894,7 +895,7 @@ sketch002 = startSketchOn({
range,
type,
},
execState.memory,
programMemory,
async () => {
await new Promise((resolve) => setTimeout(resolve, 100))
return {

View File

@ -241,6 +241,7 @@ export function mutateObjExpProp(
value: updateWith,
start: 0,
end: 0,
digest: null,
})
}
}
@ -500,7 +501,6 @@ export function sketchOnExtrudedFace(
createIdentifier(extrudeName ? extrudeName : oldSketchName),
_tag,
]),
undefined,
'const'
)
@ -578,6 +578,7 @@ export function createLiteral(value: string | number): Literal {
end: 0,
value,
raw: `${value}`,
digest: null,
}
}
@ -586,7 +587,7 @@ export function createTagDeclarator(value: string): TagDeclarator {
type: 'TagDeclarator',
start: 0,
end: 0,
digest: null,
value,
}
}
@ -596,7 +597,7 @@ export function createIdentifier(name: string): Identifier {
type: 'Identifier',
start: 0,
end: 0,
digest: null,
name,
}
}
@ -606,6 +607,7 @@ export function createPipeSubstitution(): PipeSubstitution {
type: 'PipeSubstitution',
start: 0,
end: 0,
digest: null,
}
}
@ -621,11 +623,12 @@ export function createCallExpressionStdLib(
type: 'Identifier',
start: 0,
end: 0,
digest: null,
name,
},
optional: false,
arguments: args,
digest: null,
}
}
@ -641,11 +644,12 @@ export function createCallExpression(
type: 'Identifier',
start: 0,
end: 0,
digest: null,
name,
},
optional: false,
arguments: args,
digest: null,
}
}
@ -656,7 +660,7 @@ export function createArrayExpression(
type: 'ArrayExpression',
start: 0,
end: 0,
digest: null,
nonCodeMeta: nonCodeMetaEmpty(),
elements,
}
@ -669,7 +673,7 @@ export function createPipeExpression(
type: 'PipeExpression',
start: 0,
end: 0,
digest: null,
body,
nonCodeMeta: nonCodeMetaEmpty(),
}
@ -678,25 +682,23 @@ export function createPipeExpression(
export function createVariableDeclaration(
varName: string,
init: VariableDeclarator['init'],
visibility: VariableDeclaration['visibility'] = 'default',
kind: VariableDeclaration['kind'] = 'const'
): VariableDeclaration {
return {
type: 'VariableDeclaration',
start: 0,
end: 0,
digest: null,
declarations: [
{
type: 'VariableDeclarator',
start: 0,
end: 0,
digest: null,
id: createIdentifier(varName),
init,
},
],
visibility,
kind,
}
}
@ -708,14 +710,14 @@ export function createObjectExpression(properties: {
type: 'ObjectExpression',
start: 0,
end: 0,
digest: null,
nonCodeMeta: nonCodeMetaEmpty(),
properties: Object.entries(properties).map(([key, value]) => ({
type: 'ObjectProperty',
start: 0,
end: 0,
key: createIdentifier(key),
digest: null,
value,
})),
}
@ -729,7 +731,7 @@ export function createUnaryExpression(
type: 'UnaryExpression',
start: 0,
end: 0,
digest: null,
operator,
argument,
}
@ -744,7 +746,7 @@ export function createBinaryExpression([left, operator, right]: [
type: 'BinaryExpression',
start: 0,
end: 0,
digest: null,
operator,
left,
right,
@ -1134,5 +1136,5 @@ export async function deleteFromSelection(
}
const nonCodeMetaEmpty = () => {
return { nonCodeNodes: {}, start: [] }
return { nonCodeNodes: {}, start: [], digest: null }
}

View File

@ -41,7 +41,7 @@ beforeAll(async () => {
},
})
})
}, 30_000)
}, 20_000)
afterAll(() => {
engineCommandManager.tearDown()
@ -620,7 +620,7 @@ describe('Testing button states', () => {
it('should return true when body exists and segment is selected', async () => {
await runButtonStateTest(codeWithBody, `line([10, 0], %)`, true)
})
it('should return false when body exists and not a segment is selected', async () => {
it('hould return false when body exists and not a segment is selected', async () => {
await runButtonStateTest(codeWithBody, `close(%)`, false)
})
})

View File

@ -1,7 +1,5 @@
import {
CallExpression,
Expr,
Identifier,
ObjectExpression,
PathToNode,
Program,
@ -29,7 +27,7 @@ import {
sketchLineHelperMap,
} from '../std/sketch'
import { err, trap } from 'lib/trap'
import { Selections } from 'lib/selections'
import { Selections, canFilletSelection } from 'lib/selections'
import { KclCommandValue } from 'lib/commandTypes'
import {
ArtifactGraph,
@ -68,10 +66,7 @@ export function modifyAstCloneWithFilletAndTag(
const artifactGraph = engineCommandManager.artifactGraph
// Step 1: modify ast with tags and group them by extrude nodes (bodies)
const extrudeToTagsMap: Map<
PathToNode,
Array<{ tag: string; selectionType: string }>
> = new Map()
const extrudeToTagsMap: Map<PathToNode, string[]> = new Map()
const lookupMap: Map<string, PathToNode> = new Map() // work around for Map key comparison
for (const selectionRange of selection.codeBasedSelections) {
@ -79,7 +74,6 @@ export function modifyAstCloneWithFilletAndTag(
codeBasedSelections: [selectionRange],
otherSelections: [],
}
const selectionType = singleSelection.codeBasedSelections[0].type
const result = getPathToExtrudeForSegmentSelection(
clonedAstForGetExtrude,
@ -95,7 +89,6 @@ export function modifyAstCloneWithFilletAndTag(
)
if (err(tagResult)) return tagResult
const { tag } = tagResult
const tagInfo = { tag, selectionType }
// Group tags by their corresponding extrude node
const extrudeKey = JSON.stringify(pathToExtrudeNode)
@ -103,29 +96,23 @@ export function modifyAstCloneWithFilletAndTag(
if (lookupMap.has(extrudeKey)) {
const existingPath = lookupMap.get(extrudeKey)
if (!existingPath) return new Error('Path to extrude node not found.')
extrudeToTagsMap.get(existingPath)?.push(tagInfo)
extrudeToTagsMap.get(existingPath)?.push(tag)
} else {
lookupMap.set(extrudeKey, pathToExtrudeNode)
extrudeToTagsMap.set(pathToExtrudeNode, [tagInfo])
extrudeToTagsMap.set(pathToExtrudeNode, [tag])
}
}
// Step 2: Apply fillet(s) for each extrude node (body)
let pathToFilletNodes: Array<PathToNode> = []
for (const [pathToExtrudeNode, tagInfos] of extrudeToTagsMap.entries()) {
for (const [pathToExtrudeNode, tags] of extrudeToTagsMap.entries()) {
// Create a fillet expression with multiple tags
const radiusValue =
'variableName' in radius ? radius.variableIdentifierAst : radius.valueAst
const tagCalls = tagInfos.map(({ tag, selectionType }) => {
return getEdgeTagCall(tag, selectionType)
})
const firstTag = tagCalls[0] // can be Identifier or CallExpression (for opposite and adjacent edges)
const filletCall = createCallExpressionStdLib('fillet', [
createObjectExpression({
radius: radiusValue,
tags: createArrayExpression(tagCalls),
tags: createArrayExpression(tags.map((tag) => createIdentifier(tag))),
}),
createPipeSubstitution(),
])
@ -157,7 +144,7 @@ export function modifyAstCloneWithFilletAndTag(
pathToFilletNode = getPathToNodeOfFilletLiteral(
pathToExtrudeNode,
extrudeDeclarator,
firstTag
tags[0]
)
pathToFilletNodes.push(pathToFilletNode)
} else if (extrudeDeclarator.init.type === 'PipeExpression') {
@ -178,7 +165,7 @@ export function modifyAstCloneWithFilletAndTag(
pathToFilletNode = getPathToNodeOfFilletLiteral(
pathToExtrudeNode,
extrudeDeclarator,
firstTag
tags[0]
)
pathToFilletNodes.push(pathToFilletNode)
} else {
@ -289,21 +276,6 @@ function mutateAstWithTagForSketchSegment(
return { modifiedAst: astClone, tag }
}
function getEdgeTagCall(
tag: string,
selectionType: string
): Identifier | CallExpression {
let tagCall: Expr = createIdentifier(tag)
// Modify the tag based on selectionType
if (selectionType === 'edge') {
tagCall = createCallExpressionStdLib('getOppositeEdge', [tagCall])
} else if (selectionType === 'adjacent-edge') {
tagCall = createCallExpressionStdLib('getNextAdjacentEdge', [tagCall])
}
return tagCall
}
function locateExtrudeDeclarator(
node: Program,
pathToExtrudeNode: PathToNode
@ -339,7 +311,7 @@ function locateExtrudeDeclarator(
function getPathToNodeOfFilletLiteral(
pathToExtrudeNode: PathToNode,
extrudeDeclarator: VariableDeclarator,
tag: Identifier | CallExpression
tag: string
): PathToNode {
let pathToFilletObj: PathToNode = []
let inFillet = false
@ -375,30 +347,12 @@ function getPathToNodeOfFilletLiteral(
]
}
function hasTag(
node: ObjectExpression,
tag: Identifier | CallExpression
): boolean {
function hasTag(node: ObjectExpression, tag: string): boolean {
return node.properties.some((prop) => {
if (prop.key.name === 'tags' && prop.value.type === 'ArrayExpression') {
// if selection is a base edge:
if (tag.type === 'Identifier') {
return prop.value.elements.some(
(element) =>
element.type === 'Identifier' && element.name === tag.name
)
}
// if selection is an adjacent or opposite edge:
if (tag.type === 'CallExpression') {
return prop.value.elements.some(
(element) =>
element.type === 'CallExpression' &&
element.callee.name === tag.callee.name && // edge location
element.arguments[0].type === 'Identifier' &&
tag.arguments[0].type === 'Identifier' &&
element.arguments[0].name === tag.arguments[0].name // tag name
)
}
return prop.value.elements.some(
(element) => element.type === 'Identifier' && element.name === tag
)
}
return false
})
@ -429,7 +383,7 @@ export const hasValidFilletSelection = ({
ast: Program
code: string
}) => {
// check if there is anything filletable in the scene
// case 0: check if there is anything filletable in the scene
let extrudeExists = false
traverse(ast, {
enter(node) {
@ -440,88 +394,65 @@ export const hasValidFilletSelection = ({
})
if (!extrudeExists) return false
// check if nothing is selected
if (selectionRanges.codeBasedSelections.length === 0) {
return true
// case 1: nothing selected, test whether the extrusion exists
if (selectionRanges) {
if (selectionRanges.codeBasedSelections.length === 0) {
return true
}
const range0 = selectionRanges.codeBasedSelections[0].range[0]
const codeLength = code.length
if (range0 === codeLength) {
return true
}
}
// check if selection is last string in code
if (selectionRanges.codeBasedSelections[0].range[0] === code.length) {
return true
}
// selection exists:
for (const selection of selectionRanges.codeBasedSelections) {
// check if all selections are in sketchLineHelperMap
const path = getNodePathFromSourceRange(ast, selection.range)
const segmentNode = getNodeFromPath<CallExpression>(
ast,
path,
'CallExpression'
// case 2: sketch segment selected, test whether it is extruded
// TODO: add loft / sweep check
if (selectionRanges.codeBasedSelections.length > 0) {
const isExtruded = hasSketchPipeBeenExtruded(
selectionRanges.codeBasedSelections[0],
ast
)
if (err(segmentNode)) return false
if (segmentNode.node.type !== 'CallExpression') {
return false
}
if (!(segmentNode.node.callee.name in sketchLineHelperMap)) {
return false
}
// check if selection is extruded
// TODO: option 1 : extrude is in the sketch pipe
// option 2: extrude is outside the sketch pipe
const extrudeExists = hasSketchPipeBeenExtruded(selection, ast)
if (err(extrudeExists)) {
return false
}
if (!extrudeExists) {
return false
}
// check if tag exists for the selection
let tagExists = false
let tag = ''
traverse(segmentNode.node, {
enter(node) {
if (node.type === 'TagDeclarator') {
tagExists = true
tag = node.value
if (isExtruded) {
const pathToSelectedNode = getNodePathFromSourceRange(
ast,
selectionRanges.codeBasedSelections[0].range
)
const segmentNode = getNodeFromPath<CallExpression>(
ast,
pathToSelectedNode,
'CallExpression'
)
if (err(segmentNode)) return false
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,
})
// edge has already been filleted
if (
['edge', 'default'].includes(
selectionRanges.codeBasedSelections[0].type
) &&
edges.includes('baseEdge')
)
return false
return true
} else {
return false
}
},
})
// check if tag is used in fillet
if (tagExists) {
// create tag call
let tagCall: Expr = getEdgeTagCall(tag, selection.type)
// check if tag is used in fillet
let inFillet = false
let tagUsedInFillet = false
traverse(ast, {
enter(node) {
if (node.type === 'CallExpression' && node.callee.name === 'fillet') {
inFillet = true
}
if (inFillet && node.type === 'ObjectExpression') {
if (hasTag(node, tagCall)) {
tagUsedInFillet = true
}
}
},
leave(node) {
if (node.type === 'CallExpression' && node.callee.name === 'fillet') {
inFillet = false
}
},
})
if (tagUsedInFillet) {
return false
}
} else {
return false
}
}
return true
return canFilletSelection(selectionRanges)
}
type EdgeTypes =

View File

@ -45,11 +45,11 @@ variableBelowShouldNotBeIncluded = 3
const rangeStart = code.indexOf('// selection-range-7ish-before-this') - 7
const ast = parse(code)
if (err(ast)) throw ast
const execState = await enginelessExecutor(ast)
const programMemory = await enginelessExecutor(ast)
const { variables, bodyPath, insertIndex } = findAllPreviousVariables(
ast,
execState.memory,
programMemory,
[rangeStart, rangeStart]
)
expect(variables).toEqual([
@ -351,11 +351,11 @@ part001 = startSketchAt([-1.41, 3.46])
const ast = parse(exampleCode)
if (err(ast)) throw ast
const execState = await enginelessExecutor(ast)
const programMemory = await enginelessExecutor(ast)
const result = hasExtrudeSketch({
ast,
selection: { type: 'default', range: [100, 101] },
programMemory: execState.memory,
programMemory,
})
expect(result).toEqual(true)
})
@ -370,11 +370,11 @@ part001 = startSketchAt([-1.41, 3.46])
const ast = parse(exampleCode)
if (err(ast)) throw ast
const execState = await enginelessExecutor(ast)
const programMemory = await enginelessExecutor(ast)
const result = hasExtrudeSketch({
ast,
selection: { type: 'default', range: [100, 101] },
programMemory: execState.memory,
programMemory,
})
expect(result).toEqual(true)
})
@ -383,11 +383,11 @@ part001 = startSketchAt([-1.41, 3.46])
const ast = parse(exampleCode)
if (err(ast)) throw ast
const execState = await enginelessExecutor(ast)
const programMemory = await enginelessExecutor(ast)
const result = hasExtrudeSketch({
ast,
selection: { type: 'default', range: [10, 11] },
programMemory: execState.memory,
programMemory,
})
expect(result).toEqual(false)
})

View File

@ -28,7 +28,6 @@ import {
getConstraintType,
} from './std/sketchcombos'
import { err } from 'lib/trap'
import { ImportStatement } from 'wasm-lib/kcl/bindings/ImportStatement'
/**
* Retrieves a node from a given path within a Program node structure, optionally stopping at a specified node type.
@ -121,12 +120,7 @@ export function getNodeFromPathCurry(
}
function moreNodePathFromSourceRange(
node:
| Expr
| ImportStatement
| ExpressionStatement
| VariableDeclaration
| ReturnStatement,
node: Expr | ExpressionStatement | VariableDeclaration | ReturnStatement,
sourceRange: Selection['range'],
previousPath: PathToNode = [['body', '']]
): PathToNode {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 378 KiB

After

Width:  |  Height:  |  Size: 357 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 613 KiB

After

Width:  |  Height:  |  Size: 577 KiB

View File

@ -50,11 +50,6 @@ export enum ExportIntent {
Make = 'make',
}
export interface ExportInfo {
intent: ExportIntent
name: string
}
type ClientMetrics = Models['ClientMetrics_type']
interface WebRTCClientMetrics extends ClientMetrics {
@ -1359,7 +1354,7 @@ export class EngineCommandManager extends EventTarget {
* export in progress. Otherwise it is an enum value of the intent.
* Another export cannot be started if one is already in progress.
*/
private _exportInfo: ExportInfo | null = null
private _exportIntent: ExportIntent | null = null
_commandLogCallBack: (command: CommandLog[]) => void = () => {}
subscriptions: {
@ -1415,12 +1410,12 @@ export class EngineCommandManager extends EventTarget {
(() => {}) as any
kclManager: null | KclManager = null
set exportInfo(info: ExportInfo | null) {
this._exportInfo = info
set exportIntent(intent: ExportIntent | null) {
this._exportIntent = intent
}
get exportInfo() {
return this._exportInfo
get exportIntent() {
return this._exportIntent
}
start({
@ -1612,7 +1607,7 @@ export class EngineCommandManager extends EventTarget {
// because in all other cases we send JSON strings. But in the case of
// export we send a binary blob.
// Pass this to our export function.
if (this.exportInfo === null || this.pendingExport === undefined) {
if (this.exportIntent === null || this.pendingExport === undefined) {
toast.error(
'Export intent was not set, but export data was received'
)
@ -1622,7 +1617,7 @@ export class EngineCommandManager extends EventTarget {
return
}
switch (this.exportInfo.intent) {
switch (this.exportIntent) {
case ExportIntent.Save: {
exportSave(event.data, this.pendingExport.toastId).then(() => {
this.pendingExport?.resolve(null)
@ -1630,22 +1625,21 @@ export class EngineCommandManager extends EventTarget {
break
}
case ExportIntent.Make: {
exportMake(
event.data,
this.exportInfo.name,
this.pendingExport.toastId
).then((result) => {
if (result) {
this.pendingExport?.resolve(null)
} else {
this.pendingExport?.reject('Failed to make export')
}
}, this.pendingExport?.reject)
exportMake(event.data, this.pendingExport.toastId).then(
(result) => {
if (result) {
this.pendingExport?.resolve(null)
} else {
this.pendingExport?.reject('Failed to make export')
}
},
this.pendingExport?.reject
)
break
}
}
// Set the export intent back to null.
this.exportInfo = null
this.exportIntent = null
return
}
@ -1959,15 +1953,15 @@ export class EngineCommandManager extends EventTarget {
return Promise.resolve(null)
} else if (cmd.type === 'export') {
const promise = new Promise<null>((resolve, reject) => {
if (this.exportInfo === null) {
if (this.exportInfo === null) {
if (this.exportIntent === null) {
if (this.exportIntent === null) {
toast.error('Export intent was not set, but export is being sent')
console.error('Export intent was not set, but export is being sent')
return
}
}
const toastId = toast.loading(
this.exportInfo.intent === ExportIntent.Save
this.exportIntent === ExportIntent.Save
? EXPORT_TOAST_MESSAGES.START
: MAKE_TOAST_MESSAGES.START
)
@ -1981,7 +1975,7 @@ export class EngineCommandManager extends EventTarget {
resolve(passThrough)
},
reject: (reason: string) => {
this.exportInfo = null
this.exportIntent = null
reject(reason)
},
commandId: command.cmd_id,

View File

@ -18,7 +18,7 @@ class FileSystemManager {
return Promise.resolve(window.electron.path.join(dir, path))
}
async readFile(path: string): Promise<Uint8Array> {
async readFile(path: string): Promise<Uint8Array | void> {
// Using local file system only works from desktop.
if (!isDesktop()) {
return Promise.reject(

View File

@ -117,11 +117,11 @@ describe('testing changeSketchArguments', () => {
const ast = parse(code)
if (err(ast)) return ast
const execState = await enginelessExecutor(ast)
const programMemory = await enginelessExecutor(ast)
const sourceStart = code.indexOf(lineToChange)
const changeSketchArgsRetVal = changeSketchArguments(
ast,
execState.memory,
programMemory,
{
type: 'sourceRange',
sourceRange: [sourceStart, sourceStart + lineToChange.length],
@ -150,12 +150,12 @@ mySketch001 = startSketchOn('XY')
const ast = parse(code)
if (err(ast)) return ast
const execState = await enginelessExecutor(ast)
const programMemory = await enginelessExecutor(ast)
const sourceStart = code.indexOf(lineToChange)
expect(sourceStart).toBe(89)
const newSketchLnRetVal = addNewSketchLn({
node: ast,
programMemory: execState.memory,
programMemory,
input: {
type: 'straight-segment',
from: [0, 0],
@ -186,7 +186,7 @@ mySketch001 = startSketchOn('XY')
const modifiedAst2 = addCloseToPipe({
node: ast,
programMemory: execState.memory,
programMemory,
pathToNode: [
['body', ''],
[0, 'index'],
@ -230,7 +230,7 @@ describe('testing addTagForSketchOnFace', () => {
const pathToNode = getNodePathFromSourceRange(ast, sourceRange)
const sketchOnFaceRetVal = addTagForSketchOnFace(
{
// previousProgramMemory: execState.memory, // redundant?
// previousProgramMemory: programMemory, // redundant?
pathToNode,
node: ast,
},

View File

@ -1823,10 +1823,11 @@ export const updateStartProfileAtArgs: SketchLineHelper['updateArgs'] = ({
start: 0,
end: 0,
body: [],
digest: null,
nonCodeMeta: {
start: [],
nonCodeNodes: [],
digest: null,
},
},
pathToNode,

Some files were not shown because too many files have changed in this diff Show More