Compare commits
63 Commits
kurt-rever
...
franknoiro
Author | SHA1 | Date | |
---|---|---|---|
1a9a2ef51e | |||
ebade29ed0 | |||
582d37e51b | |||
4ef9429842 | |||
0577b6a984 | |||
7d44de0c12 | |||
f7d5313588 | |||
bd4783e885 | |||
8794696b26 | |||
1c2e415c70 | |||
248ef8ebb3 | |||
fbac9935fe | |||
b4c171a347 | |||
0811d9fa4e | |||
1efc2b9762 | |||
d361bda180 | |||
1d3ade114f | |||
3382b66075 | |||
5e8b5c254d | |||
b99b2d9a96 | |||
81041661c7 | |||
9d99b5be7f | |||
85a39109f8 | |||
23c2aa948a | |||
1fd4aa9ede | |||
e8a9fb7f55 | |||
cc4345b7c3 | |||
6035e834c2 | |||
b1ccc6df0f | |||
9563bd322c | |||
1e35c03dc8 | |||
7caa0aff7b | |||
accbc1fc3b | |||
05b21f100c | |||
0fb5ff7f10 | |||
e525b319d0 | |||
01c6774c54 | |||
b745cec079 | |||
90af99abf4 | |||
3c5bf70269 | |||
24cd1b2ea5 | |||
7de0b74c16 | |||
e5c20debfe | |||
2de3ad7457 | |||
9038dc4104 | |||
1491e80153 | |||
bdf45f92aa | |||
d104ca2b05 | |||
ec8cacb788 | |||
4e0dd12f5a | |||
bcf2572739 | |||
074c285e04 | |||
d7bc92afd9 | |||
11dfd87240 | |||
b283f027de | |||
7967b44508 | |||
04d21774cc | |||
3bd4fa6674 | |||
f68ed9997b | |||
a52a3bdd0e | |||
8d710e0e92 | |||
23c09dc4df | |||
04781abbb5 |
2
.github/ci-cd-scripts/playwright-electron.sh
vendored
@ -19,7 +19,7 @@ if [[ ! -f "test-results/.last-run.json" ]]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
retry=1
|
retry=1
|
||||||
max_retrys=2
|
max_retrys=4
|
||||||
|
|
||||||
# retry failed tests, doing our own retries because using inbuilt playwright retries causes connection issues
|
# retry failed tests, doing our own retries because using inbuilt playwright retries causes connection issues
|
||||||
while [[ $retry -le $max_retrys ]]; do
|
while [[ $retry -le $max_retrys ]]; do
|
||||||
|
83
.github/workflows/build-test-publish-apps.yml
vendored
@ -25,6 +25,7 @@ jobs:
|
|||||||
runs-on: ubuntu-22.04 # seperate job on Ubuntu for easy string manipulations (compared to Windows)
|
runs-on: ubuntu-22.04 # seperate job on Ubuntu for easy string manipulations (compared to Windows)
|
||||||
outputs:
|
outputs:
|
||||||
version: ${{ steps.export_version.outputs.version }}
|
version: ${{ steps.export_version.outputs.version }}
|
||||||
|
notes: ${{ steps.export_version.outputs.notes }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
@ -51,35 +52,35 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
VERSION=$(date +'%-y.%-m.%-d') yarn bump-jsons
|
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
|
- uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: prepared-files
|
name: prepared-files
|
||||||
path: |
|
path: |
|
||||||
package.json
|
package.json
|
||||||
src/wasm-lib/pkg/wasm_lib*
|
src/wasm-lib/pkg/wasm_lib*
|
||||||
|
release-notes.md
|
||||||
|
|
||||||
- id: export_version
|
- id: export_version
|
||||||
run: echo "version=`cat package.json | jq -r '.version'`" >> "$GITHUB_OUTPUT"
|
run: echo "version=`cat package.json | jq -r '.version'`" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Prepare electron-builder.yml file for nightly
|
- id: export_notes
|
||||||
if: ${{ github.event_name == 'schedule' }}
|
run: echo "notes=`cat release-notes.md'`" >> "$GITHUB_OUTPUT"
|
||||||
run: |
|
|
||||||
yq -i '.publish[0].url = "https://dl.zoo.dev/releases/modeling-app/nightly"' electron-builder.yml
|
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v3
|
|
||||||
if: ${{ github.event_name == 'schedule' }}
|
|
||||||
with:
|
|
||||||
name: prepared-files-nightly
|
|
||||||
path: |
|
|
||||||
electron-builder.yml
|
|
||||||
|
|
||||||
- name: Prepare electron-builder.yml file for updater test
|
- name: Prepare electron-builder.yml file for updater test
|
||||||
if: ${{ env.CUT_RELEASE_PR == 'true' }}
|
if: ${{ env.CUT_RELEASE_PR == 'true' }}
|
||||||
run: |
|
run: |
|
||||||
yq -i '.publish[0].url = "https://dl.zoo.dev/releases/modeling-app/updater-test"' electron-builder.yml
|
yq -i '.publish[0].url = "https://dl.zoo.dev/releases/modeling-app/updater-test-release-notes"' electron-builder.yml
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v3
|
||||||
if: ${{ env.CUT_RELEASE_PR == 'true' }}
|
|
||||||
with:
|
with:
|
||||||
name: prepared-files-updater-test
|
name: prepared-files-updater-test
|
||||||
path: |
|
path: |
|
||||||
@ -118,16 +119,7 @@ jobs:
|
|||||||
cp prepared-files/src/wasm-lib/pkg/wasm_lib_bg.wasm public
|
cp prepared-files/src/wasm-lib/pkg/wasm_lib_bg.wasm public
|
||||||
mkdir src/wasm-lib/pkg
|
mkdir src/wasm-lib/pkg
|
||||||
cp prepared-files/src/wasm-lib/pkg/wasm_lib* 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' }}
|
|
||||||
name: prepared-files-nightly
|
|
||||||
|
|
||||||
- name: Copy updated electron-builder.yml file for nightly build
|
|
||||||
if: ${{ github.event_name == 'schedule' }}
|
|
||||||
run: |
|
|
||||||
ls -R prepared-files-nightly
|
|
||||||
cp prepared-files-nightly/electron-builder.yml electron-builder.yml
|
|
||||||
|
|
||||||
- name: Sync node version and setup cache
|
- name: Sync node version and setup cache
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
@ -173,17 +165,11 @@ jobs:
|
|||||||
|
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: out-arm64-${{ matrix.os }}
|
name: out-${{ matrix.os }}
|
||||||
path: |
|
path: |
|
||||||
out/Zoo*arm64*.*
|
out/Zoo*.*
|
||||||
out/latest*.yml
|
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
|
# TODO: add the 'Build for Mac TestFlight (nightly)' stage back
|
||||||
|
|
||||||
- uses: actions/download-artifact@v3
|
- uses: actions/download-artifact@v3
|
||||||
@ -203,16 +189,10 @@ jobs:
|
|||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v3
|
||||||
if: ${{ env.CUT_RELEASE_PR == 'true' }}
|
if: ${{ env.CUT_RELEASE_PR == 'true' }}
|
||||||
with:
|
with:
|
||||||
name: updater-test-arm64-${{ matrix.os }}
|
name: updater-test-${{ matrix.os }}
|
||||||
path: |
|
path: |
|
||||||
out/Zoo*arm64*.*
|
out/Zoo*.*
|
||||||
|
out/latest*.yml
|
||||||
- uses: actions/upload-artifact@v3
|
|
||||||
if: ${{ env.CUT_RELEASE_PR == 'true' }}
|
|
||||||
with:
|
|
||||||
name: updater-test-x64-${{ matrix.os }}
|
|
||||||
path: |
|
|
||||||
out/Zoo*x64*.*
|
|
||||||
|
|
||||||
|
|
||||||
publish-apps-release:
|
publish-apps-release:
|
||||||
@ -225,7 +205,7 @@ jobs:
|
|||||||
VERSION_NO_V: ${{ needs.prepare-files.outputs.version }}
|
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) }}
|
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 }}
|
PUB_DATE: ${{ github.event_name == 'release' && github.event.release.created_at || github.event.repository.updated_at }}
|
||||||
NOTES: ${{ github.event_name == 'release' && github.event.release.body || format('Non-release build, commit {0}', github.sha) }}
|
NOTES: ${{ needs.prepare-files.outputs.notes }}
|
||||||
BUCKET_DIR: ${{ github.event_name == 'schedule' && 'dl.kittycad.io/releases/modeling-app/nightly' || 'dl.kittycad.io/releases/modeling-app' }}
|
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' }}
|
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' }}
|
URL_CODED_NAME: ${{ github.event_name == 'schedule' && 'Zoo%20Modeling%20App%20%28Nightly%29' || 'Zoo%20Modeling%20App' }}
|
||||||
@ -234,32 +214,17 @@ jobs:
|
|||||||
|
|
||||||
- uses: actions/download-artifact@v3
|
- uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: out-arm64-windows-2022
|
name: out-windows-2022
|
||||||
path: out
|
path: out
|
||||||
|
|
||||||
- uses: actions/download-artifact@v3
|
- uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: out-x64-windows-2022
|
name: out-macos-14
|
||||||
path: out
|
path: out
|
||||||
|
|
||||||
- uses: actions/download-artifact@v3
|
- uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: out-arm64-macos-14
|
name: out-ubuntu-22.04
|
||||||
path: out
|
|
||||||
|
|
||||||
- uses: actions/download-artifact@v3
|
|
||||||
with:
|
|
||||||
name: out-x64-macos-14
|
|
||||||
path: out
|
|
||||||
|
|
||||||
- uses: actions/download-artifact@v3
|
|
||||||
with:
|
|
||||||
name: out-arm64-ubuntu-22.04
|
|
||||||
path: out
|
|
||||||
|
|
||||||
- uses: actions/download-artifact@v3
|
|
||||||
with:
|
|
||||||
name: out-x64-ubuntu-22.04
|
|
||||||
path: out
|
path: out
|
||||||
|
|
||||||
- name: Generate the download static endpoint
|
- name: Generate the download static endpoint
|
||||||
|
8
.github/workflows/e2e-tests.yml
vendored
@ -142,6 +142,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: playwright-report-${{ matrix.os }}-snapshot-${{ matrix.shardIndex }}-${{ github.sha }}
|
name: playwright-report-${{ matrix.os }}-snapshot-${{ matrix.shardIndex }}-${{ github.sha }}
|
||||||
path: playwright-report/
|
path: playwright-report/
|
||||||
|
include-hidden-files: true
|
||||||
retention-days: 30
|
retention-days: 30
|
||||||
overwrite: true
|
overwrite: true
|
||||||
- name: Clean up test-results
|
- name: Clean up test-results
|
||||||
@ -177,6 +178,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: playwright-report-${{ matrix.os }}-${{ matrix.shardIndex }}-${{ github.sha }}
|
name: playwright-report-${{ matrix.os }}-${{ matrix.shardIndex }}-${{ github.sha }}
|
||||||
path: playwright-report/
|
path: playwright-report/
|
||||||
|
include-hidden-files: true
|
||||||
retention-days: 30
|
retention-days: 30
|
||||||
- uses: actions/download-artifact@v4
|
- uses: actions/download-artifact@v4
|
||||||
if: ${{ !cancelled() && (success() || failure()) }}
|
if: ${{ !cancelled() && (success() || failure()) }}
|
||||||
@ -207,6 +209,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: test-results-${{ matrix.os }}-${{ matrix.shardIndex }}-${{ github.sha }}
|
name: test-results-${{ matrix.os }}-${{ matrix.shardIndex }}-${{ github.sha }}
|
||||||
path: test-results/
|
path: test-results/
|
||||||
|
include-hidden-files: true
|
||||||
retention-days: 30
|
retention-days: 30
|
||||||
overwrite: true
|
overwrite: true
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
@ -214,6 +217,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: playwright-report-${{ matrix.os }}-${{ matrix.shardIndex }}-${{ github.sha }}
|
name: playwright-report-${{ matrix.os }}-${{ matrix.shardIndex }}-${{ github.sha }}
|
||||||
path: playwright-report/
|
path: playwright-report/
|
||||||
|
include-hidden-files: true
|
||||||
retention-days: 30
|
retention-days: 30
|
||||||
overwrite: true
|
overwrite: true
|
||||||
|
|
||||||
@ -313,7 +317,7 @@ jobs:
|
|||||||
if: ${{ !cancelled() && (success() || failure()) }}
|
if: ${{ !cancelled() && (success() || failure()) }}
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
with:
|
with:
|
||||||
name: test-results-${{ matrix.os }}-${{ github.sha }}
|
name: test-results-electron-${{ matrix.os }}-${{ github.sha }}
|
||||||
path: test-results/
|
path: test-results/
|
||||||
- name: Run electron tests (with retries)
|
- name: Run electron tests (with retries)
|
||||||
id: retry
|
id: retry
|
||||||
@ -339,6 +343,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: test-results-electron-${{ matrix.os }}-${{ github.sha }}
|
name: test-results-electron-${{ matrix.os }}-${{ github.sha }}
|
||||||
path: test-results/
|
path: test-results/
|
||||||
|
include-hidden-files: true
|
||||||
retention-days: 30
|
retention-days: 30
|
||||||
overwrite: true
|
overwrite: true
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
@ -346,5 +351,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: playwright-report-electron-${{ matrix.os }}-${{ github.sha }}
|
name: playwright-report-electron-${{ matrix.os }}-${{ github.sha }}
|
||||||
path: playwright-report/
|
path: playwright-report/
|
||||||
|
include-hidden-files: true
|
||||||
retention-days: 30
|
retention-days: 30
|
||||||
overwrite: true
|
overwrite: true
|
||||||
|
13
.github/workflows/static-analysis.yml
vendored
@ -37,10 +37,6 @@ jobs:
|
|||||||
node-version-file: '.nvmrc'
|
node-version-file: '.nvmrc'
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
- run: yarn install
|
- run: yarn install
|
||||||
- uses: Swatinem/rust-cache@v2
|
|
||||||
with:
|
|
||||||
workspaces: './src/wasm-lib'
|
|
||||||
|
|
||||||
- run: yarn build:wasm
|
- run: yarn build:wasm
|
||||||
|
|
||||||
yarn-tsc:
|
yarn-tsc:
|
||||||
@ -70,10 +66,6 @@ jobs:
|
|||||||
node-version-file: '.nvmrc'
|
node-version-file: '.nvmrc'
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
- run: yarn install
|
- run: yarn install
|
||||||
- uses: Swatinem/rust-cache@v2
|
|
||||||
with:
|
|
||||||
workspaces: './src/wasm-lib'
|
|
||||||
|
|
||||||
- run: yarn lint
|
- run: yarn lint
|
||||||
|
|
||||||
python-codespell:
|
python-codespell:
|
||||||
@ -101,11 +93,6 @@ jobs:
|
|||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
|
|
||||||
- run: yarn install
|
- run: yarn install
|
||||||
|
|
||||||
- uses: Swatinem/rust-cache@v2
|
|
||||||
with:
|
|
||||||
workspaces: './src/wasm-lib'
|
|
||||||
|
|
||||||
- run: yarn build:wasm
|
- run: yarn build:wasm
|
||||||
|
|
||||||
- run: yarn simpleserver:bg
|
- run: yarn simpleserver:bg
|
||||||
|
56
README.md
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Zoo Modeling App
|
## Zoo Modeling App
|
||||||
|
|
||||||
live at [app.zoo.dev](https://app.zoo.dev/)
|
download at [zoo.dev/modeling-app/download](https://zoo.dev/modeling-app/download)
|
||||||
|
|
||||||
A CAD application from the future, brought to you by the [Zoo team](https://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:
|
followed by:
|
||||||
|
|
||||||
```
|
```
|
||||||
yarn build:wasm-dev
|
yarn build:wasm
|
||||||
```
|
```
|
||||||
|
|
||||||
or if you have the gh cli installed
|
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
|
./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
|
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 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.
|
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.
|
||||||
|
|
||||||
### Development environment variables
|
### Development environment variables
|
||||||
|
|
||||||
@ -91,13 +91,13 @@ Third-Party Cookies".
|
|||||||
|
|
||||||
## Desktop
|
## Desktop
|
||||||
|
|
||||||
To spin up the desktop app, `yarn install` and `yarn build:wasm-dev` need to have been done before hand then
|
To spin up the desktop app, `yarn install` and `yarn build:wasm` need to have been done before hand then
|
||||||
|
|
||||||
```
|
```
|
||||||
yarn electron:start
|
yarn tron:start
|
||||||
```
|
```
|
||||||
|
|
||||||
This will start the application and hot-reload on changed.
|
This will start the application and hot-reload on changes.
|
||||||
|
|
||||||
Devtools can be opened with the usual Cmd/Ctrl-Shift-I.
|
Devtools can be opened with the usual Cmd/Ctrl-Shift-I.
|
||||||
|
|
||||||
@ -128,7 +128,18 @@ Before you submit a contribution PR to this repo, please ensure that:
|
|||||||
|
|
||||||
## Release a new version
|
## Release a new version
|
||||||
|
|
||||||
#### 1. Bump the versions by running `./make-release.sh` and create a Cut Release PR
|
#### 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>
|
||||||
|
```
|
||||||
|
|
||||||
That will create the branch with the updated json files for you:
|
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;
|
- run `./make-release.sh` or `./make-release.sh patch` for a patch update;
|
||||||
@ -137,28 +148,32 @@ 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.
|
After it runs you should just need the push the branch and open a PR.
|
||||||
|
|
||||||
**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.
|
#### 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.
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
#### 2. Smoke test artifacts from the Cut Release PR
|
#### 3. Manually test artifacts from the Cut Release PR
|
||||||
|
|
||||||
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 find under the `artifact` zip, at the very bottom of the `ci` action 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).
|
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).
|
||||||
|
|
||||||
#### 3. Merge the Cut Release PR
|
#### 4. 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.
|
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.
|
||||||
|
|
||||||
|
|
||||||
#### 4. Publish the release
|
#### 5. 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_.
|
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_.
|
||||||
|
|
||||||
#### 5. Profit
|
#### 6. Profit
|
||||||
|
|
||||||
A new Action kicks in at https://github.com/KittyCAD/modeling-app/actions, which can be found under `release` event filter.
|
A new Action kicks in at https://github.com/KittyCAD/modeling-app/actions, which can be found under `release` event filter.
|
||||||
|
|
||||||
@ -319,7 +334,16 @@ Which will run our suite of [Vitest unit](https://vitest.dev/) and [React Testin
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd src/wasm-lib
|
cd src/wasm-lib
|
||||||
cargo test
|
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
|
||||||
```
|
```
|
||||||
|
|
||||||
### Mapping CI CD jobs to local commands
|
### Mapping CI CD jobs to local commands
|
||||||
|
@ -36,7 +36,7 @@ exampleSketch = startSketchOn('XZ')
|
|||||||
|> close(%)
|
|> close(%)
|
||||||
|> patternCircular2d({
|
|> patternCircular2d({
|
||||||
center: [0, 0],
|
center: [0, 0],
|
||||||
repetitions: 12,
|
instances: 13,
|
||||||
arcDegrees: 360,
|
arcDegrees: 360,
|
||||||
rotateDuplicates: true
|
rotateDuplicates: true
|
||||||
}, %)
|
}, %)
|
||||||
|
@ -35,7 +35,7 @@ example = extrude(-5, exampleSketch)
|
|||||||
|> patternCircular3d({
|
|> patternCircular3d({
|
||||||
axis: [1, -1, 0],
|
axis: [1, -1, 0],
|
||||||
center: [10, -20, 0],
|
center: [10, -20, 0],
|
||||||
repetitions: 10,
|
instances: 11,
|
||||||
arcDegrees: 360,
|
arcDegrees: 360,
|
||||||
rotateDuplicates: true
|
rotateDuplicates: true
|
||||||
}, %)
|
}, %)
|
||||||
|
@ -32,7 +32,7 @@ exampleSketch = startSketchOn('XZ')
|
|||||||
|> circle({ center: [0, 0], radius: 1 }, %)
|
|> circle({ center: [0, 0], radius: 1 }, %)
|
||||||
|> patternLinear2d({
|
|> patternLinear2d({
|
||||||
axis: [1, 0],
|
axis: [1, 0],
|
||||||
repetitions: 6,
|
instances: 7,
|
||||||
distance: 4
|
distance: 4
|
||||||
}, %)
|
}, %)
|
||||||
|
|
||||||
|
@ -38,7 +38,7 @@ exampleSketch = startSketchOn('XZ')
|
|||||||
example = extrude(1, exampleSketch)
|
example = extrude(1, exampleSketch)
|
||||||
|> patternLinear3d({
|
|> patternLinear3d({
|
||||||
axis: [1, 0, 1],
|
axis: [1, 0, 1],
|
||||||
repetitions: 6,
|
instances: 7,
|
||||||
distance: 6
|
distance: 6
|
||||||
}, %)
|
}, %)
|
||||||
```
|
```
|
||||||
|
15029
docs/kcl/std.json
@ -82,6 +82,78 @@ 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:** `<=`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
----
|
----
|
||||||
|
|
||||||
|
|
||||||
|
@ -18,6 +18,27 @@ 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
|
## Properties
|
||||||
|
|
||||||
| Property | Type | Description | Required |
|
| Property | Type | Description | Required |
|
||||||
@ -45,6 +66,7 @@ layout: manual
|
|||||||
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
|
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
|
||||||
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
|
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
|
||||||
| `declarations` |`[` [`VariableDeclarator`](/docs/kcl/types/VariableDeclarator) `]`| | No |
|
| `declarations` |`[` [`VariableDeclarator`](/docs/kcl/types/VariableDeclarator) `]`| | No |
|
||||||
|
| `visibility` |[`ItemVisibility`](/docs/kcl/types/ItemVisibility)| | No |
|
||||||
| `kind` |[`VariableKind`](/docs/kcl/types/VariableKind)| | 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 |
|
| `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 |
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ Data for a circular pattern on a 2D sketch.
|
|||||||
|
|
||||||
| Property | Type | Description | Required |
|
| Property | Type | Description | Required |
|
||||||
|----------|------|-------------|----------|
|
|----------|------|-------------|----------|
|
||||||
| `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 |
|
| `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 |
|
||||||
| `center` |`[number, number]`| The center about which to make the pattern. This is a 2D vector. | 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 |
|
| `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 |
|
| `rotateDuplicates` |`boolean`| Whether or not to rotate the duplicates as they are copied. | No |
|
||||||
|
@ -16,7 +16,7 @@ Data for a circular pattern on a 3D model.
|
|||||||
|
|
||||||
| Property | Type | Description | Required |
|
| Property | Type | Description | Required |
|
||||||
|----------|------|-------------|----------|
|
|----------|------|-------------|----------|
|
||||||
| `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 |
|
| `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 |
|
||||||
| `axis` |`[number, number, number]`| The axis around which to make the pattern. This is a 3D vector. | 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 |
|
| `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 |
|
| `arcDegrees` |`number`| The arc angle (in degrees) to place the repetitions. Must be greater than 0. | No |
|
||||||
|
@ -197,6 +197,27 @@ 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
|
## Properties
|
||||||
|
|
||||||
| Property | Type | Description | Required |
|
| Property | Type | Description | Required |
|
||||||
|
24
docs/kcl/types/ImportItem.md
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
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 |
|
||||||
|
|
||||||
|
|
16
docs/kcl/types/ItemVisibility.md
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "ItemVisibility"
|
||||||
|
excerpt: ""
|
||||||
|
layout: manual
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
**enum:** `default`, `export`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
|||||||
---
|
---
|
||||||
title: "KclValue"
|
title: "KclValue"
|
||||||
excerpt: "A memory item."
|
excerpt: "Any KCL value."
|
||||||
layout: manual
|
layout: manual
|
||||||
---
|
---
|
||||||
|
|
||||||
A memory item.
|
Any KCL value.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -80,7 +80,7 @@ A plane.
|
|||||||
|----------|------|-------------|----------|
|
|----------|------|-------------|----------|
|
||||||
| `type` |enum: `Plane`| | No |
|
| `type` |enum: `Plane`| | No |
|
||||||
| `id` |`string`| The id of the plane. | No |
|
| `id` |`string`| The id of the plane. | No |
|
||||||
| `value` |[`PlaneType`](/docs/kcl/types/PlaneType)| A memory item. | No |
|
| `value` |[`PlaneType`](/docs/kcl/types/PlaneType)| Any KCL value. | No |
|
||||||
| `origin` |[`Point3d`](/docs/kcl/types/Point3d)| Origin of the plane. | No |
|
| `origin` |[`Point3d`](/docs/kcl/types/Point3d)| Origin of the plane. | No |
|
||||||
| `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane’s X axis be? | No |
|
| `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane’s X axis be? | No |
|
||||||
| `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane’s Y axis be? | No |
|
| `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane’s Y axis be? | No |
|
||||||
@ -183,8 +183,8 @@ Data for an imported geometry.
|
|||||||
| Property | Type | Description | Required |
|
| Property | Type | Description | Required |
|
||||||
|----------|------|-------------|----------|
|
|----------|------|-------------|----------|
|
||||||
| `type` |enum: `Function`| | No |
|
| `type` |enum: `Function`| | No |
|
||||||
| `expression` |[`FunctionExpression`](/docs/kcl/types/FunctionExpression)| A memory item. | No |
|
| `expression` |[`FunctionExpression`](/docs/kcl/types/FunctionExpression)| Any KCL value. | No |
|
||||||
| `memory` |[`ProgramMemory`](/docs/kcl/types/ProgramMemory)| A memory item. | No |
|
| `memory` |[`ProgramMemory`](/docs/kcl/types/ProgramMemory)| Any KCL value. | No |
|
||||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
||||||
|
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ Data for a linear pattern on a 2D sketch.
|
|||||||
|
|
||||||
| Property | Type | Description | Required |
|
| Property | Type | Description | Required |
|
||||||
|----------|------|-------------|----------|
|
|----------|------|-------------|----------|
|
||||||
| `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 |
|
| `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 |
|
||||||
| `distance` |`number`| The distance between each repetition. This can also be referred to as spacing. | 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 |
|
| `axis` |`[number, number]`| The axis of the pattern. This is a 2D vector. | No |
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ Data for a linear pattern on a 3D model.
|
|||||||
|
|
||||||
| Property | Type | Description | Required |
|
| Property | Type | Description | Required |
|
||||||
|----------|------|-------------|----------|
|
|----------|------|-------------|----------|
|
||||||
| `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 |
|
| `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 |
|
||||||
| `distance` |`number`| The distance between each repetition. This can also be referred to as spacing. | 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 |
|
| `axis` |`[number, number, number]`| The axis of the pattern. | No |
|
||||||
|
|
||||||
|
80
e2e/playwright/debug-pane.spec.ts
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
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)
|
||||||
|
})
|
||||||
|
})
|
@ -13,6 +13,13 @@ type mouseParams = {
|
|||||||
pixelDiff: number
|
pixelDiff: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SceneSerialised = {
|
||||||
|
camera: {
|
||||||
|
position: [number, number, number]
|
||||||
|
target: [number, number, number]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class SceneFixture {
|
export class SceneFixture {
|
||||||
public page: Page
|
public page: Page
|
||||||
|
|
||||||
@ -22,6 +29,22 @@ export class SceneFixture {
|
|||||||
this.page = page
|
this.page = page
|
||||||
this.reConstruct(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) => {
|
reConstruct = (page: Page) => {
|
||||||
this.page = page
|
this.page = page
|
||||||
|
|
||||||
@ -31,7 +54,7 @@ export class SceneFixture {
|
|||||||
makeMouseHelpers = (
|
makeMouseHelpers = (
|
||||||
x: number,
|
x: number,
|
||||||
y: number,
|
y: number,
|
||||||
{ steps }: { steps: number } = { steps: 5000 }
|
{ steps }: { steps: number } = { steps: 20 }
|
||||||
) =>
|
) =>
|
||||||
[
|
[
|
||||||
(clickParams?: mouseParams) => {
|
(clickParams?: mouseParams) => {
|
||||||
@ -87,6 +110,36 @@ export class SceneFixture {
|
|||||||
)
|
)
|
||||||
await closeDebugPanel(this.page)
|
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 () => {
|
waitForExecutionDone = async () => {
|
||||||
await expect(this.exeIndicator).toBeVisible()
|
await expect(this.exeIndicator).toBeVisible()
|
||||||
}
|
}
|
||||||
@ -114,4 +167,17 @@ 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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { test, expect, Page } from '@playwright/test'
|
import { test, expect } from '@playwright/test'
|
||||||
import {
|
import {
|
||||||
doExport,
|
doExport,
|
||||||
executorInputPath,
|
executorInputPath,
|
||||||
@ -618,31 +618,30 @@ test(
|
|||||||
'Deleting projects, can delete individual project, can still create projects after deleting all',
|
'Deleting projects, can delete individual project, can still create projects after deleting all',
|
||||||
{ tag: '@electron' },
|
{ tag: '@electron' },
|
||||||
async ({ browserName }, testInfo) => {
|
async ({ browserName }, testInfo) => {
|
||||||
|
const projectData = [
|
||||||
|
['router-template-slate', 'cylinder.kcl'],
|
||||||
|
['bracket', 'focusrite_scarlett_mounting_braket.kcl'],
|
||||||
|
['lego', 'lego.kcl'],
|
||||||
|
]
|
||||||
|
|
||||||
const { electronApp, page } = await setupElectron({
|
const { electronApp, page } = await setupElectron({
|
||||||
testInfo,
|
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 })
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
|
|
||||||
page.on('console', console.log)
|
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 () => {
|
await test.step('delete the middle project, i.e. the bracket project', async () => {
|
||||||
const project = page.getByText('bracket')
|
const project = page.getByText('bracket')
|
||||||
|
|
||||||
@ -744,8 +743,26 @@ test(
|
|||||||
'Can sort projects on home page',
|
'Can sort projects on home page',
|
||||||
{ tag: '@electron' },
|
{ tag: '@electron' },
|
||||||
async ({ browserName }, testInfo) => {
|
async ({ browserName }, testInfo) => {
|
||||||
|
const projectData = [
|
||||||
|
['router-template-slate', 'cylinder.kcl'],
|
||||||
|
['bracket', 'focusrite_scarlett_mounting_braket.kcl'],
|
||||||
|
['lego', 'lego.kcl'],
|
||||||
|
]
|
||||||
|
|
||||||
const { electronApp, page } = await setupElectron({
|
const { electronApp, page } = await setupElectron({
|
||||||
testInfo,
|
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 })
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
|
|
||||||
@ -753,24 +770,6 @@ test(
|
|||||||
|
|
||||||
page.on('console', console.log)
|
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 () => {
|
await test.step('should be shorted by modified initially', async () => {
|
||||||
const lastModifiedButton = page.getByRole('button', {
|
const lastModifiedButton = page.getByRole('button', {
|
||||||
name: 'Last Modified',
|
name: 'Last Modified',
|
||||||
|
@ -1115,6 +1115,102 @@ sketch002 = startSketchOn(extrude001, 'END')
|
|||||||
).toHaveAttribute('aria-pressed', 'true')
|
).toHaveAttribute('aria-pressed', 'true')
|
||||||
}).toPass({ timeout: 40_000, intervals: [1_000] })
|
}).toPass({ timeout: 40_000, intervals: [1_000] })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('Can sketch on face when user defined function was used in the sketch', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const u = await getUtils(page)
|
||||||
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
|
|
||||||
|
// Checking for a regression that performs a sketch when a user defined function
|
||||||
|
// is declared at the top of the file and used in the sketch that is being drawn on.
|
||||||
|
// fn in2mm is declared at the top of the file and used rail which does a an extrusion with the function.
|
||||||
|
|
||||||
|
await page.addInitScript(async () => {
|
||||||
|
localStorage.setItem(
|
||||||
|
'persistCode',
|
||||||
|
`fn in2mm = (inches) => {
|
||||||
|
return inches * 25.4
|
||||||
|
}
|
||||||
|
|
||||||
|
const railTop = in2mm(.748)
|
||||||
|
const railSide = in2mm(.024)
|
||||||
|
const railBaseWidth = in2mm(.612)
|
||||||
|
const railWideWidth = in2mm(.835)
|
||||||
|
const railBaseLength = in2mm(.200)
|
||||||
|
const railClampable = in2mm(.200)
|
||||||
|
|
||||||
|
const rail = startSketchOn('XZ')
|
||||||
|
|> startProfileAt([
|
||||||
|
-railTop / 2,
|
||||||
|
railClampable + railBaseLength
|
||||||
|
], %)
|
||||||
|
|> lineTo([
|
||||||
|
railTop / 2,
|
||||||
|
railClampable + railBaseLength
|
||||||
|
], %)
|
||||||
|
|> lineTo([
|
||||||
|
railWideWidth / 2,
|
||||||
|
railClampable / 2 + railBaseLength
|
||||||
|
], %, $seg01)
|
||||||
|
|> lineTo([railTop / 2, railBaseLength], %)
|
||||||
|
|> lineTo([railBaseWidth / 2, railBaseLength], %)
|
||||||
|
|> lineTo([railBaseWidth / 2, 0], %)
|
||||||
|
|> lineTo([-railBaseWidth / 2, 0], %)
|
||||||
|
|> lineTo([-railBaseWidth / 2, railBaseLength], %)
|
||||||
|
|> lineTo([-railTop / 2, railBaseLength], %)
|
||||||
|
|> lineTo([
|
||||||
|
-railWideWidth / 2,
|
||||||
|
railClampable / 2 + railBaseLength
|
||||||
|
], %)
|
||||||
|
|> lineTo([
|
||||||
|
-railTop / 2,
|
||||||
|
railClampable + railBaseLength
|
||||||
|
], %)
|
||||||
|
|> close(%)
|
||||||
|
|> extrude(in2mm(2), %)`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const center = { x: 600, y: 250 }
|
||||||
|
const rectangleSize = 20
|
||||||
|
await u.waitForAuthSkipAppStart()
|
||||||
|
|
||||||
|
// Start a sketch
|
||||||
|
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
||||||
|
|
||||||
|
// Click the top face of this rail
|
||||||
|
await page.mouse.click(center.x, center.y)
|
||||||
|
await page.waitForTimeout(1000)
|
||||||
|
|
||||||
|
// Draw a rectangle
|
||||||
|
// top left
|
||||||
|
await page.mouse.click(center.x - rectangleSize, center.y - rectangleSize)
|
||||||
|
await page.waitForTimeout(250)
|
||||||
|
// top right
|
||||||
|
await page.mouse.click(center.x + rectangleSize, center.y - rectangleSize)
|
||||||
|
await page.waitForTimeout(250)
|
||||||
|
|
||||||
|
// bottom right
|
||||||
|
await page.mouse.click(center.x + rectangleSize, center.y + rectangleSize)
|
||||||
|
await page.waitForTimeout(250)
|
||||||
|
|
||||||
|
// bottom left
|
||||||
|
await page.mouse.click(center.x - rectangleSize, center.y + rectangleSize)
|
||||||
|
await page.waitForTimeout(250)
|
||||||
|
|
||||||
|
// top left
|
||||||
|
await page.mouse.click(center.x - rectangleSize, center.y - rectangleSize)
|
||||||
|
await page.waitForTimeout(250)
|
||||||
|
|
||||||
|
// exit sketch
|
||||||
|
await page.getByRole('button', { name: 'Exit Sketch' }).click()
|
||||||
|
|
||||||
|
// Check execution is done
|
||||||
|
await u.openDebugPanel()
|
||||||
|
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||||
|
await u.closeDebugPanel()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test2.describe('Sketch mode should be toleratant to syntax errors', () => {
|
test2.describe('Sketch mode should be toleratant to syntax errors', () => {
|
||||||
|
@ -521,7 +521,6 @@ test(
|
|||||||
const startXPx = 600
|
const startXPx = 600
|
||||||
|
|
||||||
// Equip the rectangle tool
|
// Equip the rectangle tool
|
||||||
await page.getByRole('button', { name: 'line Line', exact: true }).click()
|
|
||||||
await page
|
await page
|
||||||
.getByRole('button', { name: 'rectangle Corner rectangle', exact: true })
|
.getByRole('button', { name: 'rectangle Corner rectangle', exact: true })
|
||||||
.click()
|
.click()
|
||||||
@ -670,6 +669,7 @@ test.describe(
|
|||||||
// screen shot should show the sketch
|
// screen shot should show the sketch
|
||||||
await expect(page).toHaveScreenshot({
|
await expect(page).toHaveScreenshot({
|
||||||
maxDiffPixels: 100,
|
maxDiffPixels: 100,
|
||||||
|
mask: [page.getByTestId('model-state-indicator')],
|
||||||
})
|
})
|
||||||
|
|
||||||
// exit sketch
|
// exit sketch
|
||||||
@ -687,6 +687,7 @@ test.describe(
|
|||||||
// second screen shot should look almost identical, i.e. scale should be the same.
|
// second screen shot should look almost identical, i.e. scale should be the same.
|
||||||
await expect(page).toHaveScreenshot({
|
await expect(page).toHaveScreenshot({
|
||||||
maxDiffPixels: 100,
|
maxDiffPixels: 100,
|
||||||
|
mask: [page.getByTestId('model-state-indicator')],
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 55 KiB |
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 74 KiB |
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 65 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 64 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB |
@ -1,18 +1,18 @@
|
|||||||
import { test, expect } from '@playwright/test'
|
import { _test, _expect } from './playwright-deprecated'
|
||||||
|
import { test } from './fixtures/fixtureSetup'
|
||||||
import { getUtils, setup, tearDown } from './test-utils'
|
import { getUtils, setup, tearDown } from './test-utils'
|
||||||
import { uuidv4 } from 'lib/utils'
|
import { uuidv4 } from 'lib/utils'
|
||||||
import { TEST_CODE_GIZMO } from './storageStates'
|
import { TEST_CODE_GIZMO } from './storageStates'
|
||||||
|
|
||||||
test.beforeEach(async ({ context, page }, testInfo) => {
|
_test.beforeEach(async ({ context, page }, testInfo) => {
|
||||||
await setup(context, page, testInfo)
|
await setup(context, page, testInfo)
|
||||||
})
|
})
|
||||||
|
|
||||||
test.afterEach(async ({ page }, testInfo) => {
|
_test.afterEach(async ({ page }, testInfo) => {
|
||||||
await tearDown(page, testInfo)
|
await tearDown(page, testInfo)
|
||||||
})
|
})
|
||||||
|
|
||||||
test.describe('Testing Gizmo', () => {
|
_test.describe('Testing Gizmo', () => {
|
||||||
const cases = [
|
const cases = [
|
||||||
{
|
{
|
||||||
testDescription: 'top view',
|
testDescription: 'top view',
|
||||||
@ -57,7 +57,7 @@ test.describe('Testing Gizmo', () => {
|
|||||||
expectedCameraTarget,
|
expectedCameraTarget,
|
||||||
testDescription,
|
testDescription,
|
||||||
} of cases) {
|
} of cases) {
|
||||||
test(`check ${testDescription}`, async ({ page, browserName }) => {
|
_test(`check ${testDescription}`, async ({ page, browserName }) => {
|
||||||
const u = await getUtils(page)
|
const u = await getUtils(page)
|
||||||
await page.addInitScript((TEST_CODE_GIZMO) => {
|
await page.addInitScript((TEST_CODE_GIZMO) => {
|
||||||
localStorage.setItem('persistCode', TEST_CODE_GIZMO)
|
localStorage.setItem('persistCode', TEST_CODE_GIZMO)
|
||||||
@ -117,30 +117,30 @@ test.describe('Testing Gizmo', () => {
|
|||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
// position
|
// position
|
||||||
expect(page.getByTestId('cam-x-position')).toHaveValue(
|
_expect(page.getByTestId('cam-x-position')).toHaveValue(
|
||||||
expectedCameraPosition.x.toString()
|
expectedCameraPosition.x.toString()
|
||||||
),
|
),
|
||||||
expect(page.getByTestId('cam-y-position')).toHaveValue(
|
_expect(page.getByTestId('cam-y-position')).toHaveValue(
|
||||||
expectedCameraPosition.y.toString()
|
expectedCameraPosition.y.toString()
|
||||||
),
|
),
|
||||||
expect(page.getByTestId('cam-z-position')).toHaveValue(
|
_expect(page.getByTestId('cam-z-position')).toHaveValue(
|
||||||
expectedCameraPosition.z.toString()
|
expectedCameraPosition.z.toString()
|
||||||
),
|
),
|
||||||
// target
|
// target
|
||||||
expect(page.getByTestId('cam-x-target')).toHaveValue(
|
_expect(page.getByTestId('cam-x-target')).toHaveValue(
|
||||||
expectedCameraTarget.x.toString()
|
expectedCameraTarget.x.toString()
|
||||||
),
|
),
|
||||||
expect(page.getByTestId('cam-y-target')).toHaveValue(
|
_expect(page.getByTestId('cam-y-target')).toHaveValue(
|
||||||
expectedCameraTarget.y.toString()
|
expectedCameraTarget.y.toString()
|
||||||
),
|
),
|
||||||
expect(page.getByTestId('cam-z-target')).toHaveValue(
|
_expect(page.getByTestId('cam-z-target')).toHaveValue(
|
||||||
expectedCameraTarget.z.toString()
|
expectedCameraTarget.z.toString()
|
||||||
),
|
),
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
test('Context menu and popover menu', async ({ page }) => {
|
_test('Context menu and popover menu', async ({ page }) => {
|
||||||
const testCase = {
|
const testCase = {
|
||||||
testDescription: 'Right view',
|
testDescription: 'Right view',
|
||||||
expectedCameraPosition: { x: 5660.02, y: -152, z: 26 },
|
expectedCameraPosition: { x: 5660.02, y: -152, z: 26 },
|
||||||
@ -196,7 +196,7 @@ test.describe('Testing Gizmo', () => {
|
|||||||
const buttonToTest = page.getByRole('button', {
|
const buttonToTest = page.getByRole('button', {
|
||||||
name: testCase.testDescription,
|
name: testCase.testDescription,
|
||||||
})
|
})
|
||||||
await expect(buttonToTest).toBeVisible()
|
await _expect(buttonToTest).toBeVisible()
|
||||||
await buttonToTest.click()
|
await buttonToTest.click()
|
||||||
|
|
||||||
// Now assert we've moved to the correct view
|
// Now assert we've moved to the correct view
|
||||||
@ -215,23 +215,23 @@ test.describe('Testing Gizmo', () => {
|
|||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
// position
|
// position
|
||||||
expect(page.getByTestId('cam-x-position')).toHaveValue(
|
_expect(page.getByTestId('cam-x-position')).toHaveValue(
|
||||||
testCase.expectedCameraPosition.x.toString()
|
testCase.expectedCameraPosition.x.toString()
|
||||||
),
|
),
|
||||||
expect(page.getByTestId('cam-y-position')).toHaveValue(
|
_expect(page.getByTestId('cam-y-position')).toHaveValue(
|
||||||
testCase.expectedCameraPosition.y.toString()
|
testCase.expectedCameraPosition.y.toString()
|
||||||
),
|
),
|
||||||
expect(page.getByTestId('cam-z-position')).toHaveValue(
|
_expect(page.getByTestId('cam-z-position')).toHaveValue(
|
||||||
testCase.expectedCameraPosition.z.toString()
|
testCase.expectedCameraPosition.z.toString()
|
||||||
),
|
),
|
||||||
// target
|
// target
|
||||||
expect(page.getByTestId('cam-x-target')).toHaveValue(
|
_expect(page.getByTestId('cam-x-target')).toHaveValue(
|
||||||
testCase.expectedCameraTarget.x.toString()
|
testCase.expectedCameraTarget.x.toString()
|
||||||
),
|
),
|
||||||
expect(page.getByTestId('cam-y-target')).toHaveValue(
|
_expect(page.getByTestId('cam-y-target')).toHaveValue(
|
||||||
testCase.expectedCameraTarget.y.toString()
|
testCase.expectedCameraTarget.y.toString()
|
||||||
),
|
),
|
||||||
expect(page.getByTestId('cam-z-target')).toHaveValue(
|
_expect(page.getByTestId('cam-z-target')).toHaveValue(
|
||||||
testCase.expectedCameraTarget.z.toString()
|
testCase.expectedCameraTarget.z.toString()
|
||||||
),
|
),
|
||||||
])
|
])
|
||||||
@ -242,8 +242,60 @@ test.describe('Testing Gizmo', () => {
|
|||||||
const gizmoPopoverButton = page.getByRole('button', {
|
const gizmoPopoverButton = page.getByRole('button', {
|
||||||
name: 'view settings',
|
name: 'view settings',
|
||||||
})
|
})
|
||||||
await expect(gizmoPopoverButton).toBeVisible()
|
await _expect(gizmoPopoverButton).toBeVisible()
|
||||||
await gizmoPopoverButton.click()
|
await gizmoPopoverButton.click()
|
||||||
await expect(buttonToTest).toBeVisible()
|
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],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -1208,6 +1208,12 @@ extrude001 = extrude(50, sketch001)
|
|||||||
test('Deselecting line tool should mean nothing happens on click', async ({
|
test('Deselecting line tool should mean nothing happens on click', async ({
|
||||||
page,
|
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)
|
const u = await getUtils(page)
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
|
|
||||||
@ -1228,6 +1234,7 @@ extrude001 = extrude(50, sketch001)
|
|||||||
200
|
200
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Clicks the XZ Plane in the page
|
||||||
await page.mouse.click(700, 200)
|
await page.mouse.click(700, 200)
|
||||||
|
|
||||||
await expect(page.locator('.cm-content')).toHaveText(
|
await expect(page.locator('.cm-content')).toHaveText(
|
||||||
@ -1236,6 +1243,11 @@ extrude001 = extrude(50, sketch001)
|
|||||||
|
|
||||||
await page.waitForTimeout(600)
|
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()
|
let previousCodeContent = await page.locator('.cm-content').innerText()
|
||||||
|
|
||||||
// deselect the line tool by clicking it
|
// deselect the line tool by clicking it
|
||||||
|
@ -9,6 +9,7 @@ import {
|
|||||||
executorInputPath,
|
executorInputPath,
|
||||||
} from './test-utils'
|
} from './test-utils'
|
||||||
import { SaveSettingsPayload, SettingsLevel } from 'lib/settings/settingsTypes'
|
import { SaveSettingsPayload, SettingsLevel } from 'lib/settings/settingsTypes'
|
||||||
|
import { SETTINGS_FILE_NAME } from 'lib/constants'
|
||||||
import {
|
import {
|
||||||
TEST_SETTINGS_KEY,
|
TEST_SETTINGS_KEY,
|
||||||
TEST_SETTINGS_CORRUPTED,
|
TEST_SETTINGS_CORRUPTED,
|
||||||
@ -343,7 +344,7 @@ test.describe('Testing settings', () => {
|
|||||||
|
|
||||||
// Selectors and constants
|
// Selectors and constants
|
||||||
const errorHeading = page.getByRole('heading', {
|
const errorHeading = page.getByRole('heading', {
|
||||||
name: 'An unextected error occurred',
|
name: 'An unexpected error occurred',
|
||||||
})
|
})
|
||||||
const projectDirLink = page.getByText('Loaded from')
|
const projectDirLink = page.getByText('Loaded from')
|
||||||
|
|
||||||
@ -372,7 +373,7 @@ test.describe('Testing settings', () => {
|
|||||||
|
|
||||||
// Selectors and constants
|
// Selectors and constants
|
||||||
const errorHeading = page.getByRole('heading', {
|
const errorHeading = page.getByRole('heading', {
|
||||||
name: 'An unextected error occurred',
|
name: 'An unexpected error occurred',
|
||||||
})
|
})
|
||||||
const projectDirLink = page.getByText('Loaded from')
|
const projectDirLink = page.getByText('Loaded from')
|
||||||
|
|
||||||
@ -384,6 +385,66 @@ 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(
|
test(
|
||||||
`Closing settings modal should go back to the original file being viewed`,
|
`Closing settings modal should go back to the original file being viewed`,
|
||||||
{ tag: '@electron' },
|
{ tag: '@electron' },
|
||||||
|
@ -32,10 +32,10 @@ win:
|
|||||||
arch:
|
arch:
|
||||||
- x64
|
- x64
|
||||||
- arm64
|
- arm64
|
||||||
- target: msi
|
# - target: msi
|
||||||
arch:
|
# arch:
|
||||||
- x64
|
# - x64
|
||||||
- arm64
|
# - arm64
|
||||||
signingHashAlgorithms:
|
signingHashAlgorithms:
|
||||||
- sha256
|
- sha256
|
||||||
sign: "./sign-win.js"
|
sign: "./sign-win.js"
|
||||||
@ -47,9 +47,9 @@ win:
|
|||||||
mimeType: text/vnd.zoo.kcl
|
mimeType: text/vnd.zoo.kcl
|
||||||
description: Zoo KCL File
|
description: Zoo KCL File
|
||||||
role: Editor
|
role: Editor
|
||||||
msi:
|
# msi:
|
||||||
oneClick: false
|
# oneClick: false
|
||||||
perMachine: true
|
# perMachine: true
|
||||||
nsis:
|
nsis:
|
||||||
oneClick: false
|
oneClick: false
|
||||||
perMachine: true
|
perMachine: true
|
||||||
@ -73,3 +73,5 @@ publish:
|
|||||||
- provider: generic
|
- provider: generic
|
||||||
url: https://dl.zoo.dev/releases/modeling-app
|
url: https://dl.zoo.dev/releases/modeling-app
|
||||||
channel: latest
|
channel: latest
|
||||||
|
releaseInfo:
|
||||||
|
releaseNotesFile: release-notes.md
|
||||||
|
9
interface.d.ts
vendored
@ -23,7 +23,6 @@ export interface IElectronAPI {
|
|||||||
callback: (eventType: string, path: string) => void
|
callback: (eventType: string, path: string) => void
|
||||||
) => void
|
) => void
|
||||||
watchFileOff: (path: string) => void
|
watchFileOff: (path: string) => void
|
||||||
watchFileObliterate: () => void
|
|
||||||
readFile: (path: string) => ReturnType<fs.readFile>
|
readFile: (path: string) => ReturnType<fs.readFile>
|
||||||
writeFile: (
|
writeFile: (
|
||||||
path: string,
|
path: string,
|
||||||
@ -70,9 +69,13 @@ export interface IElectronAPI {
|
|||||||
kittycad: (access: string, args: any) => any
|
kittycad: (access: string, args: any) => any
|
||||||
listMachines: () => Promise<MachinesListing>
|
listMachines: () => Promise<MachinesListing>
|
||||||
getMachineApiIp: () => Promise<string | null>
|
getMachineApiIp: () => Promise<string | null>
|
||||||
onUpdateDownloaded: (
|
onUpdateDownloadStart: (
|
||||||
callback: (value: string) => void
|
callback: (value: { version: string }) => void
|
||||||
) => Electron.IpcRenderer
|
) => Electron.IpcRenderer
|
||||||
|
onUpdateDownloaded: (
|
||||||
|
callback: (value: { version: string; releaseNotes: string }) => void
|
||||||
|
) => Electron.IpcRenderer
|
||||||
|
onUpdateError: (callback: (value: { error: Error }) => void) => Electron
|
||||||
appRestart: () => void
|
appRestart: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -113,12 +113,21 @@
|
|||||||
],
|
],
|
||||||
"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!",
|
"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
|
"nullable": true
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/MachineState"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Status of the printer -- be it printing, idle, or unreachable. This may dictate if a machine is capable of taking a new job."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"id",
|
"id",
|
||||||
"machine_type",
|
"machine_type",
|
||||||
"make_model"
|
"make_model",
|
||||||
|
"state"
|
||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
@ -143,6 +152,67 @@
|
|||||||
},
|
},
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"MachineState": {
|
||||||
|
"description": "Current state of the machine -- be it printing, idle or offline. This can be used to determine if a printer is in the correct state to take a new job.",
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"description": "If a print state can not be resolved at this time, an Unknown may be returned.",
|
||||||
|
"enum": [
|
||||||
|
"Unknown"
|
||||||
|
],
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Idle, and ready for another job.",
|
||||||
|
"enum": [
|
||||||
|
"Idle"
|
||||||
|
],
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Running a job -- 3D printing or CNC-ing a part.",
|
||||||
|
"enum": [
|
||||||
|
"Running"
|
||||||
|
],
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Machine is currently offline or unreachable.",
|
||||||
|
"enum": [
|
||||||
|
"Offline"
|
||||||
|
],
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Job is underway but halted, waiting for some action to take place.",
|
||||||
|
"enum": [
|
||||||
|
"Paused"
|
||||||
|
],
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Job is finished, but waiting manual action to move back to Idle.",
|
||||||
|
"enum": [
|
||||||
|
"Complete"
|
||||||
|
],
|
||||||
|
"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": {
|
||||||
|
"Failed": {
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"Failed"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"MachineType": {
|
"MachineType": {
|
||||||
"description": "Specific technique by which this Machine takes a design, and produces a real-world 3D object.",
|
"description": "Specific technique by which this Machine takes a design, and produces a real-world 3D object.",
|
||||||
"oneOf": [
|
"oneOf": [
|
||||||
@ -355,6 +425,34 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/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": {
|
"/ping": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "ping",
|
"operationId": "ping",
|
||||||
@ -422,6 +520,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tags": [
|
"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.",
|
"description": "Utilities for making parts and discovering machines.",
|
||||||
"externalDocs": {
|
"externalDocs": {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "zoo-modeling-app",
|
"name": "zoo-modeling-app",
|
||||||
"version": "0.25.5",
|
"version": "0.25.6",
|
||||||
"private": true,
|
"private": true,
|
||||||
"productName": "Zoo Modeling App",
|
"productName": "Zoo Modeling App",
|
||||||
"author": {
|
"author": {
|
||||||
@ -26,7 +26,7 @@
|
|||||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||||
"@headlessui/react": "^1.7.19",
|
"@headlessui/react": "^1.7.19",
|
||||||
"@headlessui/tailwindcss": "^0.2.0",
|
"@headlessui/tailwindcss": "^0.2.0",
|
||||||
"@kittycad/lib": "^2.0.1",
|
"@kittycad/lib": "2.0.7",
|
||||||
"@lezer/highlight": "^1.2.1",
|
"@lezer/highlight": "^1.2.1",
|
||||||
"@lezer/lr": "^1.4.1",
|
"@lezer/lr": "^1.4.1",
|
||||||
"@react-hook/resize-observer": "^2.0.1",
|
"@react-hook/resize-observer": "^2.0.1",
|
||||||
@ -36,6 +36,7 @@
|
|||||||
"@xstate/inspect": "^0.8.0",
|
"@xstate/inspect": "^0.8.0",
|
||||||
"@xstate/react": "^4.1.1",
|
"@xstate/react": "^4.1.1",
|
||||||
"bonjour-service": "^1.2.1",
|
"bonjour-service": "^1.2.1",
|
||||||
|
"chokidar": "^4.0.1",
|
||||||
"codemirror": "^6.0.1",
|
"codemirror": "^6.0.1",
|
||||||
"decamelize": "^6.0.0",
|
"decamelize": "^6.0.0",
|
||||||
"electron-squirrel-startup": "^1.0.1",
|
"electron-squirrel-startup": "^1.0.1",
|
||||||
|
@ -893,6 +893,7 @@ export class CameraControls {
|
|||||||
type: 'zoom_to_fit',
|
type: 'zoom_to_fit',
|
||||||
object_ids: [], // leave empty to zoom to all objects
|
object_ids: [], // leave empty to zoom to all objects
|
||||||
padding: 0.2, // padding around the objects
|
padding: 0.2, // padding around the objects
|
||||||
|
animated: false, // don't animate the zoom for now
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -408,6 +408,7 @@ export async function deleteSegment({
|
|||||||
|
|
||||||
const testExecute = await executeAst({
|
const testExecute = await executeAst({
|
||||||
ast: modifiedAst,
|
ast: modifiedAst,
|
||||||
|
idGenerator: kclManager.execState.idGenerator,
|
||||||
useFakeExecutor: true,
|
useFakeExecutor: true,
|
||||||
engineCommandManager: engineCommandManager,
|
engineCommandManager: engineCommandManager,
|
||||||
})
|
})
|
||||||
|
@ -391,12 +391,14 @@ export class SceneEntities {
|
|||||||
const { truncatedAst, programMemoryOverride, variableDeclarationName } =
|
const { truncatedAst, programMemoryOverride, variableDeclarationName } =
|
||||||
prepared
|
prepared
|
||||||
|
|
||||||
const { programMemory } = await executeAst({
|
const { execState } = await executeAst({
|
||||||
ast: truncatedAst,
|
ast: truncatedAst,
|
||||||
useFakeExecutor: true,
|
useFakeExecutor: true,
|
||||||
engineCommandManager: this.engineCommandManager,
|
engineCommandManager: this.engineCommandManager,
|
||||||
programMemoryOverride,
|
programMemoryOverride,
|
||||||
|
idGenerator: kclManager.execState.idGenerator,
|
||||||
})
|
})
|
||||||
|
const programMemory = execState.memory
|
||||||
const sketch = sketchFromPathToNode({
|
const sketch = sketchFromPathToNode({
|
||||||
pathToNode: sketchPathToNode,
|
pathToNode: sketchPathToNode,
|
||||||
ast: maybeModdedAst,
|
ast: maybeModdedAst,
|
||||||
@ -801,12 +803,14 @@ export class SceneEntities {
|
|||||||
updateRectangleSketch(sketchInit, x, y, tags[0])
|
updateRectangleSketch(sketchInit, x, y, tags[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
const { programMemory } = await executeAst({
|
const { execState } = await executeAst({
|
||||||
ast: truncatedAst,
|
ast: truncatedAst,
|
||||||
useFakeExecutor: true,
|
useFakeExecutor: true,
|
||||||
engineCommandManager: this.engineCommandManager,
|
engineCommandManager: this.engineCommandManager,
|
||||||
programMemoryOverride,
|
programMemoryOverride,
|
||||||
|
idGenerator: kclManager.execState.idGenerator,
|
||||||
})
|
})
|
||||||
|
const programMemory = execState.memory
|
||||||
this.sceneProgramMemory = programMemory
|
this.sceneProgramMemory = programMemory
|
||||||
const sketch = sketchFromKclValue(
|
const sketch = sketchFromKclValue(
|
||||||
programMemory.get(variableDeclarationName),
|
programMemory.get(variableDeclarationName),
|
||||||
@ -848,12 +852,14 @@ export class SceneEntities {
|
|||||||
await kclManager.executeAstMock(_ast)
|
await kclManager.executeAstMock(_ast)
|
||||||
sceneInfra.modelingSend({ type: 'Finish rectangle' })
|
sceneInfra.modelingSend({ type: 'Finish rectangle' })
|
||||||
|
|
||||||
const { programMemory } = await executeAst({
|
const { execState } = await executeAst({
|
||||||
ast: _ast,
|
ast: _ast,
|
||||||
useFakeExecutor: true,
|
useFakeExecutor: true,
|
||||||
engineCommandManager: this.engineCommandManager,
|
engineCommandManager: this.engineCommandManager,
|
||||||
programMemoryOverride,
|
programMemoryOverride,
|
||||||
|
idGenerator: kclManager.execState.idGenerator,
|
||||||
})
|
})
|
||||||
|
const programMemory = execState.memory
|
||||||
|
|
||||||
// Prepare to update the THREEjs scene
|
// Prepare to update the THREEjs scene
|
||||||
this.sceneProgramMemory = programMemory
|
this.sceneProgramMemory = programMemory
|
||||||
@ -965,12 +971,14 @@ export class SceneEntities {
|
|||||||
modded = moddedResult.modifiedAst
|
modded = moddedResult.modifiedAst
|
||||||
}
|
}
|
||||||
|
|
||||||
const { programMemory } = await executeAst({
|
const { execState } = await executeAst({
|
||||||
ast: modded,
|
ast: modded,
|
||||||
useFakeExecutor: true,
|
useFakeExecutor: true,
|
||||||
engineCommandManager: this.engineCommandManager,
|
engineCommandManager: this.engineCommandManager,
|
||||||
programMemoryOverride,
|
programMemoryOverride,
|
||||||
|
idGenerator: kclManager.execState.idGenerator,
|
||||||
})
|
})
|
||||||
|
const programMemory = execState.memory
|
||||||
this.sceneProgramMemory = programMemory
|
this.sceneProgramMemory = programMemory
|
||||||
const sketch = sketchFromKclValue(
|
const sketch = sketchFromKclValue(
|
||||||
programMemory.get(variableDeclarationName),
|
programMemory.get(variableDeclarationName),
|
||||||
@ -1317,12 +1325,14 @@ export class SceneEntities {
|
|||||||
// don't want to mod the user's code yet as they have't committed to the change yet
|
// 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
|
// plus this would be the truncated ast being recast, it would be wrong
|
||||||
codeManager.updateCodeEditor(code)
|
codeManager.updateCodeEditor(code)
|
||||||
const { programMemory } = await executeAst({
|
const { execState } = await executeAst({
|
||||||
ast: truncatedAst,
|
ast: truncatedAst,
|
||||||
useFakeExecutor: true,
|
useFakeExecutor: true,
|
||||||
engineCommandManager: this.engineCommandManager,
|
engineCommandManager: this.engineCommandManager,
|
||||||
programMemoryOverride,
|
programMemoryOverride,
|
||||||
|
idGenerator: kclManager.execState.idGenerator,
|
||||||
})
|
})
|
||||||
|
const programMemory = execState.memory
|
||||||
this.sceneProgramMemory = programMemory
|
this.sceneProgramMemory = programMemory
|
||||||
|
|
||||||
const maybeSketch = programMemory.get(variableDeclarationName)
|
const maybeSketch = programMemory.get(variableDeclarationName)
|
||||||
|
@ -157,7 +157,7 @@ export function useCalc({
|
|||||||
engineCommandManager,
|
engineCommandManager,
|
||||||
useFakeExecutor: true,
|
useFakeExecutor: true,
|
||||||
programMemoryOverride: kclManager.programMemory.clone(),
|
programMemoryOverride: kclManager.programMemory.clone(),
|
||||||
}).then(({ programMemory }) => {
|
}).then(({ execState }) => {
|
||||||
const resultDeclaration = ast.body.find(
|
const resultDeclaration = ast.body.find(
|
||||||
(a) =>
|
(a) =>
|
||||||
a.type === 'VariableDeclaration' &&
|
a.type === 'VariableDeclaration' &&
|
||||||
@ -166,7 +166,7 @@ export function useCalc({
|
|||||||
const init =
|
const init =
|
||||||
resultDeclaration?.type === 'VariableDeclaration' &&
|
resultDeclaration?.type === 'VariableDeclaration' &&
|
||||||
resultDeclaration?.declarations?.[0]?.init
|
resultDeclaration?.declarations?.[0]?.init
|
||||||
const result = programMemory?.get('__result__')?.value
|
const result = execState.memory?.get('__result__')?.value
|
||||||
setCalcResult(typeof result === 'number' ? String(result) : 'NAN')
|
setCalcResult(typeof result === 'number' ? String(result) : 'NAN')
|
||||||
init && setValueNode(init)
|
init && setValueNode(init)
|
||||||
})
|
})
|
||||||
|
@ -91,7 +91,7 @@ function CommandBarSelectionInput({
|
|||||||
<form id="arg-form" onSubmit={handleSubmit}>
|
<form id="arg-form" onSubmit={handleSubmit}>
|
||||||
<label
|
<label
|
||||||
className={
|
className={
|
||||||
'relative flex items-center mx-4 my-4 ' +
|
'relative flex flex-col mx-4 my-4 ' +
|
||||||
(!hasSubmitted || canSubmitSelection || 'text-destroy-50')
|
(!hasSubmitted || canSubmitSelection || 'text-destroy-50')
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@ -100,13 +100,18 @@ function CommandBarSelectionInput({
|
|||||||
: `Please select ${
|
: `Please select ${
|
||||||
arg.multiple ? 'one or more ' : 'one '
|
arg.multiple ? 'one or more ' : 'one '
|
||||||
}${getSemanticSelectionType(arg.selectionTypes).join(' or ')}`}
|
}${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
|
<input
|
||||||
id="selection"
|
id="selection"
|
||||||
name="selection"
|
name="selection"
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
required
|
required
|
||||||
placeholder="Select an entity with your mouse"
|
placeholder="Select an entity with your mouse"
|
||||||
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
|
className="absolute inset-0 w-full h-full opacity-0 cursor-default"
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
if (event.key === 'Backspace') {
|
if (event.key === 'Backspace') {
|
||||||
stepBack()
|
stepBack()
|
||||||
|
111
src/components/DebugDisplayObj.tsx
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
45
src/components/DebugFeatureTree.tsx
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
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
|
||||||
|
}
|
@ -259,7 +259,7 @@ export const FileMachineProvider = ({
|
|||||||
// Refresh the route selected above because it's possible we're on
|
// Refresh the route selected above because it's possible we're on
|
||||||
// the same path on the navigate, which doesn't cause anything to
|
// the same path on the navigate, which doesn't cause anything to
|
||||||
// refresh, leaving a stale execution state.
|
// refresh, leaving a stale execution state.
|
||||||
navigate(0)
|
navigate('.')
|
||||||
return {
|
return {
|
||||||
message: 'No more files in project, created main.kcl',
|
message: 'No more files in project, created main.kcl',
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,7 @@ import {
|
|||||||
import { Popover } from '@headlessui/react'
|
import { Popover } from '@headlessui/react'
|
||||||
import { CustomIcon } from './CustomIcon'
|
import { CustomIcon } from './CustomIcon'
|
||||||
import { reportRejection } from 'lib/trap'
|
import { reportRejection } from 'lib/trap'
|
||||||
|
import { useModelingContext } from 'hooks/useModelingContext'
|
||||||
|
|
||||||
const CANVAS_SIZE = 80
|
const CANVAS_SIZE = 80
|
||||||
const FRUSTUM_SIZE = 0.5
|
const FRUSTUM_SIZE = 0.5
|
||||||
@ -62,6 +63,7 @@ export default function Gizmo() {
|
|||||||
const raycasterIntersect = useRef<Intersection<Object3D> | null>(null)
|
const raycasterIntersect = useRef<Intersection<Object3D> | null>(null)
|
||||||
const cameraPassiveUpdateTimer = useRef(0)
|
const cameraPassiveUpdateTimer = useRef(0)
|
||||||
const raycasterPassiveUpdateTimer = useRef(0)
|
const raycasterPassiveUpdateTimer = useRef(0)
|
||||||
|
const { send: modelingSend } = useModelingContext()
|
||||||
const menuItems = useMemo(
|
const menuItems = useMemo(
|
||||||
() => [
|
() => [
|
||||||
...Object.entries(axisNamesSemantic).map(([axisName, axisSemantic]) => (
|
...Object.entries(axisNamesSemantic).map(([axisName, axisSemantic]) => (
|
||||||
@ -76,6 +78,7 @@ export default function Gizmo() {
|
|||||||
{axisSemantic} view
|
{axisSemantic} view
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
)),
|
)),
|
||||||
|
<ContextMenuDivider />,
|
||||||
<ContextMenuItem
|
<ContextMenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
sceneInfra.camControls.resetCameraPosition().catch(reportRejection)
|
sceneInfra.camControls.resetCameraPosition().catch(reportRejection)
|
||||||
@ -83,6 +86,13 @@ export default function Gizmo() {
|
|||||||
>
|
>
|
||||||
Reset view
|
Reset view
|
||||||
</ContextMenuItem>,
|
</ContextMenuItem>,
|
||||||
|
<ContextMenuItem
|
||||||
|
onClick={() => {
|
||||||
|
modelingSend({ type: 'Center camera on selection' })
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Center view on selection
|
||||||
|
</ContextMenuItem>,
|
||||||
<ContextMenuDivider />,
|
<ContextMenuDivider />,
|
||||||
<ContextMenuItemRefresh />,
|
<ContextMenuItemRefresh />,
|
||||||
],
|
],
|
||||||
|
@ -83,6 +83,7 @@ import {
|
|||||||
} from 'lang/std/engineConnection'
|
} from 'lang/std/engineConnection'
|
||||||
import { submitAndAwaitTextToKcl } from 'lib/textToCad'
|
import { submitAndAwaitTextToKcl } from 'lib/textToCad'
|
||||||
import { useFileContext } from 'hooks/useFileContext'
|
import { useFileContext } from 'hooks/useFileContext'
|
||||||
|
import { uuidv4 } from 'lib/utils'
|
||||||
|
|
||||||
type MachineContext<T extends AnyStateMachine> = {
|
type MachineContext<T extends AnyStateMachine> = {
|
||||||
state: StateFrom<T>
|
state: StateFrom<T>
|
||||||
@ -148,6 +149,13 @@ export const ModelingMachineProvider = ({
|
|||||||
},
|
},
|
||||||
'sketch exit execute': ({ context: { store } }) => {
|
'sketch exit execute': ({ context: { store } }) => {
|
||||||
;(async () => {
|
;(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'
|
sceneInfra.camControls.syncDirection = 'clientToEngine'
|
||||||
|
|
||||||
if (cameraProjection.current === 'perspective') {
|
if (cameraProjection.current === 'perspective') {
|
||||||
@ -243,6 +251,17 @@ export const ModelingMachineProvider = ({
|
|||||||
return {}
|
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 }) => {
|
'Set sketchDetails': assign(({ context: { sketchDetails }, event }) => {
|
||||||
if (event.type !== 'Delete segment') return {}
|
if (event.type !== 'Delete segment') return {}
|
||||||
if (!sketchDetails) return {}
|
if (!sketchDetails) return {}
|
||||||
@ -1037,6 +1056,11 @@ export const ModelingMachineProvider = ({
|
|||||||
modelingSend({ type: 'Delete selection' })
|
modelingSend({ type: 'Delete selection' })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Allow ctrl+alt+c to center to selection
|
||||||
|
useHotkeys(['mod + alt + c'], () => {
|
||||||
|
modelingSend({ type: 'Center camera on selection' })
|
||||||
|
})
|
||||||
|
|
||||||
useStateMachineCommands({
|
useStateMachineCommands({
|
||||||
machineId: 'modeling',
|
machineId: 'modeling',
|
||||||
state: modelingState,
|
state: modelingState,
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { DebugFeatureTree } from 'components/DebugFeatureTree'
|
||||||
import { AstExplorer } from '../../AstExplorer'
|
import { AstExplorer } from '../../AstExplorer'
|
||||||
import { EngineCommands } from '../../EngineCommands'
|
import { EngineCommands } from '../../EngineCommands'
|
||||||
import { CamDebugSettings } from 'clientSideScene/ClientSideSceneComp'
|
import { CamDebugSettings } from 'clientSideScene/ClientSideSceneComp'
|
||||||
@ -12,6 +13,7 @@ export const DebugPane = () => {
|
|||||||
<EngineCommands />
|
<EngineCommands />
|
||||||
<CamDebugSettings />
|
<CamDebugSettings />
|
||||||
<AstExplorer />
|
<AstExplorer />
|
||||||
|
<DebugFeatureTree />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
|
@ -29,8 +29,8 @@ describe('processMemory', () => {
|
|||||||
|> lineTo([2.15, 4.32], %)
|
|> lineTo([2.15, 4.32], %)
|
||||||
// |> rx(90, %)`
|
// |> rx(90, %)`
|
||||||
const ast = parse(code)
|
const ast = parse(code)
|
||||||
const programMemory = await enginelessExecutor(ast, ProgramMemory.empty())
|
const execState = await enginelessExecutor(ast, ProgramMemory.empty())
|
||||||
const output = processMemory(programMemory)
|
const output = processMemory(execState.memory)
|
||||||
expect(output.myVar).toEqual(5)
|
expect(output.myVar).toEqual(5)
|
||||||
expect(output.otherVar).toEqual(3)
|
expect(output.otherVar).toEqual(3)
|
||||||
expect(output).toEqual({
|
expect(output).toEqual({
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
|
import { trap } from 'lib/trap'
|
||||||
import { useMachine } from '@xstate/react'
|
import { useMachine } from '@xstate/react'
|
||||||
import { useNavigate, useRouteLoaderData, useLocation } from 'react-router-dom'
|
import { useNavigate, useRouteLoaderData, useLocation } from 'react-router-dom'
|
||||||
import { PATHS } from 'lib/paths'
|
import { PATHS } from 'lib/paths'
|
||||||
import { authMachine, TOKEN_PERSIST_KEY } from '../machines/authMachine'
|
import { authMachine, TOKEN_PERSIST_KEY } from '../machines/authMachine'
|
||||||
import withBaseUrl from '../lib/withBaseURL'
|
import withBaseUrl from '../lib/withBaseURL'
|
||||||
import React, { createContext, useEffect } from 'react'
|
import React, { createContext, useEffect, useState } from 'react'
|
||||||
import useStateMachineCommands from '../hooks/useStateMachineCommands'
|
import useStateMachineCommands from '../hooks/useStateMachineCommands'
|
||||||
import { settingsMachine } from 'machines/settingsMachine'
|
import { settingsMachine } from 'machines/settingsMachine'
|
||||||
import { toast } from 'react-hot-toast'
|
import { toast } from 'react-hot-toast'
|
||||||
@ -15,7 +16,6 @@ import {
|
|||||||
} from 'lib/theme'
|
} from 'lib/theme'
|
||||||
import decamelize from 'decamelize'
|
import decamelize from 'decamelize'
|
||||||
import { Actor, AnyStateMachine, ContextFrom, Prop, StateFrom } from 'xstate'
|
import { Actor, AnyStateMachine, ContextFrom, Prop, StateFrom } from 'xstate'
|
||||||
import { isDesktop } from 'lib/isDesktop'
|
|
||||||
import { authCommandBarConfig } from 'lib/commandBarConfigs/authCommandConfig'
|
import { authCommandBarConfig } from 'lib/commandBarConfigs/authCommandConfig'
|
||||||
import {
|
import {
|
||||||
kclManager,
|
kclManager,
|
||||||
@ -33,8 +33,14 @@ import {
|
|||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||||
import { Command } from 'lib/commandTypes'
|
import { Command } from 'lib/commandTypes'
|
||||||
import { BaseUnit } from 'lib/settings/settingsTypes'
|
import { BaseUnit } from 'lib/settings/settingsTypes'
|
||||||
import { saveSettings } from 'lib/settings/settingsUtils'
|
import {
|
||||||
|
saveSettings,
|
||||||
|
loadAndValidateSettings,
|
||||||
|
} from 'lib/settings/settingsUtils'
|
||||||
import { reportRejection } from 'lib/trap'
|
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> = {
|
type MachineContext<T extends AnyStateMachine> = {
|
||||||
state: StateFrom<T>
|
state: StateFrom<T>
|
||||||
@ -99,6 +105,9 @@ export const SettingsAuthProviderBase = ({
|
|||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { commandBarSend } = useCommandsContext()
|
const { commandBarSend } = useCommandsContext()
|
||||||
|
const [settingsPath, setSettingsPath] = useState<string | undefined>(
|
||||||
|
undefined
|
||||||
|
)
|
||||||
|
|
||||||
const [settingsState, settingsSend, settingsActor] = useMachine(
|
const [settingsState, settingsSend, settingsActor] = useMachine(
|
||||||
settingsMachine.provide({
|
settingsMachine.provide({
|
||||||
@ -191,7 +200,11 @@ export const SettingsAuthProviderBase = ({
|
|||||||
console.error('Error executing AST after settings change', e)
|
console.error('Error executing AST after settings change', e)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
persistSettings: ({ context }) => {
|
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
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
saveSettings(context, loadedProject?.project?.path)
|
saveSettings(context, loadedProject?.project?.path)
|
||||||
},
|
},
|
||||||
@ -201,6 +214,23 @@ export const SettingsAuthProviderBase = ({
|
|||||||
)
|
)
|
||||||
settingsStateRef = settingsState.context
|
settingsStateRef = settingsState.context
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isDesktop()) return
|
||||||
|
getAppSettingsFilePath().then(setSettingsPath).catch(trap)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useFileSystemWatcher(
|
||||||
|
async () => {
|
||||||
|
const data = await loadAndValidateSettings(loadedProject?.project?.path)
|
||||||
|
settingsSend({
|
||||||
|
type: 'Set all settings',
|
||||||
|
settings: data.settings,
|
||||||
|
doNotPersist: true,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
settingsPath ? [settingsPath] : []
|
||||||
|
)
|
||||||
|
|
||||||
// Add settings commands to the command bar
|
// Add settings commands to the command bar
|
||||||
// They're treated slightly differently than other commands
|
// They're treated slightly differently than other commands
|
||||||
// Because their state machine doesn't have a meaningful .nextEvents,
|
// Because their state machine doesn't have a meaningful .nextEvents,
|
||||||
|
153
src/components/ToastUpdate.test.tsx
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
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()
|
||||||
|
})
|
||||||
|
})
|
@ -1,14 +1,23 @@
|
|||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
import { ActionButton } from './ActionButton'
|
import { ActionButton } from './ActionButton'
|
||||||
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
||||||
|
import { Marked } from '@ts-stack/markdown'
|
||||||
|
|
||||||
export function ToastUpdate({
|
export function ToastUpdate({
|
||||||
version,
|
version,
|
||||||
|
releaseNotes,
|
||||||
onRestart,
|
onRestart,
|
||||||
|
onDismiss,
|
||||||
}: {
|
}: {
|
||||||
version: string
|
version: string
|
||||||
|
releaseNotes?: string
|
||||||
onRestart: () => void
|
onRestart: () => void
|
||||||
|
onDismiss: () => void
|
||||||
}) {
|
}) {
|
||||||
|
const containsBreakingChanges = releaseNotes
|
||||||
|
?.toLocaleLowerCase()
|
||||||
|
.includes('breaking')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="inset-0 z-50 grid place-content-center rounded bg-chalkboard-110/50 shadow-md">
|
<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">
|
<div className="max-w-3xl min-w-[35rem] p-8 rounded bg-chalkboard-10 dark:bg-chalkboard-90">
|
||||||
@ -19,7 +28,7 @@ export function ToastUpdate({
|
|||||||
>
|
>
|
||||||
v{version}
|
v{version}
|
||||||
</span>
|
</span>
|
||||||
<span className="ml-4 text-md text-bold">
|
<p className="ml-4 text-md text-bold">
|
||||||
A new update has downloaded and will be available next time you
|
A new update has downloaded and will be available next time you
|
||||||
start the app. You can view the release notes{' '}
|
start the app. You can view the release notes{' '}
|
||||||
<a
|
<a
|
||||||
@ -32,15 +41,39 @@ export function ToastUpdate({
|
|||||||
>
|
>
|
||||||
here on GitHub.
|
here on GitHub.
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</p>
|
||||||
</div>
|
</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">
|
<div className="flex justify-between gap-8">
|
||||||
<ActionButton
|
<ActionButton
|
||||||
Element="button"
|
Element="button"
|
||||||
iconStart={{
|
iconStart={{
|
||||||
icon: 'arrowRotateRight',
|
icon: 'arrowRotateRight',
|
||||||
}}
|
}}
|
||||||
name="Restart app now"
|
name="restart"
|
||||||
onClick={onRestart}
|
onClick={onRestart}
|
||||||
>
|
>
|
||||||
Restart app now
|
Restart app now
|
||||||
@ -50,9 +83,10 @@ export function ToastUpdate({
|
|||||||
iconStart={{
|
iconStart={{
|
||||||
icon: 'checkmark',
|
icon: 'checkmark',
|
||||||
}}
|
}}
|
||||||
name="Got it"
|
name="dismiss"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
toast.dismiss()
|
toast.dismiss()
|
||||||
|
onDismiss()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Got it
|
Got it
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
import { styleTags, tags as t } from '@lezer/highlight'
|
import { styleTags, tags as t } from '@lezer/highlight'
|
||||||
|
|
||||||
export const kclHighlight = styleTags({
|
export const kclHighlight = styleTags({
|
||||||
|
'import export': t.moduleKeyword,
|
||||||
|
ImportItemAs: t.definitionKeyword,
|
||||||
|
ImportFrom: t.moduleKeyword,
|
||||||
'fn var let const': t.definitionKeyword,
|
'fn var let const': t.definitionKeyword,
|
||||||
|
'if else': t.controlKeyword,
|
||||||
return: t.controlKeyword,
|
return: t.controlKeyword,
|
||||||
'true false': t.bool,
|
'true false': t.bool,
|
||||||
nil: t.null,
|
nil: t.null,
|
||||||
|
@ -15,8 +15,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
statement[@isGroup=Statement] {
|
statement[@isGroup=Statement] {
|
||||||
FunctionDeclaration { kw<"fn"> VariableDefinition Equals ParamList Arrow Body } |
|
ImportStatement { kw<"import"> ImportItems ImportFrom String } |
|
||||||
VariableDeclaration { (kw<"var"> | kw<"let"> | kw<"const">) VariableDefinition Equals expression } |
|
FunctionDeclaration { kw<"export">? kw<"fn"> VariableDefinition Equals ParamList Arrow Body } |
|
||||||
|
VariableDeclaration { kw<"export">? (kw<"var"> | kw<"let"> | kw<"const">)? VariableDefinition Equals expression } |
|
||||||
ReturnStatement { kw<"return"> expression } |
|
ReturnStatement { kw<"return"> expression } |
|
||||||
ExpressionStatement { expression }
|
ExpressionStatement { expression }
|
||||||
}
|
}
|
||||||
@ -25,6 +26,9 @@ ParamList { "(" commaSep<Parameter { VariableDefinition "?"? (":" type)? }> ")"
|
|||||||
|
|
||||||
Body { "{" statement* "}" }
|
Body { "{" statement* "}" }
|
||||||
|
|
||||||
|
ImportItems { commaSep1NoTrailingComma<ImportItem> }
|
||||||
|
ImportItem { identifier (ImportItemAs identifier)? }
|
||||||
|
|
||||||
expression[@isGroup=Expression] {
|
expression[@isGroup=Expression] {
|
||||||
String |
|
String |
|
||||||
Number |
|
Number |
|
||||||
@ -40,6 +44,7 @@ expression[@isGroup=Expression] {
|
|||||||
} |
|
} |
|
||||||
UnaryExpression { UnaryOp expression } |
|
UnaryExpression { UnaryOp expression } |
|
||||||
ParenthesizedExpression { "(" expression ")" } |
|
ParenthesizedExpression { "(" expression ")" } |
|
||||||
|
IfExpression { kw<"if"> expression Body kw<"else"> Body } |
|
||||||
CallExpression { expression !call ArgumentList } |
|
CallExpression { expression !call ArgumentList } |
|
||||||
ArrayExpression { "[" commaSep<expression | IntegerRange { expression !range ".." expression }> "]" } |
|
ArrayExpression { "[" commaSep<expression | IntegerRange { expression !range ".." expression }> "]" } |
|
||||||
ObjectExpression { "{" commaSep<ObjectProperty> "}" } |
|
ObjectExpression { "{" commaSep<ObjectProperty> "}" } |
|
||||||
@ -73,6 +78,8 @@ kw<term> { @specialize[@name={term}]<identifier, term> }
|
|||||||
|
|
||||||
commaSep<term> { (term ("," term)*)? ","? }
|
commaSep<term> { (term ("," term)*)? ","? }
|
||||||
|
|
||||||
|
commaSep1NoTrailingComma<term> { term ("," term)* }
|
||||||
|
|
||||||
@tokens {
|
@tokens {
|
||||||
String[isolate] { "'" ("\\" _ | !['\\])* "'" | '"' ("\\" _ | !["\\])* '"' }
|
String[isolate] { "'" ("\\" _ | !['\\])* "'" | '"' ("\\" _ | !["\\])* '"' }
|
||||||
|
|
||||||
@ -105,6 +112,9 @@ commaSep<term> { (term ("," term)*)? ","? }
|
|||||||
|
|
||||||
Shebang { "#!" ![\n]* }
|
Shebang { "#!" ![\n]* }
|
||||||
|
|
||||||
|
ImportItemAs { "as" }
|
||||||
|
ImportFrom { "from" }
|
||||||
|
|
||||||
"(" ")"
|
"(" ")"
|
||||||
"{" "}"
|
"{" "}"
|
||||||
"[" "]"
|
"[" "]"
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { isDesktop } from 'lib/isDesktop'
|
import { isDesktop } from 'lib/isDesktop'
|
||||||
|
import { reportRejection } from 'lib/trap'
|
||||||
import { useEffect, useState, useRef } from 'react'
|
import { useEffect, useState, useRef } from 'react'
|
||||||
|
|
||||||
type Path = string
|
type Path = string
|
||||||
@ -11,13 +12,13 @@ type Path = string
|
|||||||
// watcher.addListener(() => { ... }).
|
// watcher.addListener(() => { ... }).
|
||||||
|
|
||||||
export const useFileSystemWatcher = (
|
export const useFileSystemWatcher = (
|
||||||
callback: (path: Path) => void,
|
callback: (path: Path) => Promise<void>,
|
||||||
dependencyArray: Path[]
|
dependencyArray: Path[]
|
||||||
): void => {
|
): void => {
|
||||||
// Track a ref to the callback. This is how we get the callback updated
|
// Track a ref to the callback. This is how we get the callback updated
|
||||||
// across the NodeJS<->Browser boundary.
|
// across the NodeJS<->Browser boundary.
|
||||||
const callbackRef = useRef<{ fn: (path: Path) => void }>({
|
const callbackRef = useRef<{ fn: (path: Path) => Promise<void> }>({
|
||||||
fn: (_path) => {},
|
fn: async (_path) => {},
|
||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -35,7 +36,9 @@ export const useFileSystemWatcher = (
|
|||||||
if (!isDesktop()) return
|
if (!isDesktop()) return
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.electron.watchFileObliterate()
|
for (let path of dependencyArray) {
|
||||||
|
window.electron.watchFileOff(path)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@ -46,6 +49,9 @@ export const useFileSystemWatcher = (
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasDiff =
|
||||||
|
difference(dependencyArray, dependencyArrayTracked)[0].length !== 0
|
||||||
|
|
||||||
// Removing 1 watcher at a time is only possible because in a filesystem,
|
// 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).
|
// 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.
|
// Otherwise we would have to obliterate() the whole list and reconstruct it.
|
||||||
@ -53,6 +59,8 @@ export const useFileSystemWatcher = (
|
|||||||
// The hook is useless on web.
|
// The hook is useless on web.
|
||||||
if (!isDesktop()) return
|
if (!isDesktop()) return
|
||||||
|
|
||||||
|
if (!hasDiff) return
|
||||||
|
|
||||||
const [pathsRemoved, pathsRemaining] = difference(
|
const [pathsRemoved, pathsRemaining] = difference(
|
||||||
dependencyArrayTracked,
|
dependencyArrayTracked,
|
||||||
dependencyArray
|
dependencyArray
|
||||||
@ -62,10 +70,10 @@ export const useFileSystemWatcher = (
|
|||||||
}
|
}
|
||||||
const [pathsAdded] = difference(dependencyArray, dependencyArrayTracked)
|
const [pathsAdded] = difference(dependencyArray, dependencyArrayTracked)
|
||||||
for (let path of pathsAdded) {
|
for (let path of pathsAdded) {
|
||||||
window.electron.watchFileOn(path, (_eventType: string, path: Path) =>
|
window.electron.watchFileOn(path, (_eventType: string, path: Path) => {
|
||||||
callbackRef.current.fn(path)
|
callbackRef.current.fn(path).catch(reportRejection)
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
setDependencyArrayTracked(pathsRemaining.concat(pathsAdded))
|
setDependencyArrayTracked(pathsRemaining.concat(pathsAdded))
|
||||||
}, [difference(dependencyArray, dependencyArrayTracked)[0].length !== 0])
|
}, [hasDiff])
|
||||||
}
|
}
|
||||||
|
@ -293,6 +293,24 @@ code {
|
|||||||
which lets you use them with @apply in your CSS, and get
|
which lets you use them with @apply in your CSS, and get
|
||||||
autocomplete in classNames in your JSX.
|
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,
|
#code-mirror-override .cm-scroller,
|
||||||
|
@ -8,6 +8,7 @@ import ModalContainer from 'react-modal-promise'
|
|||||||
import { isDesktop } from 'lib/isDesktop'
|
import { isDesktop } from 'lib/isDesktop'
|
||||||
import { AppStreamProvider } from 'AppState'
|
import { AppStreamProvider } from 'AppState'
|
||||||
import { ToastUpdate } from 'components/ToastUpdate'
|
import { ToastUpdate } from 'components/ToastUpdate'
|
||||||
|
import { AUTO_UPDATER_TOAST_ID } from 'lib/constants'
|
||||||
|
|
||||||
// uncomment for xstate inspector
|
// uncomment for xstate inspector
|
||||||
// import { DEV } from 'env'
|
// import { DEV } from 'env'
|
||||||
@ -53,17 +54,35 @@ root.render(
|
|||||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||||
reportWebVitals()
|
reportWebVitals()
|
||||||
|
|
||||||
isDesktop() &&
|
if (isDesktop()) {
|
||||||
window.electron.onUpdateDownloaded((version: string) => {
|
// 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 }) => {
|
||||||
const message = `A new update (${version}) was downloaded and will be available next time you open the app.`
|
const message = `A new update (${version}) was downloaded and will be available next time you open the app.`
|
||||||
console.log(message)
|
console.log(message)
|
||||||
toast.custom(
|
toast.custom(
|
||||||
ToastUpdate({
|
ToastUpdate({
|
||||||
version,
|
version,
|
||||||
|
releaseNotes,
|
||||||
onRestart: () => {
|
onRestart: () => {
|
||||||
window.electron.appRestart()
|
window.electron.appRestart()
|
||||||
},
|
},
|
||||||
|
onDismiss: () => {},
|
||||||
}),
|
}),
|
||||||
{ duration: 30000 }
|
{ duration: 30000, id: AUTO_UPDATER_TOAST_ID }
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
@ -8,6 +8,8 @@ import { EXECUTE_AST_INTERRUPT_ERROR_MESSAGE } from 'lib/constants'
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
CallExpression,
|
CallExpression,
|
||||||
|
emptyExecState,
|
||||||
|
ExecState,
|
||||||
initPromise,
|
initPromise,
|
||||||
parse,
|
parse,
|
||||||
PathToNode,
|
PathToNode,
|
||||||
@ -42,6 +44,7 @@ export class KclManager {
|
|||||||
},
|
},
|
||||||
digest: null,
|
digest: null,
|
||||||
}
|
}
|
||||||
|
private _execState: ExecState = emptyExecState()
|
||||||
private _programMemory: ProgramMemory = ProgramMemory.empty()
|
private _programMemory: ProgramMemory = ProgramMemory.empty()
|
||||||
lastSuccessfulProgramMemory: ProgramMemory = ProgramMemory.empty()
|
lastSuccessfulProgramMemory: ProgramMemory = ProgramMemory.empty()
|
||||||
private _logs: string[] = []
|
private _logs: string[] = []
|
||||||
@ -72,11 +75,21 @@ export class KclManager {
|
|||||||
get programMemory() {
|
get programMemory() {
|
||||||
return this._programMemory
|
return this._programMemory
|
||||||
}
|
}
|
||||||
set programMemory(programMemory) {
|
// This is private because callers should be setting the entire execState.
|
||||||
|
private set programMemory(programMemory) {
|
||||||
this._programMemory = programMemory
|
this._programMemory = programMemory
|
||||||
this._programMemoryCallBack(programMemory)
|
this._programMemoryCallBack(programMemory)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
set execState(execState) {
|
||||||
|
this._execState = execState
|
||||||
|
this.programMemory = execState.memory
|
||||||
|
}
|
||||||
|
|
||||||
|
get execState() {
|
||||||
|
return this._execState
|
||||||
|
}
|
||||||
|
|
||||||
get logs() {
|
get logs() {
|
||||||
return this._logs
|
return this._logs
|
||||||
}
|
}
|
||||||
@ -253,8 +266,9 @@ export class KclManager {
|
|||||||
// Make sure we clear before starting again. End session will do this.
|
// Make sure we clear before starting again. End session will do this.
|
||||||
this.engineCommandManager?.endSession()
|
this.engineCommandManager?.endSession()
|
||||||
await this.ensureWasmInit()
|
await this.ensureWasmInit()
|
||||||
const { logs, errors, programMemory, isInterrupted } = await executeAst({
|
const { logs, errors, execState, isInterrupted } = await executeAst({
|
||||||
ast,
|
ast,
|
||||||
|
idGenerator: this.execState.idGenerator,
|
||||||
engineCommandManager: this.engineCommandManager,
|
engineCommandManager: this.engineCommandManager,
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -264,7 +278,7 @@ export class KclManager {
|
|||||||
this.lints = await lintAst({ ast: ast })
|
this.lints = await lintAst({ ast: ast })
|
||||||
|
|
||||||
sceneInfra.modelingSend({ type: 'code edit during sketch' })
|
sceneInfra.modelingSend({ type: 'code edit during sketch' })
|
||||||
defaultSelectionFilter(programMemory, this.engineCommandManager)
|
defaultSelectionFilter(execState.memory, this.engineCommandManager)
|
||||||
|
|
||||||
if (args.zoomToFit) {
|
if (args.zoomToFit) {
|
||||||
let zoomObjectId: string | undefined = ''
|
let zoomObjectId: string | undefined = ''
|
||||||
@ -282,6 +296,7 @@ export class KclManager {
|
|||||||
type: 'zoom_to_fit',
|
type: 'zoom_to_fit',
|
||||||
object_ids: zoomObjectId ? [zoomObjectId] : [], // leave empty to zoom to all objects
|
object_ids: zoomObjectId ? [zoomObjectId] : [], // leave empty to zoom to all objects
|
||||||
padding: 0.1, // padding around the objects
|
padding: 0.1, // padding around the objects
|
||||||
|
animated: false, // don't animate the zoom for now
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -294,12 +309,20 @@ export class KclManager {
|
|||||||
this._cancelTokens.delete(currentExecutionId)
|
this._cancelTokens.delete(currentExecutionId)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Exit sketch mode if the AST is empty
|
||||||
|
if (this._isAstEmpty(ast)) {
|
||||||
|
await this.disableSketchMode()
|
||||||
|
}
|
||||||
|
|
||||||
this.logs = logs
|
this.logs = logs
|
||||||
// Do not add the errors since the program was interrupted and the error is not a real KCL error
|
// Do not add the errors since the program was interrupted and the error is not a real KCL error
|
||||||
this.addKclErrors(isInterrupted ? [] : errors)
|
this.addKclErrors(isInterrupted ? [] : errors)
|
||||||
this.programMemory = programMemory
|
// Reset the next ID index so that we reuse the previous IDs next time.
|
||||||
|
execState.idGenerator.nextId = 0
|
||||||
|
this.execState = execState
|
||||||
if (!errors.length) {
|
if (!errors.length) {
|
||||||
this.lastSuccessfulProgramMemory = programMemory
|
this.lastSuccessfulProgramMemory = execState.memory
|
||||||
}
|
}
|
||||||
this.ast = { ...ast }
|
this.ast = { ...ast }
|
||||||
this._executeCallback()
|
this._executeCallback()
|
||||||
@ -337,17 +360,19 @@ export class KclManager {
|
|||||||
await codeManager.writeToFile()
|
await codeManager.writeToFile()
|
||||||
this._ast = { ...newAst }
|
this._ast = { ...newAst }
|
||||||
|
|
||||||
const { logs, errors, programMemory } = await executeAst({
|
const { logs, errors, execState } = await executeAst({
|
||||||
ast: newAst,
|
ast: newAst,
|
||||||
|
idGenerator: this.execState.idGenerator,
|
||||||
engineCommandManager: this.engineCommandManager,
|
engineCommandManager: this.engineCommandManager,
|
||||||
useFakeExecutor: true,
|
useFakeExecutor: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
this._logs = logs
|
this._logs = logs
|
||||||
this._kclErrors = errors
|
this._kclErrors = errors
|
||||||
this._programMemory = programMemory
|
this._execState = execState
|
||||||
|
this._programMemory = execState.memory
|
||||||
if (!errors.length) {
|
if (!errors.length) {
|
||||||
this.lastSuccessfulProgramMemory = programMemory
|
this.lastSuccessfulProgramMemory = execState.memory
|
||||||
}
|
}
|
||||||
if (updates !== 'artifactRanges') return
|
if (updates !== 'artifactRanges') return
|
||||||
|
|
||||||
@ -552,6 +577,24 @@ export class KclManager {
|
|||||||
defaultSelectionFilter() {
|
defaultSelectionFilter() {
|
||||||
defaultSelectionFilter(this.programMemory, this.engineCommandManager)
|
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(
|
function defaultSelectionFilter(
|
||||||
|
@ -14,9 +14,9 @@ const mySketch001 = startSketchOn('XY')
|
|||||||
|> lineTo([-1.59, -1.54], %)
|
|> lineTo([-1.59, -1.54], %)
|
||||||
|> lineTo([0.46, -5.82], %)
|
|> lineTo([0.46, -5.82], %)
|
||||||
// |> rx(45, %)`
|
// |> rx(45, %)`
|
||||||
const programMemory = await enginelessExecutor(parse(code))
|
const execState = await enginelessExecutor(parse(code))
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const sketch001 = programMemory?.get('mySketch001')
|
const sketch001 = execState.memory.get('mySketch001')
|
||||||
expect(sketch001).toEqual({
|
expect(sketch001).toEqual({
|
||||||
type: 'UserVal',
|
type: 'UserVal',
|
||||||
__meta: [{ sourceRange: [46, 71] }],
|
__meta: [{ sourceRange: [46, 71] }],
|
||||||
@ -68,9 +68,9 @@ const mySketch001 = startSketchOn('XY')
|
|||||||
|> lineTo([0.46, -5.82], %)
|
|> lineTo([0.46, -5.82], %)
|
||||||
// |> rx(45, %)
|
// |> rx(45, %)
|
||||||
|> extrude(2, %)`
|
|> extrude(2, %)`
|
||||||
const programMemory = await enginelessExecutor(parse(code))
|
const execState = await enginelessExecutor(parse(code))
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const sketch001 = programMemory?.get('mySketch001')
|
const sketch001 = execState.memory.get('mySketch001')
|
||||||
expect(sketch001).toEqual({
|
expect(sketch001).toEqual({
|
||||||
type: 'Solid',
|
type: 'Solid',
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
@ -148,9 +148,10 @@ const sk2 = startSketchOn('XY')
|
|||||||
|> extrude(2, %)
|
|> extrude(2, %)
|
||||||
|
|
||||||
`
|
`
|
||||||
const programMemory = await enginelessExecutor(parse(code))
|
const execState = await enginelessExecutor(parse(code))
|
||||||
|
const programMemory = execState.memory
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const geos = [programMemory?.get('theExtrude'), programMemory?.get('sk2')]
|
const geos = [programMemory.get('theExtrude'), programMemory.get('sk2')]
|
||||||
expect(geos).toEqual([
|
expect(geos).toEqual([
|
||||||
{
|
{
|
||||||
type: 'Solid',
|
type: 'Solid',
|
||||||
|
@ -443,6 +443,6 @@ async function exe(
|
|||||||
) {
|
) {
|
||||||
const ast = parse(code)
|
const ast = parse(code)
|
||||||
|
|
||||||
const result = await enginelessExecutor(ast, programMemory)
|
const execState = await enginelessExecutor(ast, programMemory)
|
||||||
return result
|
return execState.memory
|
||||||
}
|
}
|
||||||
|
@ -4,11 +4,14 @@ import {
|
|||||||
ProgramMemory,
|
ProgramMemory,
|
||||||
programMemoryInit,
|
programMemoryInit,
|
||||||
kclLint,
|
kclLint,
|
||||||
|
emptyExecState,
|
||||||
|
ExecState,
|
||||||
} from 'lang/wasm'
|
} from 'lang/wasm'
|
||||||
import { enginelessExecutor } from 'lib/testHelpers'
|
import { enginelessExecutor } from 'lib/testHelpers'
|
||||||
import { EngineCommandManager } from 'lang/std/engineConnection'
|
import { EngineCommandManager } from 'lang/std/engineConnection'
|
||||||
import { KCLError } from 'lang/errors'
|
import { KCLError } from 'lang/errors'
|
||||||
import { Diagnostic } from '@codemirror/lint'
|
import { Diagnostic } from '@codemirror/lint'
|
||||||
|
import { IdGenerator } from 'wasm-lib/kcl/bindings/IdGenerator'
|
||||||
|
|
||||||
export type ToolTip =
|
export type ToolTip =
|
||||||
| 'lineTo'
|
| 'lineTo'
|
||||||
@ -47,16 +50,18 @@ export async function executeAst({
|
|||||||
engineCommandManager,
|
engineCommandManager,
|
||||||
useFakeExecutor = false,
|
useFakeExecutor = false,
|
||||||
programMemoryOverride,
|
programMemoryOverride,
|
||||||
|
idGenerator,
|
||||||
}: {
|
}: {
|
||||||
ast: Program
|
ast: Program
|
||||||
engineCommandManager: EngineCommandManager
|
engineCommandManager: EngineCommandManager
|
||||||
useFakeExecutor?: boolean
|
useFakeExecutor?: boolean
|
||||||
programMemoryOverride?: ProgramMemory
|
programMemoryOverride?: ProgramMemory
|
||||||
|
idGenerator?: IdGenerator
|
||||||
isInterrupted?: boolean
|
isInterrupted?: boolean
|
||||||
}): Promise<{
|
}): Promise<{
|
||||||
logs: string[]
|
logs: string[]
|
||||||
errors: KCLError[]
|
errors: KCLError[]
|
||||||
programMemory: ProgramMemory
|
execState: ExecState
|
||||||
isInterrupted: boolean
|
isInterrupted: boolean
|
||||||
}> {
|
}> {
|
||||||
try {
|
try {
|
||||||
@ -65,15 +70,21 @@ export async function executeAst({
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
engineCommandManager.startNewSession()
|
engineCommandManager.startNewSession()
|
||||||
}
|
}
|
||||||
const programMemory = await (useFakeExecutor
|
const execState = await (useFakeExecutor
|
||||||
? enginelessExecutor(ast, programMemoryOverride || programMemoryInit())
|
? enginelessExecutor(ast, programMemoryOverride || programMemoryInit())
|
||||||
: _executor(ast, programMemoryInit(), engineCommandManager, false))
|
: _executor(
|
||||||
|
ast,
|
||||||
|
programMemoryInit(),
|
||||||
|
idGenerator,
|
||||||
|
engineCommandManager,
|
||||||
|
false
|
||||||
|
))
|
||||||
|
|
||||||
await engineCommandManager.waitForAllCommands()
|
await engineCommandManager.waitForAllCommands()
|
||||||
return {
|
return {
|
||||||
logs: [],
|
logs: [],
|
||||||
errors: [],
|
errors: [],
|
||||||
programMemory,
|
execState,
|
||||||
isInterrupted: false,
|
isInterrupted: false,
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
@ -89,7 +100,7 @@ export async function executeAst({
|
|||||||
return {
|
return {
|
||||||
errors: [e],
|
errors: [e],
|
||||||
logs: [],
|
logs: [],
|
||||||
programMemory: ProgramMemory.empty(),
|
execState: emptyExecState(),
|
||||||
isInterrupted,
|
isInterrupted,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -97,7 +108,7 @@ export async function executeAst({
|
|||||||
return {
|
return {
|
||||||
logs: [e],
|
logs: [e],
|
||||||
errors: [],
|
errors: [],
|
||||||
programMemory: ProgramMemory.empty(),
|
execState: emptyExecState(),
|
||||||
isInterrupted,
|
isInterrupted,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -220,11 +220,11 @@ yo2 = hmm([identifierGuy + 5])`
|
|||||||
it('should move a binary expression into a new variable', async () => {
|
it('should move a binary expression into a new variable', async () => {
|
||||||
const ast = parse(code)
|
const ast = parse(code)
|
||||||
if (err(ast)) throw ast
|
if (err(ast)) throw ast
|
||||||
const programMemory = await enginelessExecutor(ast)
|
const execState = await enginelessExecutor(ast)
|
||||||
const startIndex = code.indexOf('100 + 100') + 1
|
const startIndex = code.indexOf('100 + 100') + 1
|
||||||
const { modifiedAst } = moveValueIntoNewVariable(
|
const { modifiedAst } = moveValueIntoNewVariable(
|
||||||
ast,
|
ast,
|
||||||
programMemory,
|
execState.memory,
|
||||||
[startIndex, startIndex],
|
[startIndex, startIndex],
|
||||||
'newVar'
|
'newVar'
|
||||||
)
|
)
|
||||||
@ -235,11 +235,11 @@ yo2 = hmm([identifierGuy + 5])`
|
|||||||
it('should move a value into a new variable', async () => {
|
it('should move a value into a new variable', async () => {
|
||||||
const ast = parse(code)
|
const ast = parse(code)
|
||||||
if (err(ast)) throw ast
|
if (err(ast)) throw ast
|
||||||
const programMemory = await enginelessExecutor(ast)
|
const execState = await enginelessExecutor(ast)
|
||||||
const startIndex = code.indexOf('2.8') + 1
|
const startIndex = code.indexOf('2.8') + 1
|
||||||
const { modifiedAst } = moveValueIntoNewVariable(
|
const { modifiedAst } = moveValueIntoNewVariable(
|
||||||
ast,
|
ast,
|
||||||
programMemory,
|
execState.memory,
|
||||||
[startIndex, startIndex],
|
[startIndex, startIndex],
|
||||||
'newVar'
|
'newVar'
|
||||||
)
|
)
|
||||||
@ -250,11 +250,11 @@ yo2 = hmm([identifierGuy + 5])`
|
|||||||
it('should move a callExpression into a new variable', async () => {
|
it('should move a callExpression into a new variable', async () => {
|
||||||
const ast = parse(code)
|
const ast = parse(code)
|
||||||
if (err(ast)) throw ast
|
if (err(ast)) throw ast
|
||||||
const programMemory = await enginelessExecutor(ast)
|
const execState = await enginelessExecutor(ast)
|
||||||
const startIndex = code.indexOf('def(')
|
const startIndex = code.indexOf('def(')
|
||||||
const { modifiedAst } = moveValueIntoNewVariable(
|
const { modifiedAst } = moveValueIntoNewVariable(
|
||||||
ast,
|
ast,
|
||||||
programMemory,
|
execState.memory,
|
||||||
[startIndex, startIndex],
|
[startIndex, startIndex],
|
||||||
'newVar'
|
'newVar'
|
||||||
)
|
)
|
||||||
@ -265,11 +265,11 @@ yo2 = hmm([identifierGuy + 5])`
|
|||||||
it('should move a binary expression with call expression into a new variable', async () => {
|
it('should move a binary expression with call expression into a new variable', async () => {
|
||||||
const ast = parse(code)
|
const ast = parse(code)
|
||||||
if (err(ast)) throw ast
|
if (err(ast)) throw ast
|
||||||
const programMemory = await enginelessExecutor(ast)
|
const execState = await enginelessExecutor(ast)
|
||||||
const startIndex = code.indexOf('jkl(') + 1
|
const startIndex = code.indexOf('jkl(') + 1
|
||||||
const { modifiedAst } = moveValueIntoNewVariable(
|
const { modifiedAst } = moveValueIntoNewVariable(
|
||||||
ast,
|
ast,
|
||||||
programMemory,
|
execState.memory,
|
||||||
[startIndex, startIndex],
|
[startIndex, startIndex],
|
||||||
'newVar'
|
'newVar'
|
||||||
)
|
)
|
||||||
@ -280,11 +280,11 @@ yo2 = hmm([identifierGuy + 5])`
|
|||||||
it('should move a identifier into a new variable', async () => {
|
it('should move a identifier into a new variable', async () => {
|
||||||
const ast = parse(code)
|
const ast = parse(code)
|
||||||
if (err(ast)) throw ast
|
if (err(ast)) throw ast
|
||||||
const programMemory = await enginelessExecutor(ast)
|
const execState = await enginelessExecutor(ast)
|
||||||
const startIndex = code.indexOf('identifierGuy +') + 1
|
const startIndex = code.indexOf('identifierGuy +') + 1
|
||||||
const { modifiedAst } = moveValueIntoNewVariable(
|
const { modifiedAst } = moveValueIntoNewVariable(
|
||||||
ast,
|
ast,
|
||||||
programMemory,
|
execState.memory,
|
||||||
[startIndex, startIndex],
|
[startIndex, startIndex],
|
||||||
'newVar'
|
'newVar'
|
||||||
)
|
)
|
||||||
@ -465,7 +465,7 @@ describe('Testing deleteSegmentFromPipeExpression', () => {
|
|||||||
|> line([306.21, 198.87], %)`
|
|> line([306.21, 198.87], %)`
|
||||||
const ast = parse(code)
|
const ast = parse(code)
|
||||||
if (err(ast)) throw ast
|
if (err(ast)) throw ast
|
||||||
const programMemory = await enginelessExecutor(ast)
|
const execState = await enginelessExecutor(ast)
|
||||||
const lineOfInterest = 'line([306.21, 198.85], %, $a)'
|
const lineOfInterest = 'line([306.21, 198.85], %, $a)'
|
||||||
const range: [number, number] = [
|
const range: [number, number] = [
|
||||||
code.indexOf(lineOfInterest),
|
code.indexOf(lineOfInterest),
|
||||||
@ -475,7 +475,7 @@ describe('Testing deleteSegmentFromPipeExpression', () => {
|
|||||||
const modifiedAst = deleteSegmentFromPipeExpression(
|
const modifiedAst = deleteSegmentFromPipeExpression(
|
||||||
[],
|
[],
|
||||||
ast,
|
ast,
|
||||||
programMemory,
|
execState.memory,
|
||||||
code,
|
code,
|
||||||
pathToNode
|
pathToNode
|
||||||
)
|
)
|
||||||
@ -543,7 +543,7 @@ ${!replace1 ? ` |> ${line}\n` : ''} |> angledLine([-65, ${
|
|||||||
const code = makeCode(line)
|
const code = makeCode(line)
|
||||||
const ast = parse(code)
|
const ast = parse(code)
|
||||||
if (err(ast)) throw ast
|
if (err(ast)) throw ast
|
||||||
const programMemory = await enginelessExecutor(ast)
|
const execState = await enginelessExecutor(ast)
|
||||||
const lineOfInterest = line
|
const lineOfInterest = line
|
||||||
const range: [number, number] = [
|
const range: [number, number] = [
|
||||||
code.indexOf(lineOfInterest),
|
code.indexOf(lineOfInterest),
|
||||||
@ -554,7 +554,7 @@ ${!replace1 ? ` |> ${line}\n` : ''} |> angledLine([-65, ${
|
|||||||
const modifiedAst = deleteSegmentFromPipeExpression(
|
const modifiedAst = deleteSegmentFromPipeExpression(
|
||||||
dependentSegments,
|
dependentSegments,
|
||||||
ast,
|
ast,
|
||||||
programMemory,
|
execState.memory,
|
||||||
code,
|
code,
|
||||||
pathToNode
|
pathToNode
|
||||||
)
|
)
|
||||||
@ -632,7 +632,7 @@ describe('Testing removeSingleConstraintInfo', () => {
|
|||||||
const ast = parse(code)
|
const ast = parse(code)
|
||||||
if (err(ast)) throw ast
|
if (err(ast)) throw ast
|
||||||
|
|
||||||
const programMemory = await enginelessExecutor(ast)
|
const execState = await enginelessExecutor(ast)
|
||||||
const lineOfInterest = expectedFinish.split('(')[0] + '('
|
const lineOfInterest = expectedFinish.split('(')[0] + '('
|
||||||
const range: [number, number] = [
|
const range: [number, number] = [
|
||||||
code.indexOf(lineOfInterest) + 1,
|
code.indexOf(lineOfInterest) + 1,
|
||||||
@ -661,7 +661,7 @@ describe('Testing removeSingleConstraintInfo', () => {
|
|||||||
pathToNode,
|
pathToNode,
|
||||||
argPosition,
|
argPosition,
|
||||||
ast,
|
ast,
|
||||||
programMemory
|
execState.memory
|
||||||
)
|
)
|
||||||
if (!mod) return new Error('mod is undefined')
|
if (!mod) return new Error('mod is undefined')
|
||||||
const recastCode = recast(mod.modifiedAst)
|
const recastCode = recast(mod.modifiedAst)
|
||||||
@ -686,7 +686,7 @@ describe('Testing removeSingleConstraintInfo', () => {
|
|||||||
const ast = parse(code)
|
const ast = parse(code)
|
||||||
if (err(ast)) throw ast
|
if (err(ast)) throw ast
|
||||||
|
|
||||||
const programMemory = await enginelessExecutor(ast)
|
const execState = await enginelessExecutor(ast)
|
||||||
const lineOfInterest = expectedFinish.split('(')[0] + '('
|
const lineOfInterest = expectedFinish.split('(')[0] + '('
|
||||||
const range: [number, number] = [
|
const range: [number, number] = [
|
||||||
code.indexOf(lineOfInterest) + 1,
|
code.indexOf(lineOfInterest) + 1,
|
||||||
@ -711,7 +711,7 @@ describe('Testing removeSingleConstraintInfo', () => {
|
|||||||
pathToNode,
|
pathToNode,
|
||||||
argPosition,
|
argPosition,
|
||||||
ast,
|
ast,
|
||||||
programMemory
|
execState.memory
|
||||||
)
|
)
|
||||||
if (!mod) return new Error('mod is undefined')
|
if (!mod) return new Error('mod is undefined')
|
||||||
const recastCode = recast(mod.modifiedAst)
|
const recastCode = recast(mod.modifiedAst)
|
||||||
@ -882,7 +882,7 @@ sketch002 = startSketchOn({
|
|||||||
// const lineOfInterest = 'line([-2.94, 2.7], %)'
|
// const lineOfInterest = 'line([-2.94, 2.7], %)'
|
||||||
const ast = parse(codeBefore)
|
const ast = parse(codeBefore)
|
||||||
if (err(ast)) throw ast
|
if (err(ast)) throw ast
|
||||||
const programMemory = await enginelessExecutor(ast)
|
const execState = await enginelessExecutor(ast)
|
||||||
|
|
||||||
// deleteFromSelection
|
// deleteFromSelection
|
||||||
const range: [number, number] = [
|
const range: [number, number] = [
|
||||||
@ -895,7 +895,7 @@ sketch002 = startSketchOn({
|
|||||||
range,
|
range,
|
||||||
type,
|
type,
|
||||||
},
|
},
|
||||||
programMemory,
|
execState.memory,
|
||||||
async () => {
|
async () => {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||||
return {
|
return {
|
||||||
|
@ -501,6 +501,7 @@ export function sketchOnExtrudedFace(
|
|||||||
createIdentifier(extrudeName ? extrudeName : oldSketchName),
|
createIdentifier(extrudeName ? extrudeName : oldSketchName),
|
||||||
_tag,
|
_tag,
|
||||||
]),
|
]),
|
||||||
|
undefined,
|
||||||
'const'
|
'const'
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -682,6 +683,7 @@ export function createPipeExpression(
|
|||||||
export function createVariableDeclaration(
|
export function createVariableDeclaration(
|
||||||
varName: string,
|
varName: string,
|
||||||
init: VariableDeclarator['init'],
|
init: VariableDeclarator['init'],
|
||||||
|
visibility: VariableDeclaration['visibility'] = 'default',
|
||||||
kind: VariableDeclaration['kind'] = 'const'
|
kind: VariableDeclaration['kind'] = 'const'
|
||||||
): VariableDeclaration {
|
): VariableDeclaration {
|
||||||
return {
|
return {
|
||||||
@ -699,6 +701,7 @@ export function createVariableDeclaration(
|
|||||||
init,
|
init,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
visibility,
|
||||||
kind,
|
kind,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -620,7 +620,7 @@ describe('Testing button states', () => {
|
|||||||
it('should return true when body exists and segment is selected', async () => {
|
it('should return true when body exists and segment is selected', async () => {
|
||||||
await runButtonStateTest(codeWithBody, `line([10, 0], %)`, true)
|
await runButtonStateTest(codeWithBody, `line([10, 0], %)`, true)
|
||||||
})
|
})
|
||||||
it('hould return false when body exists and not a segment is selected', async () => {
|
it('should return false when body exists and not a segment is selected', async () => {
|
||||||
await runButtonStateTest(codeWithBody, `close(%)`, false)
|
await runButtonStateTest(codeWithBody, `close(%)`, false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
CallExpression,
|
CallExpression,
|
||||||
|
Expr,
|
||||||
|
Identifier,
|
||||||
ObjectExpression,
|
ObjectExpression,
|
||||||
PathToNode,
|
PathToNode,
|
||||||
Program,
|
Program,
|
||||||
@ -27,7 +29,7 @@ import {
|
|||||||
sketchLineHelperMap,
|
sketchLineHelperMap,
|
||||||
} from '../std/sketch'
|
} from '../std/sketch'
|
||||||
import { err, trap } from 'lib/trap'
|
import { err, trap } from 'lib/trap'
|
||||||
import { Selections, canFilletSelection } from 'lib/selections'
|
import { Selections } from 'lib/selections'
|
||||||
import { KclCommandValue } from 'lib/commandTypes'
|
import { KclCommandValue } from 'lib/commandTypes'
|
||||||
import {
|
import {
|
||||||
ArtifactGraph,
|
ArtifactGraph,
|
||||||
@ -66,7 +68,10 @@ export function modifyAstCloneWithFilletAndTag(
|
|||||||
const artifactGraph = engineCommandManager.artifactGraph
|
const artifactGraph = engineCommandManager.artifactGraph
|
||||||
|
|
||||||
// Step 1: modify ast with tags and group them by extrude nodes (bodies)
|
// Step 1: modify ast with tags and group them by extrude nodes (bodies)
|
||||||
const extrudeToTagsMap: Map<PathToNode, string[]> = new Map()
|
const extrudeToTagsMap: Map<
|
||||||
|
PathToNode,
|
||||||
|
Array<{ tag: string; selectionType: string }>
|
||||||
|
> = new Map()
|
||||||
const lookupMap: Map<string, PathToNode> = new Map() // work around for Map key comparison
|
const lookupMap: Map<string, PathToNode> = new Map() // work around for Map key comparison
|
||||||
|
|
||||||
for (const selectionRange of selection.codeBasedSelections) {
|
for (const selectionRange of selection.codeBasedSelections) {
|
||||||
@ -74,6 +79,7 @@ export function modifyAstCloneWithFilletAndTag(
|
|||||||
codeBasedSelections: [selectionRange],
|
codeBasedSelections: [selectionRange],
|
||||||
otherSelections: [],
|
otherSelections: [],
|
||||||
}
|
}
|
||||||
|
const selectionType = singleSelection.codeBasedSelections[0].type
|
||||||
|
|
||||||
const result = getPathToExtrudeForSegmentSelection(
|
const result = getPathToExtrudeForSegmentSelection(
|
||||||
clonedAstForGetExtrude,
|
clonedAstForGetExtrude,
|
||||||
@ -89,6 +95,7 @@ export function modifyAstCloneWithFilletAndTag(
|
|||||||
)
|
)
|
||||||
if (err(tagResult)) return tagResult
|
if (err(tagResult)) return tagResult
|
||||||
const { tag } = tagResult
|
const { tag } = tagResult
|
||||||
|
const tagInfo = { tag, selectionType }
|
||||||
|
|
||||||
// Group tags by their corresponding extrude node
|
// Group tags by their corresponding extrude node
|
||||||
const extrudeKey = JSON.stringify(pathToExtrudeNode)
|
const extrudeKey = JSON.stringify(pathToExtrudeNode)
|
||||||
@ -96,23 +103,29 @@ export function modifyAstCloneWithFilletAndTag(
|
|||||||
if (lookupMap.has(extrudeKey)) {
|
if (lookupMap.has(extrudeKey)) {
|
||||||
const existingPath = lookupMap.get(extrudeKey)
|
const existingPath = lookupMap.get(extrudeKey)
|
||||||
if (!existingPath) return new Error('Path to extrude node not found.')
|
if (!existingPath) return new Error('Path to extrude node not found.')
|
||||||
extrudeToTagsMap.get(existingPath)?.push(tag)
|
extrudeToTagsMap.get(existingPath)?.push(tagInfo)
|
||||||
} else {
|
} else {
|
||||||
lookupMap.set(extrudeKey, pathToExtrudeNode)
|
lookupMap.set(extrudeKey, pathToExtrudeNode)
|
||||||
extrudeToTagsMap.set(pathToExtrudeNode, [tag])
|
extrudeToTagsMap.set(pathToExtrudeNode, [tagInfo])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 2: Apply fillet(s) for each extrude node (body)
|
// Step 2: Apply fillet(s) for each extrude node (body)
|
||||||
let pathToFilletNodes: Array<PathToNode> = []
|
let pathToFilletNodes: Array<PathToNode> = []
|
||||||
for (const [pathToExtrudeNode, tags] of extrudeToTagsMap.entries()) {
|
for (const [pathToExtrudeNode, tagInfos] of extrudeToTagsMap.entries()) {
|
||||||
// Create a fillet expression with multiple tags
|
// Create a fillet expression with multiple tags
|
||||||
const radiusValue =
|
const radiusValue =
|
||||||
'variableName' in radius ? radius.variableIdentifierAst : radius.valueAst
|
'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', [
|
const filletCall = createCallExpressionStdLib('fillet', [
|
||||||
createObjectExpression({
|
createObjectExpression({
|
||||||
radius: radiusValue,
|
radius: radiusValue,
|
||||||
tags: createArrayExpression(tags.map((tag) => createIdentifier(tag))),
|
tags: createArrayExpression(tagCalls),
|
||||||
}),
|
}),
|
||||||
createPipeSubstitution(),
|
createPipeSubstitution(),
|
||||||
])
|
])
|
||||||
@ -144,7 +157,7 @@ export function modifyAstCloneWithFilletAndTag(
|
|||||||
pathToFilletNode = getPathToNodeOfFilletLiteral(
|
pathToFilletNode = getPathToNodeOfFilletLiteral(
|
||||||
pathToExtrudeNode,
|
pathToExtrudeNode,
|
||||||
extrudeDeclarator,
|
extrudeDeclarator,
|
||||||
tags[0]
|
firstTag
|
||||||
)
|
)
|
||||||
pathToFilletNodes.push(pathToFilletNode)
|
pathToFilletNodes.push(pathToFilletNode)
|
||||||
} else if (extrudeDeclarator.init.type === 'PipeExpression') {
|
} else if (extrudeDeclarator.init.type === 'PipeExpression') {
|
||||||
@ -165,7 +178,7 @@ export function modifyAstCloneWithFilletAndTag(
|
|||||||
pathToFilletNode = getPathToNodeOfFilletLiteral(
|
pathToFilletNode = getPathToNodeOfFilletLiteral(
|
||||||
pathToExtrudeNode,
|
pathToExtrudeNode,
|
||||||
extrudeDeclarator,
|
extrudeDeclarator,
|
||||||
tags[0]
|
firstTag
|
||||||
)
|
)
|
||||||
pathToFilletNodes.push(pathToFilletNode)
|
pathToFilletNodes.push(pathToFilletNode)
|
||||||
} else {
|
} else {
|
||||||
@ -276,6 +289,21 @@ function mutateAstWithTagForSketchSegment(
|
|||||||
return { modifiedAst: astClone, tag }
|
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(
|
function locateExtrudeDeclarator(
|
||||||
node: Program,
|
node: Program,
|
||||||
pathToExtrudeNode: PathToNode
|
pathToExtrudeNode: PathToNode
|
||||||
@ -311,7 +339,7 @@ function locateExtrudeDeclarator(
|
|||||||
function getPathToNodeOfFilletLiteral(
|
function getPathToNodeOfFilletLiteral(
|
||||||
pathToExtrudeNode: PathToNode,
|
pathToExtrudeNode: PathToNode,
|
||||||
extrudeDeclarator: VariableDeclarator,
|
extrudeDeclarator: VariableDeclarator,
|
||||||
tag: string
|
tag: Identifier | CallExpression
|
||||||
): PathToNode {
|
): PathToNode {
|
||||||
let pathToFilletObj: PathToNode = []
|
let pathToFilletObj: PathToNode = []
|
||||||
let inFillet = false
|
let inFillet = false
|
||||||
@ -347,13 +375,31 @@ function getPathToNodeOfFilletLiteral(
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasTag(node: ObjectExpression, tag: string): boolean {
|
function hasTag(
|
||||||
|
node: ObjectExpression,
|
||||||
|
tag: Identifier | CallExpression
|
||||||
|
): boolean {
|
||||||
return node.properties.some((prop) => {
|
return node.properties.some((prop) => {
|
||||||
if (prop.key.name === 'tags' && prop.value.type === 'ArrayExpression') {
|
if (prop.key.name === 'tags' && prop.value.type === 'ArrayExpression') {
|
||||||
|
// if selection is a base edge:
|
||||||
|
if (tag.type === 'Identifier') {
|
||||||
return prop.value.elements.some(
|
return prop.value.elements.some(
|
||||||
(element) => element.type === 'Identifier' && element.name === tag
|
(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 false
|
return false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -383,7 +429,7 @@ export const hasValidFilletSelection = ({
|
|||||||
ast: Program
|
ast: Program
|
||||||
code: string
|
code: string
|
||||||
}) => {
|
}) => {
|
||||||
// case 0: check if there is anything filletable in the scene
|
// check if there is anything filletable in the scene
|
||||||
let extrudeExists = false
|
let extrudeExists = false
|
||||||
traverse(ast, {
|
traverse(ast, {
|
||||||
enter(node) {
|
enter(node) {
|
||||||
@ -394,65 +440,88 @@ export const hasValidFilletSelection = ({
|
|||||||
})
|
})
|
||||||
if (!extrudeExists) return false
|
if (!extrudeExists) return false
|
||||||
|
|
||||||
// case 1: nothing selected, test whether the extrusion exists
|
// check if nothing is selected
|
||||||
if (selectionRanges) {
|
|
||||||
if (selectionRanges.codeBasedSelections.length === 0) {
|
if (selectionRanges.codeBasedSelections.length === 0) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
const range0 = selectionRanges.codeBasedSelections[0].range[0]
|
|
||||||
const codeLength = code.length
|
// check if selection is last string in code
|
||||||
if (range0 === codeLength) {
|
if (selectionRanges.codeBasedSelections[0].range[0] === code.length) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// case 2: sketch segment selected, test whether it is extruded
|
// selection exists:
|
||||||
// TODO: add loft / sweep check
|
for (const selection of selectionRanges.codeBasedSelections) {
|
||||||
if (selectionRanges.codeBasedSelections.length > 0) {
|
// check if all selections are in sketchLineHelperMap
|
||||||
const isExtruded = hasSketchPipeBeenExtruded(
|
const path = getNodePathFromSourceRange(ast, selection.range)
|
||||||
selectionRanges.codeBasedSelections[0],
|
|
||||||
ast
|
|
||||||
)
|
|
||||||
if (isExtruded) {
|
|
||||||
const pathToSelectedNode = getNodePathFromSourceRange(
|
|
||||||
ast,
|
|
||||||
selectionRanges.codeBasedSelections[0].range
|
|
||||||
)
|
|
||||||
const segmentNode = getNodeFromPath<CallExpression>(
|
const segmentNode = getNodeFromPath<CallExpression>(
|
||||||
ast,
|
ast,
|
||||||
pathToSelectedNode,
|
path,
|
||||||
'CallExpression'
|
'CallExpression'
|
||||||
)
|
)
|
||||||
if (err(segmentNode)) return false
|
if (err(segmentNode)) return false
|
||||||
if (segmentNode.node.type === 'CallExpression') {
|
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
|
return false
|
||||||
}
|
}
|
||||||
}
|
if (!(segmentNode.node.callee.name in sketchLineHelperMap)) {
|
||||||
} else {
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return canFilletSelection(selectionRanges)
|
// 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
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
type EdgeTypes =
|
type EdgeTypes =
|
||||||
|
@ -45,11 +45,11 @@ variableBelowShouldNotBeIncluded = 3
|
|||||||
const rangeStart = code.indexOf('// selection-range-7ish-before-this') - 7
|
const rangeStart = code.indexOf('// selection-range-7ish-before-this') - 7
|
||||||
const ast = parse(code)
|
const ast = parse(code)
|
||||||
if (err(ast)) throw ast
|
if (err(ast)) throw ast
|
||||||
const programMemory = await enginelessExecutor(ast)
|
const execState = await enginelessExecutor(ast)
|
||||||
|
|
||||||
const { variables, bodyPath, insertIndex } = findAllPreviousVariables(
|
const { variables, bodyPath, insertIndex } = findAllPreviousVariables(
|
||||||
ast,
|
ast,
|
||||||
programMemory,
|
execState.memory,
|
||||||
[rangeStart, rangeStart]
|
[rangeStart, rangeStart]
|
||||||
)
|
)
|
||||||
expect(variables).toEqual([
|
expect(variables).toEqual([
|
||||||
@ -351,11 +351,11 @@ part001 = startSketchAt([-1.41, 3.46])
|
|||||||
const ast = parse(exampleCode)
|
const ast = parse(exampleCode)
|
||||||
if (err(ast)) throw ast
|
if (err(ast)) throw ast
|
||||||
|
|
||||||
const programMemory = await enginelessExecutor(ast)
|
const execState = await enginelessExecutor(ast)
|
||||||
const result = hasExtrudeSketch({
|
const result = hasExtrudeSketch({
|
||||||
ast,
|
ast,
|
||||||
selection: { type: 'default', range: [100, 101] },
|
selection: { type: 'default', range: [100, 101] },
|
||||||
programMemory,
|
programMemory: execState.memory,
|
||||||
})
|
})
|
||||||
expect(result).toEqual(true)
|
expect(result).toEqual(true)
|
||||||
})
|
})
|
||||||
@ -370,11 +370,11 @@ part001 = startSketchAt([-1.41, 3.46])
|
|||||||
const ast = parse(exampleCode)
|
const ast = parse(exampleCode)
|
||||||
if (err(ast)) throw ast
|
if (err(ast)) throw ast
|
||||||
|
|
||||||
const programMemory = await enginelessExecutor(ast)
|
const execState = await enginelessExecutor(ast)
|
||||||
const result = hasExtrudeSketch({
|
const result = hasExtrudeSketch({
|
||||||
ast,
|
ast,
|
||||||
selection: { type: 'default', range: [100, 101] },
|
selection: { type: 'default', range: [100, 101] },
|
||||||
programMemory,
|
programMemory: execState.memory,
|
||||||
})
|
})
|
||||||
expect(result).toEqual(true)
|
expect(result).toEqual(true)
|
||||||
})
|
})
|
||||||
@ -383,11 +383,11 @@ part001 = startSketchAt([-1.41, 3.46])
|
|||||||
const ast = parse(exampleCode)
|
const ast = parse(exampleCode)
|
||||||
if (err(ast)) throw ast
|
if (err(ast)) throw ast
|
||||||
|
|
||||||
const programMemory = await enginelessExecutor(ast)
|
const execState = await enginelessExecutor(ast)
|
||||||
const result = hasExtrudeSketch({
|
const result = hasExtrudeSketch({
|
||||||
ast,
|
ast,
|
||||||
selection: { type: 'default', range: [10, 11] },
|
selection: { type: 'default', range: [10, 11] },
|
||||||
programMemory,
|
programMemory: execState.memory,
|
||||||
})
|
})
|
||||||
expect(result).toEqual(false)
|
expect(result).toEqual(false)
|
||||||
})
|
})
|
||||||
|
@ -28,6 +28,7 @@ import {
|
|||||||
getConstraintType,
|
getConstraintType,
|
||||||
} from './std/sketchcombos'
|
} from './std/sketchcombos'
|
||||||
import { err } from 'lib/trap'
|
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.
|
* Retrieves a node from a given path within a Program node structure, optionally stopping at a specified node type.
|
||||||
@ -120,7 +121,12 @@ export function getNodeFromPathCurry(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function moreNodePathFromSourceRange(
|
function moreNodePathFromSourceRange(
|
||||||
node: Expr | ExpressionStatement | VariableDeclaration | ReturnStatement,
|
node:
|
||||||
|
| Expr
|
||||||
|
| ImportStatement
|
||||||
|
| ExpressionStatement
|
||||||
|
| VariableDeclaration
|
||||||
|
| ReturnStatement,
|
||||||
sourceRange: Selection['range'],
|
sourceRange: Selection['range'],
|
||||||
previousPath: PathToNode = [['body', '']]
|
previousPath: PathToNode = [['body', '']]
|
||||||
): PathToNode {
|
): PathToNode {
|
||||||
|
Before Width: | Height: | Size: 357 KiB After Width: | Height: | Size: 378 KiB |
Before Width: | Height: | Size: 577 KiB After Width: | Height: | Size: 613 KiB |
@ -117,11 +117,11 @@ describe('testing changeSketchArguments', () => {
|
|||||||
const ast = parse(code)
|
const ast = parse(code)
|
||||||
if (err(ast)) return ast
|
if (err(ast)) return ast
|
||||||
|
|
||||||
const programMemory = await enginelessExecutor(ast)
|
const execState = await enginelessExecutor(ast)
|
||||||
const sourceStart = code.indexOf(lineToChange)
|
const sourceStart = code.indexOf(lineToChange)
|
||||||
const changeSketchArgsRetVal = changeSketchArguments(
|
const changeSketchArgsRetVal = changeSketchArguments(
|
||||||
ast,
|
ast,
|
||||||
programMemory,
|
execState.memory,
|
||||||
{
|
{
|
||||||
type: 'sourceRange',
|
type: 'sourceRange',
|
||||||
sourceRange: [sourceStart, sourceStart + lineToChange.length],
|
sourceRange: [sourceStart, sourceStart + lineToChange.length],
|
||||||
@ -150,12 +150,12 @@ mySketch001 = startSketchOn('XY')
|
|||||||
const ast = parse(code)
|
const ast = parse(code)
|
||||||
if (err(ast)) return ast
|
if (err(ast)) return ast
|
||||||
|
|
||||||
const programMemory = await enginelessExecutor(ast)
|
const execState = await enginelessExecutor(ast)
|
||||||
const sourceStart = code.indexOf(lineToChange)
|
const sourceStart = code.indexOf(lineToChange)
|
||||||
expect(sourceStart).toBe(89)
|
expect(sourceStart).toBe(89)
|
||||||
const newSketchLnRetVal = addNewSketchLn({
|
const newSketchLnRetVal = addNewSketchLn({
|
||||||
node: ast,
|
node: ast,
|
||||||
programMemory,
|
programMemory: execState.memory,
|
||||||
input: {
|
input: {
|
||||||
type: 'straight-segment',
|
type: 'straight-segment',
|
||||||
from: [0, 0],
|
from: [0, 0],
|
||||||
@ -186,7 +186,7 @@ mySketch001 = startSketchOn('XY')
|
|||||||
|
|
||||||
const modifiedAst2 = addCloseToPipe({
|
const modifiedAst2 = addCloseToPipe({
|
||||||
node: ast,
|
node: ast,
|
||||||
programMemory,
|
programMemory: execState.memory,
|
||||||
pathToNode: [
|
pathToNode: [
|
||||||
['body', ''],
|
['body', ''],
|
||||||
[0, 'index'],
|
[0, 'index'],
|
||||||
@ -230,7 +230,7 @@ describe('testing addTagForSketchOnFace', () => {
|
|||||||
const pathToNode = getNodePathFromSourceRange(ast, sourceRange)
|
const pathToNode = getNodePathFromSourceRange(ast, sourceRange)
|
||||||
const sketchOnFaceRetVal = addTagForSketchOnFace(
|
const sketchOnFaceRetVal = addTagForSketchOnFace(
|
||||||
{
|
{
|
||||||
// previousProgramMemory: programMemory, // redundant?
|
// previousProgramMemory: execState.memory, // redundant?
|
||||||
pathToNode,
|
pathToNode,
|
||||||
node: ast,
|
node: ast,
|
||||||
},
|
},
|
||||||
|
@ -34,7 +34,7 @@ async function testingSwapSketchFnCall({
|
|||||||
const ast = parse(inputCode)
|
const ast = parse(inputCode)
|
||||||
if (err(ast)) return Promise.reject(ast)
|
if (err(ast)) return Promise.reject(ast)
|
||||||
|
|
||||||
const programMemory = await enginelessExecutor(ast)
|
const execState = await enginelessExecutor(ast)
|
||||||
const selections = {
|
const selections = {
|
||||||
codeBasedSelections: [range],
|
codeBasedSelections: [range],
|
||||||
otherSelections: [],
|
otherSelections: [],
|
||||||
@ -45,7 +45,7 @@ async function testingSwapSketchFnCall({
|
|||||||
return Promise.reject(new Error('transformInfos undefined'))
|
return Promise.reject(new Error('transformInfos undefined'))
|
||||||
const ast2 = transformAstSketchLines({
|
const ast2 = transformAstSketchLines({
|
||||||
ast,
|
ast,
|
||||||
programMemory,
|
programMemory: execState.memory,
|
||||||
selectionRanges: selections,
|
selectionRanges: selections,
|
||||||
transformInfos,
|
transformInfos,
|
||||||
referenceSegName: '',
|
referenceSegName: '',
|
||||||
@ -360,10 +360,10 @@ part001 = startSketchOn('XY')
|
|||||||
|> line([2.14, 1.35], %) // normal-segment
|
|> line([2.14, 1.35], %) // normal-segment
|
||||||
|> xLine(3.54, %)`
|
|> xLine(3.54, %)`
|
||||||
it('normal case works', async () => {
|
it('normal case works', async () => {
|
||||||
const programMemory = await enginelessExecutor(parse(code))
|
const execState = await enginelessExecutor(parse(code))
|
||||||
const index = code.indexOf('// normal-segment') - 7
|
const index = code.indexOf('// normal-segment') - 7
|
||||||
const sg = sketchFromKclValue(
|
const sg = sketchFromKclValue(
|
||||||
programMemory.get('part001'),
|
execState.memory.get('part001'),
|
||||||
'part001'
|
'part001'
|
||||||
) as Sketch
|
) as Sketch
|
||||||
const _segment = getSketchSegmentFromSourceRange(sg, [index, index])
|
const _segment = getSketchSegmentFromSourceRange(sg, [index, index])
|
||||||
@ -377,10 +377,10 @@ part001 = startSketchOn('XY')
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
it('verify it works when the segment is in the `start` property', async () => {
|
it('verify it works when the segment is in the `start` property', async () => {
|
||||||
const programMemory = await enginelessExecutor(parse(code))
|
const execState = await enginelessExecutor(parse(code))
|
||||||
const index = code.indexOf('// segment-in-start') - 7
|
const index = code.indexOf('// segment-in-start') - 7
|
||||||
const _segment = getSketchSegmentFromSourceRange(
|
const _segment = getSketchSegmentFromSourceRange(
|
||||||
sketchFromKclValue(programMemory.get('part001'), 'part001') as Sketch,
|
sketchFromKclValue(execState.memory.get('part001'), 'part001') as Sketch,
|
||||||
[index, index]
|
[index, index]
|
||||||
)
|
)
|
||||||
if (err(_segment)) throw _segment
|
if (err(_segment)) throw _segment
|
||||||
|
@ -9,7 +9,7 @@ import {
|
|||||||
getConstraintLevelFromSourceRange,
|
getConstraintLevelFromSourceRange,
|
||||||
} from './sketchcombos'
|
} from './sketchcombos'
|
||||||
import { ToolTip } from 'lang/langHelpers'
|
import { ToolTip } from 'lang/langHelpers'
|
||||||
import { Selections } from 'lib/selections'
|
import { Selection, Selections } from 'lib/selections'
|
||||||
import { err } from 'lib/trap'
|
import { err } from 'lib/trap'
|
||||||
import { enginelessExecutor } from '../../lib/testHelpers'
|
import { enginelessExecutor } from '../../lib/testHelpers'
|
||||||
|
|
||||||
@ -96,6 +96,86 @@ function makeSelections(
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('testing transformAstForSketchLines for equal length constraint', () => {
|
describe('testing transformAstForSketchLines for equal length constraint', () => {
|
||||||
|
describe(`should always reorder selections to have the base selection first`, () => {
|
||||||
|
const inputScript = `sketch001 = startSketchOn('XZ')
|
||||||
|
|> startProfileAt([0, 0], %)
|
||||||
|
|> line([5, 5], %)
|
||||||
|
|> line([-2, 5], %)
|
||||||
|
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||||
|
|> close(%)`
|
||||||
|
|
||||||
|
const expectedModifiedScript = `sketch001 = startSketchOn('XZ')
|
||||||
|
|> startProfileAt([0, 0], %)
|
||||||
|
|> line([5, 5], %, $seg01)
|
||||||
|
|> angledLine([112, segLen(seg01)], %)
|
||||||
|
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||||
|
|> close(%)
|
||||||
|
`
|
||||||
|
|
||||||
|
const selectLine = (script: string, lineNumber: number): Selection => {
|
||||||
|
const lines = script.split('\n')
|
||||||
|
const codeBeforeLine = lines.slice(0, lineNumber).join('\n').length
|
||||||
|
const line = lines.find((_, i) => i === lineNumber)
|
||||||
|
if (!line) {
|
||||||
|
throw new Error(
|
||||||
|
`line index ${lineNumber} not found in test sample, friend`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const start = codeBeforeLine + line.indexOf('|> ' + 5)
|
||||||
|
const range: [number, number] = [start, start]
|
||||||
|
return {
|
||||||
|
type: 'default',
|
||||||
|
range,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyTransformation(
|
||||||
|
inputCode: string,
|
||||||
|
selectionRanges: Selections['codeBasedSelections']
|
||||||
|
) {
|
||||||
|
const ast = parse(inputCode)
|
||||||
|
if (err(ast)) return Promise.reject(ast)
|
||||||
|
const execState = await enginelessExecutor(ast)
|
||||||
|
const transformInfos = getTransformInfos(
|
||||||
|
makeSelections(selectionRanges.slice(1)),
|
||||||
|
ast,
|
||||||
|
'equalLength'
|
||||||
|
)
|
||||||
|
|
||||||
|
const transformedSelection = makeSelections(selectionRanges)
|
||||||
|
|
||||||
|
const newAst = transformSecondarySketchLinesTagFirst({
|
||||||
|
ast,
|
||||||
|
selectionRanges: transformedSelection,
|
||||||
|
transformInfos,
|
||||||
|
programMemory: execState.memory,
|
||||||
|
})
|
||||||
|
if (err(newAst)) return Promise.reject(newAst)
|
||||||
|
|
||||||
|
const newCode = recast(newAst.modifiedAst)
|
||||||
|
return newCode
|
||||||
|
}
|
||||||
|
|
||||||
|
it(`Should reorder when user selects first-to-last`, async () => {
|
||||||
|
const selectionRanges: Selections['codeBasedSelections'] = [
|
||||||
|
selectLine(inputScript, 3),
|
||||||
|
selectLine(inputScript, 4),
|
||||||
|
]
|
||||||
|
|
||||||
|
const newCode = await applyTransformation(inputScript, selectionRanges)
|
||||||
|
expect(newCode).toBe(expectedModifiedScript)
|
||||||
|
})
|
||||||
|
|
||||||
|
it(`Should reorder when user selects last-to-first`, async () => {
|
||||||
|
const selectionRanges: Selections['codeBasedSelections'] = [
|
||||||
|
selectLine(inputScript, 4),
|
||||||
|
selectLine(inputScript, 3),
|
||||||
|
]
|
||||||
|
|
||||||
|
const newCode = await applyTransformation(inputScript, selectionRanges)
|
||||||
|
expect(newCode).toBe(expectedModifiedScript)
|
||||||
|
})
|
||||||
|
})
|
||||||
const inputScript = `myVar = 3
|
const inputScript = `myVar = 3
|
||||||
myVar2 = 5
|
myVar2 = 5
|
||||||
myVar3 = 6
|
myVar3 = 6
|
||||||
@ -220,7 +300,7 @@ part001 = startSketchOn('XY')
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const programMemory = await enginelessExecutor(ast)
|
const execState = await enginelessExecutor(ast)
|
||||||
const transformInfos = getTransformInfos(
|
const transformInfos = getTransformInfos(
|
||||||
makeSelections(selectionRanges.slice(1)),
|
makeSelections(selectionRanges.slice(1)),
|
||||||
ast,
|
ast,
|
||||||
@ -231,7 +311,7 @@ part001 = startSketchOn('XY')
|
|||||||
ast,
|
ast,
|
||||||
selectionRanges: makeSelections(selectionRanges),
|
selectionRanges: makeSelections(selectionRanges),
|
||||||
transformInfos,
|
transformInfos,
|
||||||
programMemory,
|
programMemory: execState.memory,
|
||||||
})
|
})
|
||||||
if (err(newAst)) return Promise.reject(newAst)
|
if (err(newAst)) return Promise.reject(newAst)
|
||||||
|
|
||||||
@ -311,7 +391,7 @@ part001 = startSketchOn('XY')
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const programMemory = await enginelessExecutor(ast)
|
const execState = await enginelessExecutor(ast)
|
||||||
const transformInfos = getTransformInfos(
|
const transformInfos = getTransformInfos(
|
||||||
makeSelections(selectionRanges),
|
makeSelections(selectionRanges),
|
||||||
ast,
|
ast,
|
||||||
@ -322,7 +402,7 @@ part001 = startSketchOn('XY')
|
|||||||
ast,
|
ast,
|
||||||
selectionRanges: makeSelections(selectionRanges),
|
selectionRanges: makeSelections(selectionRanges),
|
||||||
transformInfos,
|
transformInfos,
|
||||||
programMemory,
|
programMemory: execState.memory,
|
||||||
referenceSegName: '',
|
referenceSegName: '',
|
||||||
})
|
})
|
||||||
if (err(newAst)) return Promise.reject(newAst)
|
if (err(newAst)) return Promise.reject(newAst)
|
||||||
@ -373,7 +453,7 @@ part001 = startSketchOn('XY')
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const programMemory = await enginelessExecutor(ast)
|
const execState = await enginelessExecutor(ast)
|
||||||
const transformInfos = getTransformInfos(
|
const transformInfos = getTransformInfos(
|
||||||
makeSelections(selectionRanges),
|
makeSelections(selectionRanges),
|
||||||
ast,
|
ast,
|
||||||
@ -384,7 +464,7 @@ part001 = startSketchOn('XY')
|
|||||||
ast,
|
ast,
|
||||||
selectionRanges: makeSelections(selectionRanges),
|
selectionRanges: makeSelections(selectionRanges),
|
||||||
transformInfos,
|
transformInfos,
|
||||||
programMemory,
|
programMemory: execState.memory,
|
||||||
referenceSegName: '',
|
referenceSegName: '',
|
||||||
})
|
})
|
||||||
if (err(newAst)) return Promise.reject(newAst)
|
if (err(newAst)) return Promise.reject(newAst)
|
||||||
@ -470,7 +550,7 @@ async function helperThing(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const programMemory = await enginelessExecutor(ast)
|
const execState = await enginelessExecutor(ast)
|
||||||
const transformInfos = getTransformInfos(
|
const transformInfos = getTransformInfos(
|
||||||
makeSelections(selectionRanges.slice(1)),
|
makeSelections(selectionRanges.slice(1)),
|
||||||
ast,
|
ast,
|
||||||
@ -481,7 +561,7 @@ async function helperThing(
|
|||||||
ast,
|
ast,
|
||||||
selectionRanges: makeSelections(selectionRanges),
|
selectionRanges: makeSelections(selectionRanges),
|
||||||
transformInfos,
|
transformInfos,
|
||||||
programMemory,
|
programMemory: execState.memory,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (err(newAst)) return Promise.reject(newAst)
|
if (err(newAst)) return Promise.reject(newAst)
|
||||||
|
@ -1559,7 +1559,15 @@ export function transformSecondarySketchLinesTagFirst({
|
|||||||
}
|
}
|
||||||
| Error {
|
| Error {
|
||||||
// let node = structuredClone(ast)
|
// let node = structuredClone(ast)
|
||||||
const primarySelection = selectionRanges.codeBasedSelections[0].range
|
|
||||||
|
// We need to sort the selections by their start position
|
||||||
|
// so that we can process them in dependency order and not write invalid KCL.
|
||||||
|
const sortedCodeBasedSelections =
|
||||||
|
selectionRanges.codeBasedSelections.toSorted(
|
||||||
|
(a, b) => a.range[0] - b.range[0]
|
||||||
|
)
|
||||||
|
const primarySelection = sortedCodeBasedSelections[0].range
|
||||||
|
const secondarySelections = sortedCodeBasedSelections.slice(1)
|
||||||
|
|
||||||
const _tag = giveSketchFnCallTag(ast, primarySelection, forceSegName)
|
const _tag = giveSketchFnCallTag(ast, primarySelection, forceSegName)
|
||||||
if (err(_tag)) return _tag
|
if (err(_tag)) return _tag
|
||||||
@ -1569,7 +1577,7 @@ export function transformSecondarySketchLinesTagFirst({
|
|||||||
ast: modifiedAst,
|
ast: modifiedAst,
|
||||||
selectionRanges: {
|
selectionRanges: {
|
||||||
...selectionRanges,
|
...selectionRanges,
|
||||||
codeBasedSelections: selectionRanges.codeBasedSelections.slice(1),
|
codeBasedSelections: secondarySelections,
|
||||||
},
|
},
|
||||||
referencedSegmentRange: primarySelection,
|
referencedSegmentRange: primarySelection,
|
||||||
transformInfos,
|
transformInfos,
|
||||||
|
@ -17,9 +17,9 @@ describe('testing angledLineThatIntersects', () => {
|
|||||||
offset: ${offset},
|
offset: ${offset},
|
||||||
}, %, $yo2)
|
}, %, $yo2)
|
||||||
intersect = segEndX(yo2)`
|
intersect = segEndX(yo2)`
|
||||||
const mem = await enginelessExecutor(parse(code('-1')))
|
const execState = await enginelessExecutor(parse(code('-1')))
|
||||||
expect(mem.get('intersect')?.value).toBe(1 + Math.sqrt(2))
|
expect(execState.memory.get('intersect')?.value).toBe(1 + Math.sqrt(2))
|
||||||
const noOffset = await enginelessExecutor(parse(code('0')))
|
const noOffset = await enginelessExecutor(parse(code('0')))
|
||||||
expect(noOffset.get('intersect')?.value).toBeCloseTo(1)
|
expect(noOffset.memory.get('intersect')?.value).toBeCloseTo(1)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -37,6 +37,11 @@ import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
|
|||||||
import { DeepPartial } from 'lib/types'
|
import { DeepPartial } from 'lib/types'
|
||||||
import { ProjectConfiguration } from 'wasm-lib/kcl/bindings/ProjectConfiguration'
|
import { ProjectConfiguration } from 'wasm-lib/kcl/bindings/ProjectConfiguration'
|
||||||
import { Sketch } from '../wasm-lib/kcl/bindings/Sketch'
|
import { Sketch } from '../wasm-lib/kcl/bindings/Sketch'
|
||||||
|
import { IdGenerator } from 'wasm-lib/kcl/bindings/IdGenerator'
|
||||||
|
import { ExecState as RawExecState } from '../wasm-lib/kcl/bindings/ExecState'
|
||||||
|
import { ProgramMemory as RawProgramMemory } from '../wasm-lib/kcl/bindings/ProgramMemory'
|
||||||
|
import { EnvironmentRef } from '../wasm-lib/kcl/bindings/EnvironmentRef'
|
||||||
|
import { Environment } from '../wasm-lib/kcl/bindings/Environment'
|
||||||
|
|
||||||
export type { Program } from '../wasm-lib/kcl/bindings/Program'
|
export type { Program } from '../wasm-lib/kcl/bindings/Program'
|
||||||
export type { Expr } from '../wasm-lib/kcl/bindings/Expr'
|
export type { Expr } from '../wasm-lib/kcl/bindings/Expr'
|
||||||
@ -136,29 +141,46 @@ export const parse = (code: string | Error): Program | Error => {
|
|||||||
|
|
||||||
export type PathToNode = [string | number, string][]
|
export type PathToNode = [string | number, string][]
|
||||||
|
|
||||||
interface Memory {
|
export interface ExecState {
|
||||||
[key: string]: KclValue
|
memory: ProgramMemory
|
||||||
|
idGenerator: IdGenerator
|
||||||
}
|
}
|
||||||
|
|
||||||
type EnvironmentRef = number
|
/**
|
||||||
|
* Create an empty ExecState. This is useful on init to prevent needing an
|
||||||
|
* Option.
|
||||||
|
*/
|
||||||
|
export function emptyExecState(): ExecState {
|
||||||
|
return {
|
||||||
|
memory: ProgramMemory.empty(),
|
||||||
|
idGenerator: defaultIdGenerator(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function execStateFromRaw(raw: RawExecState): ExecState {
|
||||||
|
return {
|
||||||
|
memory: ProgramMemory.fromRaw(raw.memory),
|
||||||
|
idGenerator: raw.idGenerator,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function defaultIdGenerator(): IdGenerator {
|
||||||
|
return {
|
||||||
|
nextId: 0,
|
||||||
|
ids: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Memory {
|
||||||
|
[key: string]: KclValue | undefined
|
||||||
|
}
|
||||||
|
|
||||||
const ROOT_ENVIRONMENT_REF: EnvironmentRef = 0
|
const ROOT_ENVIRONMENT_REF: EnvironmentRef = 0
|
||||||
|
|
||||||
interface Environment {
|
|
||||||
bindings: Memory
|
|
||||||
parent: EnvironmentRef | null
|
|
||||||
}
|
|
||||||
|
|
||||||
function emptyEnvironment(): Environment {
|
function emptyEnvironment(): Environment {
|
||||||
return { bindings: {}, parent: null }
|
return { bindings: {}, parent: null }
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RawProgramMemory {
|
|
||||||
environments: Environment[]
|
|
||||||
currentEnv: EnvironmentRef
|
|
||||||
return: KclValue | null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This duplicates logic in Rust. The hope is to keep ProgramMemory internals
|
* This duplicates logic in Rust. The hope is to keep ProgramMemory internals
|
||||||
* isolated from the rest of the TypeScript code so that we can move it to Rust
|
* isolated from the rest of the TypeScript code so that we can move it to Rust
|
||||||
@ -217,7 +239,7 @@ export class ProgramMemory {
|
|||||||
while (true) {
|
while (true) {
|
||||||
const env = this.environments[envRef]
|
const env = this.environments[envRef]
|
||||||
if (env.bindings.hasOwnProperty(name)) {
|
if (env.bindings.hasOwnProperty(name)) {
|
||||||
return env.bindings[name]
|
return env.bindings[name] ?? null
|
||||||
}
|
}
|
||||||
if (!env.parent) {
|
if (!env.parent) {
|
||||||
break
|
break
|
||||||
@ -260,6 +282,7 @@ export class ProgramMemory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const [name, value] of Object.entries(env.bindings)) {
|
for (const [name, value] of Object.entries(env.bindings)) {
|
||||||
|
if (value === undefined) continue
|
||||||
// Check the predicate.
|
// Check the predicate.
|
||||||
if (!predicate(value)) {
|
if (!predicate(value)) {
|
||||||
continue
|
continue
|
||||||
@ -293,6 +316,7 @@ export class ProgramMemory {
|
|||||||
while (true) {
|
while (true) {
|
||||||
const env = this.environments[envRef]
|
const env = this.environments[envRef]
|
||||||
for (const [name, value] of Object.entries(env.bindings)) {
|
for (const [name, value] of Object.entries(env.bindings)) {
|
||||||
|
if (value === undefined) continue
|
||||||
// Don't include shadowed variables.
|
// Don't include shadowed variables.
|
||||||
if (!map.has(name)) {
|
if (!map.has(name)) {
|
||||||
map.set(name, value)
|
map.set(name, value)
|
||||||
@ -356,9 +380,10 @@ export function sketchFromKclValue(
|
|||||||
export const executor = async (
|
export const executor = async (
|
||||||
node: Program,
|
node: Program,
|
||||||
programMemory: ProgramMemory | Error = ProgramMemory.empty(),
|
programMemory: ProgramMemory | Error = ProgramMemory.empty(),
|
||||||
|
idGenerator: IdGenerator = defaultIdGenerator(),
|
||||||
engineCommandManager: EngineCommandManager,
|
engineCommandManager: EngineCommandManager,
|
||||||
isMock: boolean = false
|
isMock: boolean = false
|
||||||
): Promise<ProgramMemory> => {
|
): Promise<ExecState> => {
|
||||||
if (err(programMemory)) return Promise.reject(programMemory)
|
if (err(programMemory)) return Promise.reject(programMemory)
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
@ -366,6 +391,7 @@ export const executor = async (
|
|||||||
const _programMemory = await _executor(
|
const _programMemory = await _executor(
|
||||||
node,
|
node,
|
||||||
programMemory,
|
programMemory,
|
||||||
|
idGenerator,
|
||||||
engineCommandManager,
|
engineCommandManager,
|
||||||
isMock
|
isMock
|
||||||
)
|
)
|
||||||
@ -378,9 +404,10 @@ export const executor = async (
|
|||||||
export const _executor = async (
|
export const _executor = async (
|
||||||
node: Program,
|
node: Program,
|
||||||
programMemory: ProgramMemory | Error = ProgramMemory.empty(),
|
programMemory: ProgramMemory | Error = ProgramMemory.empty(),
|
||||||
|
idGenerator: IdGenerator = defaultIdGenerator(),
|
||||||
engineCommandManager: EngineCommandManager,
|
engineCommandManager: EngineCommandManager,
|
||||||
isMock: boolean
|
isMock: boolean
|
||||||
): Promise<ProgramMemory> => {
|
): Promise<ExecState> => {
|
||||||
if (err(programMemory)) return Promise.reject(programMemory)
|
if (err(programMemory)) return Promise.reject(programMemory)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -392,15 +419,17 @@ export const _executor = async (
|
|||||||
baseUnit =
|
baseUnit =
|
||||||
(await getSettingsState)()?.modeling.defaultUnit.current || 'mm'
|
(await getSettingsState)()?.modeling.defaultUnit.current || 'mm'
|
||||||
}
|
}
|
||||||
const memory: RawProgramMemory = await execute_wasm(
|
const execState: RawExecState = await execute_wasm(
|
||||||
JSON.stringify(node),
|
JSON.stringify(node),
|
||||||
JSON.stringify(programMemory.toRaw()),
|
JSON.stringify(programMemory.toRaw()),
|
||||||
|
JSON.stringify(idGenerator),
|
||||||
baseUnit,
|
baseUnit,
|
||||||
engineCommandManager,
|
engineCommandManager,
|
||||||
fileSystemManager,
|
fileSystemManager,
|
||||||
|
undefined,
|
||||||
isMock
|
isMock
|
||||||
)
|
)
|
||||||
return ProgramMemory.fromRaw(memory)
|
return execStateFromRaw(execState)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.log(e)
|
console.log(e)
|
||||||
const parsed: RustKclError = JSON.parse(e.toString())
|
const parsed: RustKclError = JSON.parse(e.toString())
|
||||||
|
@ -281,6 +281,8 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
|||||||
multiple: true,
|
multiple: true,
|
||||||
required: true,
|
required: true,
|
||||||
skip: false,
|
skip: false,
|
||||||
|
warningMessage:
|
||||||
|
'Fillets cannot touch other fillets yet. This is under development.',
|
||||||
},
|
},
|
||||||
radius: {
|
radius: {
|
||||||
inputType: 'kcl',
|
inputType: 'kcl',
|
||||||
|
@ -113,6 +113,7 @@ export type CommandArgumentConfig<
|
|||||||
commandBarContext: { argumentsToSubmit: Record<string, unknown> }, // Should be the commandbarMachine's context, but it creates a circular dependency
|
commandBarContext: { argumentsToSubmit: Record<string, unknown> }, // Should be the commandbarMachine's context, but it creates a circular dependency
|
||||||
machineContext?: C
|
machineContext?: C
|
||||||
) => boolean)
|
) => boolean)
|
||||||
|
warningMessage?: string
|
||||||
skip?: boolean
|
skip?: boolean
|
||||||
/** For showing a summary display of the current value, such as in
|
/** For showing a summary display of the current value, such as in
|
||||||
* the command bar's header
|
* the command bar's header
|
||||||
@ -189,6 +190,7 @@ export type CommandArgument<
|
|||||||
) => boolean)
|
) => boolean)
|
||||||
skip?: boolean
|
skip?: boolean
|
||||||
machineActor?: Actor<T>
|
machineActor?: Actor<T>
|
||||||
|
warningMessage?: string
|
||||||
/** For showing a summary display of the current value, such as in
|
/** For showing a summary display of the current value, such as in
|
||||||
* the command bar's header
|
* the command bar's header
|
||||||
*/
|
*/
|
||||||
|
@ -102,3 +102,6 @@ export const KCL_SAMPLES_MANIFEST_URLS = {
|
|||||||
'https://raw.githubusercontent.com/KittyCAD/kcl-samples/main/manifest.json',
|
'https://raw.githubusercontent.com/KittyCAD/kcl-samples/main/manifest.json',
|
||||||
localFallback: '/kcl-samples-manifest-fallback.json',
|
localFallback: '/kcl-samples-manifest-fallback.json',
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
|
/** Toast id for the app auto-updater toast */
|
||||||
|
export const AUTO_UPDATER_TOAST_ID = 'auto-updater-toast'
|
||||||
|
@ -152,6 +152,7 @@ export function buildCommandArgument<
|
|||||||
skip: arg.skip,
|
skip: arg.skip,
|
||||||
machineActor,
|
machineActor,
|
||||||
valueSummary: arg.valueSummary,
|
valueSummary: arg.valueSummary,
|
||||||
|
warningMessage: arg.warningMessage ?? '',
|
||||||
} satisfies Omit<CommandArgument<O, T>, 'inputType'>
|
} satisfies Omit<CommandArgument<O, T>, 'inputType'>
|
||||||
|
|
||||||
if (arg.inputType === 'options') {
|
if (arg.inputType === 'options') {
|
||||||
|
@ -379,7 +379,7 @@ const getAppFolderName = () => {
|
|||||||
return window.electron.packageJson.name
|
return window.electron.packageJson.name
|
||||||
}
|
}
|
||||||
|
|
||||||
const getAppSettingsFilePath = async () => {
|
export const getAppSettingsFilePath = async () => {
|
||||||
const isTestEnv = window.electron.process.env.IS_PLAYWRIGHT === 'true'
|
const isTestEnv = window.electron.process.env.IS_PLAYWRIGHT === 'true'
|
||||||
const testSettingsPath = window.electron.process.env.TEST_SETTINGS_FILE_KEY
|
const testSettingsPath = window.electron.process.env.TEST_SETTINGS_FILE_KEY
|
||||||
const appConfig = await window.electron.getPath('appData')
|
const appConfig = await window.electron.getPath('appData')
|
||||||
|
52
src/lib/machine-api.d.ts
vendored
@ -55,6 +55,23 @@ export interface paths {
|
|||||||
patch?: never
|
patch?: never
|
||||||
trace?: never
|
trace?: never
|
||||||
}
|
}
|
||||||
|
'/metrics': {
|
||||||
|
parameters: {
|
||||||
|
query?: never
|
||||||
|
header?: never
|
||||||
|
path?: never
|
||||||
|
cookie?: never
|
||||||
|
}
|
||||||
|
/** List available machines and their statuses */
|
||||||
|
get: operations['get_metrics']
|
||||||
|
put?: never
|
||||||
|
post?: never
|
||||||
|
delete?: never
|
||||||
|
options?: never
|
||||||
|
head?: never
|
||||||
|
patch?: never
|
||||||
|
trace?: never
|
||||||
|
}
|
||||||
'/ping': {
|
'/ping': {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never
|
query?: never
|
||||||
@ -126,6 +143,8 @@ export interface components {
|
|||||||
*
|
*
|
||||||
* What "close" means is up to you! */
|
* What "close" means is up to you! */
|
||||||
max_part_volume?: components['schemas']['Volume'] | null
|
max_part_volume?: components['schemas']['Volume'] | null
|
||||||
|
/** @description Status of the printer -- be it printing, idle, or unreachable. This may dictate if a machine is capable of taking a new job. */
|
||||||
|
state: components['schemas']['MachineState']
|
||||||
}
|
}
|
||||||
/** @description Information regarding the make/model of a discovered endpoint. */
|
/** @description Information regarding the make/model of a discovered endpoint. */
|
||||||
MachineMakeModel: {
|
MachineMakeModel: {
|
||||||
@ -136,6 +155,17 @@ export interface components {
|
|||||||
/** @description The unique serial number of the connected Machine. */
|
/** @description The unique serial number of the connected Machine. */
|
||||||
serial?: string | null
|
serial?: string | null
|
||||||
}
|
}
|
||||||
|
/** @description Current state of the machine -- be it printing, idle or offline. This can be used to determine if a printer is in the correct state to take a new job. */
|
||||||
|
MachineState:
|
||||||
|
| 'Unknown'
|
||||||
|
| 'Idle'
|
||||||
|
| 'Running'
|
||||||
|
| 'Offline'
|
||||||
|
| 'Paused'
|
||||||
|
| 'Complete'
|
||||||
|
| {
|
||||||
|
Failed: string | null
|
||||||
|
}
|
||||||
/** @description Specific technique by which this Machine takes a design, and produces a real-world 3D object. */
|
/** @description Specific technique by which this Machine takes a design, and produces a real-world 3D object. */
|
||||||
MachineType: 'Stereolithography' | 'FusedDeposition' | 'Cnc'
|
MachineType: 'Stereolithography' | 'FusedDeposition' | 'Cnc'
|
||||||
/** @description The response from the `/ping` endpoint. */
|
/** @description The response from the `/ping` endpoint. */
|
||||||
@ -265,6 +295,28 @@ export interface operations {
|
|||||||
'5XX': components['responses']['Error']
|
'5XX': components['responses']['Error']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
get_metrics: {
|
||||||
|
parameters: {
|
||||||
|
query?: never
|
||||||
|
header?: never
|
||||||
|
path?: never
|
||||||
|
cookie?: never
|
||||||
|
}
|
||||||
|
requestBody?: never
|
||||||
|
responses: {
|
||||||
|
/** @description successful operation */
|
||||||
|
200: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown
|
||||||
|
}
|
||||||
|
content: {
|
||||||
|
'application/json': string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'4XX': components['responses']['Error']
|
||||||
|
'5XX': components['responses']['Error']
|
||||||
|
}
|
||||||
|
}
|
||||||
ping: {
|
ping: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never
|
query?: never
|
||||||
|
@ -145,6 +145,13 @@ export const interactionMap: Record<
|
|||||||
description:
|
description:
|
||||||
'Available while modeling with either a face selected or an empty selection, when not typing in the code editor.',
|
'Available while modeling with either a face selected or an empty selection, when not typing in the code editor.',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'center-on-selection',
|
||||||
|
sequence: `${PRIMARY}+Alt+C`,
|
||||||
|
title: 'Center on selection',
|
||||||
|
description:
|
||||||
|
'Centers the view on the selected geometry, or everything if nothing is selected.',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
'Code Editor': [
|
'Code Editor': [
|
||||||
{
|
{
|
||||||
|
@ -177,14 +177,14 @@ export async function loadAndValidateSettings(
|
|||||||
|
|
||||||
if (err(appSettingsPayload)) return Promise.reject(appSettingsPayload)
|
if (err(appSettingsPayload)) return Promise.reject(appSettingsPayload)
|
||||||
|
|
||||||
const settings = createSettings()
|
let settingsNext = createSettings()
|
||||||
// Because getting the default directory is async, we need to set it after
|
// Because getting the default directory is async, we need to set it after
|
||||||
if (onDesktop) {
|
if (onDesktop) {
|
||||||
settings.app.projectDirectory.default = await getInitialDefaultDir()
|
settings.app.projectDirectory.default = await getInitialDefaultDir()
|
||||||
}
|
}
|
||||||
|
|
||||||
setSettingsAtLevel(
|
settingsNext = setSettingsAtLevel(
|
||||||
settings,
|
settingsNext,
|
||||||
'user',
|
'user',
|
||||||
configurationToSettingsPayload(appSettingsPayload)
|
configurationToSettingsPayload(appSettingsPayload)
|
||||||
)
|
)
|
||||||
@ -199,8 +199,8 @@ export async function loadAndValidateSettings(
|
|||||||
return Promise.reject(new Error('Invalid project settings'))
|
return Promise.reject(new Error('Invalid project settings'))
|
||||||
|
|
||||||
const projectSettingsPayload = projectSettings
|
const projectSettingsPayload = projectSettings
|
||||||
setSettingsAtLevel(
|
settingsNext = setSettingsAtLevel(
|
||||||
settings,
|
settingsNext,
|
||||||
'project',
|
'project',
|
||||||
projectConfigurationToSettingsPayload(projectSettingsPayload)
|
projectConfigurationToSettingsPayload(projectSettingsPayload)
|
||||||
)
|
)
|
||||||
@ -208,7 +208,7 @@ export async function loadAndValidateSettings(
|
|||||||
|
|
||||||
// Return the settings object
|
// Return the settings object
|
||||||
return {
|
return {
|
||||||
settings,
|
settings: settingsNext,
|
||||||
configuration: appSettingsPayload,
|
configuration: appSettingsPayload,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -49,6 +49,7 @@ if (typeof window !== 'undefined') {
|
|||||||
type: 'zoom_to_fit',
|
type: 'zoom_to_fit',
|
||||||
object_ids: [], // leave empty to zoom to all objects
|
object_ids: [], // leave empty to zoom to all objects
|
||||||
padding: 0.2, // padding around the objects
|
padding: 0.2, // padding around the objects
|
||||||
|
animated: false, // don't animate the zoom for now
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,11 @@
|
|||||||
import { Program, ProgramMemory, _executor, SourceRange } from '../lang/wasm'
|
import {
|
||||||
|
Program,
|
||||||
|
ProgramMemory,
|
||||||
|
_executor,
|
||||||
|
SourceRange,
|
||||||
|
ExecState,
|
||||||
|
defaultIdGenerator,
|
||||||
|
} from '../lang/wasm'
|
||||||
import {
|
import {
|
||||||
EngineCommandManager,
|
EngineCommandManager,
|
||||||
EngineCommandManagerEvents,
|
EngineCommandManagerEvents,
|
||||||
@ -9,6 +16,7 @@ import { v4 as uuidv4 } from 'uuid'
|
|||||||
import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
|
import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
|
||||||
import { err, reportRejection } from 'lib/trap'
|
import { err, reportRejection } from 'lib/trap'
|
||||||
import { toSync } from './utils'
|
import { toSync } from './utils'
|
||||||
|
import { IdGenerator } from 'wasm-lib/kcl/bindings/IdGenerator'
|
||||||
|
|
||||||
type WebSocketResponse = Models['WebSocketResponse_type']
|
type WebSocketResponse = Models['WebSocketResponse_type']
|
||||||
|
|
||||||
@ -77,8 +85,9 @@ class MockEngineCommandManager {
|
|||||||
|
|
||||||
export async function enginelessExecutor(
|
export async function enginelessExecutor(
|
||||||
ast: Program | Error,
|
ast: Program | Error,
|
||||||
pm: ProgramMemory | Error = ProgramMemory.empty()
|
pm: ProgramMemory | Error = ProgramMemory.empty(),
|
||||||
): Promise<ProgramMemory> {
|
idGenerator: IdGenerator = defaultIdGenerator()
|
||||||
|
): Promise<ExecState> {
|
||||||
if (err(ast)) return Promise.reject(ast)
|
if (err(ast)) return Promise.reject(ast)
|
||||||
if (err(pm)) return Promise.reject(pm)
|
if (err(pm)) return Promise.reject(pm)
|
||||||
|
|
||||||
@ -88,15 +97,22 @@ export async function enginelessExecutor(
|
|||||||
}) as any as EngineCommandManager
|
}) as any as EngineCommandManager
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
mockEngineCommandManager.startNewSession()
|
mockEngineCommandManager.startNewSession()
|
||||||
const programMemory = await _executor(ast, pm, mockEngineCommandManager, true)
|
const execState = await _executor(
|
||||||
|
ast,
|
||||||
|
pm,
|
||||||
|
idGenerator,
|
||||||
|
mockEngineCommandManager,
|
||||||
|
true
|
||||||
|
)
|
||||||
await mockEngineCommandManager.waitForAllCommands()
|
await mockEngineCommandManager.waitForAllCommands()
|
||||||
return programMemory
|
return execState
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function executor(
|
export async function executor(
|
||||||
ast: Program,
|
ast: Program,
|
||||||
pm: ProgramMemory = ProgramMemory.empty()
|
pm: ProgramMemory = ProgramMemory.empty(),
|
||||||
): Promise<ProgramMemory> {
|
idGenerator: IdGenerator = defaultIdGenerator()
|
||||||
|
): Promise<ExecState> {
|
||||||
const engineCommandManager = new EngineCommandManager()
|
const engineCommandManager = new EngineCommandManager()
|
||||||
engineCommandManager.start({
|
engineCommandManager.start({
|
||||||
setIsStreamReady: () => {},
|
setIsStreamReady: () => {},
|
||||||
@ -117,14 +133,15 @@ export async function executor(
|
|||||||
toSync(async () => {
|
toSync(async () => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
engineCommandManager.startNewSession()
|
engineCommandManager.startNewSession()
|
||||||
const programMemory = await _executor(
|
const execState = await _executor(
|
||||||
ast,
|
ast,
|
||||||
pm,
|
pm,
|
||||||
|
idGenerator,
|
||||||
engineCommandManager,
|
engineCommandManager,
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
await engineCommandManager.waitForAllCommands()
|
await engineCommandManager.waitForAllCommands()
|
||||||
resolve(programMemory)
|
resolve(execState)
|
||||||
}, reportRejection)
|
}, reportRejection)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|