Compare commits
58 Commits
kurt-rever
...
kcl-test-s
Author | SHA1 | Date | |
---|---|---|---|
b6dd6e7dd0 | |||
47af18f533 | |||
0505220dac | |||
f7711b71d6 | |||
0255fde5fe | |||
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 |
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
|
||||||
|
78
.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,12 +52,22 @@ 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"
|
||||||
@ -73,10 +84,13 @@ jobs:
|
|||||||
path: |
|
path: |
|
||||||
electron-builder.yml
|
electron-builder.yml
|
||||||
|
|
||||||
|
- id: export_notes
|
||||||
|
run: echo "notes=`cat release-notes.md'`" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- 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' }}
|
if: ${{ env.CUT_RELEASE_PR == 'true' }}
|
||||||
@ -91,7 +105,13 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os: [macos-14, windows-2022, ubuntu-22.04]
|
include:
|
||||||
|
- os: macos-14
|
||||||
|
platform: mac
|
||||||
|
- os: windows-2022
|
||||||
|
platform: win
|
||||||
|
- os: ubuntu-22.04
|
||||||
|
platform: linux
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
env:
|
env:
|
||||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||||
@ -118,6 +138,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
|
- uses: actions/download-artifact@v3
|
||||||
if: ${{ github.event_name == 'schedule' }}
|
if: ${{ github.event_name == 'schedule' }}
|
||||||
@ -173,16 +194,28 @@ jobs:
|
|||||||
|
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: out-arm64-${{ matrix.os }}
|
name: out-arm64-${{ matrix.platform }}
|
||||||
|
# first two will pick both Zoo Modeling App-$VERSION-arm64-win.exe and Zoo Modeling App-$VERSION-win.exe
|
||||||
path: |
|
path: |
|
||||||
out/Zoo*arm64*.*
|
out/*-${{ env.VERSION_NO_V }}-win.*
|
||||||
out/latest*.yml
|
out/*-${{ env.VERSION_NO_V }}-arm64-win.*
|
||||||
|
out/*-arm64-mac.*
|
||||||
|
out/*-arm64-linux.*
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: out-x64-${{ matrix.os }}
|
name: out-x64-${{ matrix.platform }}
|
||||||
path: |
|
path: |
|
||||||
out/Zoo*x*64*.*
|
out/*-x64-win.*
|
||||||
|
out/*-x64-mac.*
|
||||||
|
out/*-x86_64-linux.*
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: ${{ env.CUT_RELEASE_PR == 'true' }}
|
||||||
|
with:
|
||||||
|
name: out-yml
|
||||||
|
path: |
|
||||||
|
out/latest*.yml
|
||||||
|
|
||||||
# TODO: add the 'Build for Mac TestFlight (nightly)' stage back
|
# TODO: add the 'Build for Mac TestFlight (nightly)' stage back
|
||||||
|
|
||||||
@ -203,16 +236,20 @@ 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-arm64-${{ matrix.platform }}
|
||||||
path: |
|
path: |
|
||||||
out/Zoo*arm64*.*
|
out/*-arm64-win.exe
|
||||||
|
out/*-arm64-mac.dmg
|
||||||
|
out/*-arm64-linux.AppImage
|
||||||
|
|
||||||
- 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-x64-${{ matrix.os }}
|
name: updater-test-x64-${{ matrix.platform }}
|
||||||
path: |
|
path: |
|
||||||
out/Zoo*x64*.*
|
out/*-x64-win.exe
|
||||||
|
out/*-x64-mac.dmg
|
||||||
|
out/*-x86_64-linux.AppImage
|
||||||
|
|
||||||
|
|
||||||
publish-apps-release:
|
publish-apps-release:
|
||||||
@ -225,7 +262,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 +271,37 @@ jobs:
|
|||||||
|
|
||||||
- uses: actions/download-artifact@v3
|
- uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: out-arm64-windows-2022
|
name: out-arm64-win
|
||||||
path: out
|
path: out
|
||||||
|
|
||||||
- uses: actions/download-artifact@v3
|
- uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: out-x64-windows-2022
|
name: out-x64-win
|
||||||
path: out
|
path: out
|
||||||
|
|
||||||
- uses: actions/download-artifact@v3
|
- uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: out-arm64-macos-14
|
name: out-arm64-mac
|
||||||
path: out
|
path: out
|
||||||
|
|
||||||
- uses: actions/download-artifact@v3
|
- uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: out-x64-macos-14
|
name: out-x64-mac
|
||||||
path: out
|
path: out
|
||||||
|
|
||||||
- uses: actions/download-artifact@v3
|
- uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: out-arm64-ubuntu-22.04
|
name: out-arm64-linux
|
||||||
path: out
|
path: out
|
||||||
|
|
||||||
- uses: actions/download-artifact@v3
|
- uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: out-x64-ubuntu-22.04
|
name: out-x64-linux
|
||||||
|
path: out
|
||||||
|
|
||||||
|
- uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
name: out-yml
|
||||||
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
|
||||||
|
78
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,50 @@ 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.
|
##### Release builds
|
||||||
|
|
||||||
We don't have a strict process, but click around and check for anything obvious, posting results as comments in the Cut Release PR.
|
The release builds can be found under the `out-{platform}` zip, at the very bottom of the `build-publish-apps` summary page for each commit on this branch.
|
||||||
|
|
||||||
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).
|
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.
|
||||||
|
|
||||||
#### 3. Merge the Cut Release PR
|
##### Updater-test builds
|
||||||
|
|
||||||
|
The other `build-publish-apps` output in Cut Release PRs is `updater-test-{platform}`. As we don't have a way to test this fully automatically, we have a semi-automated process. For macOS, Windows, and Linux, download the corresponding updater-test artifact file, install the app, run it, expect an updater prompt to a dummy v0.255.255, install it and check that the app comes back at that version.
|
||||||
|
|
||||||
|
The only difference with these builds is that they point to a different update location on the release bucket, with this dummy v0.255.255 always available. This helps ensuring that the version we release will be able to update to the next one available.
|
||||||
|
|
||||||
|
If the prompt doesn't show up, start the app in command line to grab the electron-updater logs. This is likely an issue with the current build that needs addressing (or the updater-test location in the storage bucket).
|
||||||
|
```
|
||||||
|
# Windows (PowerShell)
|
||||||
|
& 'C:\Program Files\Zoo Modeling App\Zoo Modeling App.exe'
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
/Applications/Zoo\ Modeling\ App.app/Contents/MacOS/Zoo\ Modeling\ App
|
||||||
|
|
||||||
|
# Linux
|
||||||
|
./Zoo Modeling App-{version}-{arch}-linux.AppImage
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. Merge the Cut Release PR
|
||||||
|
|
||||||
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 +352,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
|
||||||
|
}
|
@ -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)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -249,7 +249,7 @@ export async function submitAndAwaitTextToKcl({
|
|||||||
|
|
||||||
export async function sendTelemetry(
|
export async function sendTelemetry(
|
||||||
id: string,
|
id: string,
|
||||||
feedback: Models['AiFeedback_type'],
|
feedback: Models['MlFeedback_type'],
|
||||||
token?: string
|
token?: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const url =
|
const url =
|
||||||
|