Compare commits
33 Commits
kurt-rever
...
kcl-0.2.21
Author | SHA1 | Date | |
---|---|---|---|
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
|
||||
|
||||
retry=1
|
||||
max_retrys=2
|
||||
max_retrys=4
|
||||
|
||||
# retry failed tests, doing our own retries because using inbuilt playwright retries causes connection issues
|
||||
while [[ $retry -le $max_retrys ]]; do
|
||||
|
83
.github/workflows/build-test-publish-apps.yml
vendored
@ -25,6 +25,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04 # seperate job on Ubuntu for easy string manipulations (compared to Windows)
|
||||
outputs:
|
||||
version: ${{ steps.export_version.outputs.version }}
|
||||
notes: ${{ steps.export_version.outputs.notes }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@ -51,35 +52,35 @@ jobs:
|
||||
run: |
|
||||
VERSION=$(date +'%-y.%-m.%-d') yarn bump-jsons
|
||||
|
||||
# TODO: see if we need to inject updater nightly URL here https://dl.zoo.dev/releases/modeling-app/nightly/last_update.json
|
||||
|
||||
- name: Generate release notes
|
||||
env:
|
||||
NOTES: ${{ github.event_name == 'release' && github.event.release.body || format('Non-release build, commit {0}', github.sha) }}
|
||||
run: |
|
||||
echo "$NOTES" > release-notes.md
|
||||
cat release-notes.md
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: prepared-files
|
||||
path: |
|
||||
package.json
|
||||
src/wasm-lib/pkg/wasm_lib*
|
||||
release-notes.md
|
||||
|
||||
- id: export_version
|
||||
run: echo "version=`cat package.json | jq -r '.version'`" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Prepare electron-builder.yml file for nightly
|
||||
if: ${{ github.event_name == 'schedule' }}
|
||||
run: |
|
||||
yq -i '.publish[0].url = "https://dl.zoo.dev/releases/modeling-app/nightly"' electron-builder.yml
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: ${{ github.event_name == 'schedule' }}
|
||||
with:
|
||||
name: prepared-files-nightly
|
||||
path: |
|
||||
electron-builder.yml
|
||||
- id: export_notes
|
||||
run: echo "notes=`cat release-notes.md'`" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Prepare electron-builder.yml file for updater test
|
||||
if: ${{ env.CUT_RELEASE_PR == 'true' }}
|
||||
run: |
|
||||
yq -i '.publish[0].url = "https://dl.zoo.dev/releases/modeling-app/updater-test"' 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
|
||||
if: ${{ env.CUT_RELEASE_PR == 'true' }}
|
||||
with:
|
||||
name: prepared-files-updater-test
|
||||
path: |
|
||||
@ -118,16 +119,7 @@ jobs:
|
||||
cp prepared-files/src/wasm-lib/pkg/wasm_lib_bg.wasm public
|
||||
mkdir src/wasm-lib/pkg
|
||||
cp prepared-files/src/wasm-lib/pkg/wasm_lib* src/wasm-lib/pkg
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
if: ${{ github.event_name == 'schedule' }}
|
||||
name: prepared-files-nightly
|
||||
|
||||
- name: Copy updated electron-builder.yml file for nightly build
|
||||
if: ${{ github.event_name == 'schedule' }}
|
||||
run: |
|
||||
ls -R prepared-files-nightly
|
||||
cp prepared-files-nightly/electron-builder.yml electron-builder.yml
|
||||
cp prepared-files/release-notes.md release-notes.md
|
||||
|
||||
- name: Sync node version and setup cache
|
||||
uses: actions/setup-node@v4
|
||||
@ -173,17 +165,11 @@ jobs:
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: out-arm64-${{ matrix.os }}
|
||||
name: out-${{ matrix.os }}
|
||||
path: |
|
||||
out/Zoo*arm64*.*
|
||||
out/Zoo*.*
|
||||
out/latest*.yml
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: out-x64-${{ matrix.os }}
|
||||
path: |
|
||||
out/Zoo*x*64*.*
|
||||
|
||||
# TODO: add the 'Build for Mac TestFlight (nightly)' stage back
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
@ -203,16 +189,10 @@ jobs:
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: ${{ env.CUT_RELEASE_PR == 'true' }}
|
||||
with:
|
||||
name: updater-test-arm64-${{ matrix.os }}
|
||||
name: updater-test-${{ matrix.os }}
|
||||
path: |
|
||||
out/Zoo*arm64*.*
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: ${{ env.CUT_RELEASE_PR == 'true' }}
|
||||
with:
|
||||
name: updater-test-x64-${{ matrix.os }}
|
||||
path: |
|
||||
out/Zoo*x64*.*
|
||||
out/Zoo*.*
|
||||
out/latest*.yml
|
||||
|
||||
|
||||
publish-apps-release:
|
||||
@ -225,7 +205,7 @@ jobs:
|
||||
VERSION_NO_V: ${{ needs.prepare-files.outputs.version }}
|
||||
VERSION: ${{ github.event_name == 'schedule' && needs.prepare-files.outputs.version || format('v{0}', needs.prepare-files.outputs.version) }}
|
||||
PUB_DATE: ${{ github.event_name == 'release' && github.event.release.created_at || github.event.repository.updated_at }}
|
||||
NOTES: ${{ 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' }}
|
||||
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' }}
|
||||
@ -234,32 +214,17 @@ jobs:
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: out-arm64-windows-2022
|
||||
name: out-windows-2022
|
||||
path: out
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: out-x64-windows-2022
|
||||
name: out-macos-14
|
||||
path: out
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: out-arm64-macos-14
|
||||
path: out
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: out-x64-macos-14
|
||||
path: out
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: out-arm64-ubuntu-22.04
|
||||
path: out
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: out-x64-ubuntu-22.04
|
||||
name: out-ubuntu-22.04
|
||||
path: out
|
||||
|
||||
- name: Generate the download static endpoint
|
||||
|
8
.github/workflows/e2e-tests.yml
vendored
@ -142,6 +142,7 @@ jobs:
|
||||
with:
|
||||
name: playwright-report-${{ matrix.os }}-snapshot-${{ matrix.shardIndex }}-${{ github.sha }}
|
||||
path: playwright-report/
|
||||
include-hidden-files: true
|
||||
retention-days: 30
|
||||
overwrite: true
|
||||
- name: Clean up test-results
|
||||
@ -177,6 +178,7 @@ jobs:
|
||||
with:
|
||||
name: playwright-report-${{ matrix.os }}-${{ matrix.shardIndex }}-${{ github.sha }}
|
||||
path: playwright-report/
|
||||
include-hidden-files: true
|
||||
retention-days: 30
|
||||
- uses: actions/download-artifact@v4
|
||||
if: ${{ !cancelled() && (success() || failure()) }}
|
||||
@ -207,6 +209,7 @@ jobs:
|
||||
with:
|
||||
name: test-results-${{ matrix.os }}-${{ matrix.shardIndex }}-${{ github.sha }}
|
||||
path: test-results/
|
||||
include-hidden-files: true
|
||||
retention-days: 30
|
||||
overwrite: true
|
||||
- uses: actions/upload-artifact@v4
|
||||
@ -214,6 +217,7 @@ jobs:
|
||||
with:
|
||||
name: playwright-report-${{ matrix.os }}-${{ matrix.shardIndex }}-${{ github.sha }}
|
||||
path: playwright-report/
|
||||
include-hidden-files: true
|
||||
retention-days: 30
|
||||
overwrite: true
|
||||
|
||||
@ -313,7 +317,7 @@ jobs:
|
||||
if: ${{ !cancelled() && (success() || failure()) }}
|
||||
continue-on-error: true
|
||||
with:
|
||||
name: test-results-${{ matrix.os }}-${{ github.sha }}
|
||||
name: test-results-electron-${{ matrix.os }}-${{ github.sha }}
|
||||
path: test-results/
|
||||
- name: Run electron tests (with retries)
|
||||
id: retry
|
||||
@ -339,6 +343,7 @@ jobs:
|
||||
with:
|
||||
name: test-results-electron-${{ matrix.os }}-${{ github.sha }}
|
||||
path: test-results/
|
||||
include-hidden-files: true
|
||||
retention-days: 30
|
||||
overwrite: true
|
||||
- uses: actions/upload-artifact@v4
|
||||
@ -346,5 +351,6 @@ jobs:
|
||||
with:
|
||||
name: playwright-report-electron-${{ matrix.os }}-${{ github.sha }}
|
||||
path: playwright-report/
|
||||
include-hidden-files: true
|
||||
retention-days: 30
|
||||
overwrite: true
|
||||
|
56
README.md
@ -2,7 +2,7 @@
|
||||
|
||||
## 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).
|
||||
|
||||
@ -57,7 +57,7 @@ yarn install
|
||||
followed by:
|
||||
|
||||
```
|
||||
yarn build:wasm-dev
|
||||
yarn build:wasm
|
||||
```
|
||||
|
||||
or if you have the gh cli installed
|
||||
@ -66,15 +66,15 @@ or if you have the gh cli installed
|
||||
./get-latest-wasm-bundle.sh # this will download the latest main wasm bundle
|
||||
```
|
||||
|
||||
That will build the WASM binary and put in the `public` dir (though gitignored)
|
||||
That will build the WASM binary and put in the `public` dir (though gitignored).
|
||||
|
||||
finally, to run the web app only, run:
|
||||
Finally, to run the web app only, run:
|
||||
|
||||
```
|
||||
yarn start
|
||||
```
|
||||
|
||||
If you're not an KittyCAD employee you won't be able to access the dev environment, you should copy everything from `.env.production` to `.env.development` to make it point to production instead, then when you navigate to `localhost:3000` the easiest way to sign in is to paste `localStorage.setItem('TOKEN_PERSIST_KEY', "your-token-from-https://zoo.dev/account/api-tokens")` replacing the with a real token from https://zoo.dev/account/api-tokens 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
|
||||
|
||||
@ -91,13 +91,13 @@ Third-Party Cookies".
|
||||
|
||||
## 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.
|
||||
|
||||
@ -128,7 +128,18 @@ Before you submit a contribution PR to this repo, please ensure that:
|
||||
|
||||
## Release a new version
|
||||
|
||||
#### 1. Bump the versions by running `./make-release.sh` 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:
|
||||
- run `./make-release.sh` or `./make-release.sh patch` for a patch update;
|
||||
@ -137,28 +148,32 @@ That will create the branch with the updated json files for you:
|
||||
|
||||
After it runs you should just need the push the branch and open a PR.
|
||||
|
||||
**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.
|
||||
|
||||
#### 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.
|
||||
|
||||
We don't have a strict process, but click around and check for anything obvious, posting results as comments in the Cut Release PR.
|
||||
Manually test against this [list](https://github.com/KittyCAD/modeling-app/issues/3588) across Windows, MacOS, Linux and posting results as comments in the Cut Release PR.
|
||||
|
||||
The other `ci` output in Cut Release PRs is `updater-test`, because we don't have a way to test this fully automated, we have a semi-automated process. Download updater-test zip file, install the app, run it, expect an updater prompt to a dummy v0.99.99, install it and check that the app comes back at that version (on both macOS and Windows).
|
||||
|
||||
#### 3. Merge the Cut Release PR
|
||||
#### 4. Merge the Cut Release PR
|
||||
|
||||
This will kick the `create-release` action, that creates a _Draft_ release out of this Cut Release PR merge after less than a minute, with the new version as title and Cut Release PR as description.
|
||||
|
||||
|
||||
#### 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_.
|
||||
|
||||
#### 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.
|
||||
|
||||
@ -319,7 +334,16 @@ Which will run our suite of [Vitest unit](https://vitest.dev/) and [React Testin
|
||||
|
||||
```bash
|
||||
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
|
||||
|
13855
docs/kcl/std.json
@ -1,10 +1,10 @@
|
||||
---
|
||||
title: "KclValue"
|
||||
excerpt: "A memory item."
|
||||
excerpt: "Any KCL value."
|
||||
layout: manual
|
||||
---
|
||||
|
||||
A memory item.
|
||||
Any KCL value.
|
||||
|
||||
|
||||
|
||||
@ -80,7 +80,7 @@ A plane.
|
||||
|----------|------|-------------|----------|
|
||||
| `type` |enum: `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 |
|
||||
| `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 |
|
||||
@ -183,8 +183,8 @@ Data for an imported geometry.
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `type` |enum: `Function`| | No |
|
||||
| `expression` |[`FunctionExpression`](/docs/kcl/types/FunctionExpression)| A memory item. | No |
|
||||
| `memory` |[`ProgramMemory`](/docs/kcl/types/ProgramMemory)| A memory item. | No |
|
||||
| `expression` |[`FunctionExpression`](/docs/kcl/types/FunctionExpression)| Any KCL value. | No |
|
||||
| `memory` |[`ProgramMemory`](/docs/kcl/types/ProgramMemory)| Any KCL value. | No |
|
||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | 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
|
||||
}
|
||||
|
||||
type SceneSerialised = {
|
||||
camera: {
|
||||
position: [number, number, number]
|
||||
target: [number, number, number]
|
||||
}
|
||||
}
|
||||
|
||||
export class SceneFixture {
|
||||
public page: Page
|
||||
|
||||
@ -22,6 +29,22 @@ export class SceneFixture {
|
||||
this.page = page
|
||||
this.reConstruct(page)
|
||||
}
|
||||
private _serialiseScene = async (): Promise<SceneSerialised> => {
|
||||
const camera = await this.getCameraInfo()
|
||||
|
||||
return {
|
||||
camera,
|
||||
}
|
||||
}
|
||||
|
||||
expectState = async (expected: SceneSerialised) => {
|
||||
return expect
|
||||
.poll(() => this._serialiseScene(), {
|
||||
message: `Expected scene state to match`,
|
||||
})
|
||||
.toEqual(expected)
|
||||
}
|
||||
|
||||
reConstruct = (page: Page) => {
|
||||
this.page = page
|
||||
|
||||
@ -31,7 +54,7 @@ export class SceneFixture {
|
||||
makeMouseHelpers = (
|
||||
x: number,
|
||||
y: number,
|
||||
{ steps }: { steps: number } = { steps: 5000 }
|
||||
{ steps }: { steps: number } = { steps: 20 }
|
||||
) =>
|
||||
[
|
||||
(clickParams?: mouseParams) => {
|
||||
@ -87,6 +110,36 @@ export class SceneFixture {
|
||||
)
|
||||
await closeDebugPanel(this.page)
|
||||
}
|
||||
/** Forces a refresh of the camera position and target displayed
|
||||
* in the debug panel and then returns the values of the fields
|
||||
*/
|
||||
async getCameraInfo() {
|
||||
await openAndClearDebugPanel(this.page)
|
||||
await sendCustomCmd(this.page, {
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'default_camera_get_settings',
|
||||
},
|
||||
})
|
||||
await this.waitForExecutionDone()
|
||||
const position = await Promise.all([
|
||||
this.page.getByTestId('cam-x-position').inputValue().then(Number),
|
||||
this.page.getByTestId('cam-y-position').inputValue().then(Number),
|
||||
this.page.getByTestId('cam-z-position').inputValue().then(Number),
|
||||
])
|
||||
const target = await Promise.all([
|
||||
this.page.getByTestId('cam-x-target').inputValue().then(Number),
|
||||
this.page.getByTestId('cam-y-target').inputValue().then(Number),
|
||||
this.page.getByTestId('cam-z-target').inputValue().then(Number),
|
||||
])
|
||||
await closeDebugPanel(this.page)
|
||||
return {
|
||||
position,
|
||||
target,
|
||||
}
|
||||
}
|
||||
|
||||
waitForExecutionDone = async () => {
|
||||
await expect(this.exeIndicator).toBeVisible()
|
||||
}
|
||||
@ -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 {
|
||||
doExport,
|
||||
executorInputPath,
|
||||
@ -618,31 +618,30 @@ test(
|
||||
'Deleting projects, can delete individual project, can still create projects after deleting all',
|
||||
{ tag: '@electron' },
|
||||
async ({ browserName }, testInfo) => {
|
||||
const projectData = [
|
||||
['router-template-slate', 'cylinder.kcl'],
|
||||
['bracket', 'focusrite_scarlett_mounting_braket.kcl'],
|
||||
['lego', 'lego.kcl'],
|
||||
]
|
||||
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async (dir) => {
|
||||
// Do these serially to ensure the order is correct
|
||||
for (const [name, file] of projectData) {
|
||||
await fsp.mkdir(join(dir, name), { recursive: true })
|
||||
await fsp.copyFile(
|
||||
executorInputPath(file),
|
||||
join(dir, name, `main.kcl`)
|
||||
)
|
||||
// Wait 1s between each project to ensure the order is correct
|
||||
await new Promise((r) => setTimeout(r, 1_000))
|
||||
}
|
||||
},
|
||||
})
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
|
||||
page.on('console', console.log)
|
||||
|
||||
const createProjectAndRenameItTest = async ({
|
||||
name,
|
||||
page,
|
||||
}: {
|
||||
name: string
|
||||
page: Page
|
||||
}) => {
|
||||
await test.step(`Create and rename project ${name}`, async () => {
|
||||
await createProjectAndRenameIt({ name, page })
|
||||
})
|
||||
}
|
||||
|
||||
// we need to create the folders so that the order is correct
|
||||
// creating them ahead of time with fs tools means they all have the same timestamp
|
||||
await createProjectAndRenameItTest({ name: 'router-template-slate', page })
|
||||
await createProjectAndRenameItTest({ name: 'bracket', page })
|
||||
await createProjectAndRenameItTest({ name: 'lego', page })
|
||||
|
||||
await test.step('delete the middle project, i.e. the bracket project', async () => {
|
||||
const project = page.getByText('bracket')
|
||||
|
||||
@ -744,8 +743,26 @@ test(
|
||||
'Can sort projects on home page',
|
||||
{ tag: '@electron' },
|
||||
async ({ browserName }, testInfo) => {
|
||||
const projectData = [
|
||||
['router-template-slate', 'cylinder.kcl'],
|
||||
['bracket', 'focusrite_scarlett_mounting_braket.kcl'],
|
||||
['lego', 'lego.kcl'],
|
||||
]
|
||||
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async (dir) => {
|
||||
// Do these serially to ensure the order is correct
|
||||
for (const [name, file] of projectData) {
|
||||
await fsp.mkdir(join(dir, name), { recursive: true })
|
||||
await fsp.copyFile(
|
||||
executorInputPath(file),
|
||||
join(dir, name, `main.kcl`)
|
||||
)
|
||||
// Wait 1s between each project to ensure the order is correct
|
||||
await new Promise((r) => setTimeout(r, 1_000))
|
||||
}
|
||||
},
|
||||
})
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
|
||||
@ -753,24 +770,6 @@ test(
|
||||
|
||||
page.on('console', console.log)
|
||||
|
||||
const createProjectAndRenameItTest = async ({
|
||||
name,
|
||||
page,
|
||||
}: {
|
||||
name: string
|
||||
page: Page
|
||||
}) => {
|
||||
await test.step(`Create and rename project ${name}`, async () => {
|
||||
await createProjectAndRenameIt({ name, page })
|
||||
})
|
||||
}
|
||||
|
||||
// we need to create the folders so that the order is correct
|
||||
// creating them ahead of time with fs tools means they all have the same timestamp
|
||||
await createProjectAndRenameItTest({ name: 'router-template-slate', page })
|
||||
await createProjectAndRenameItTest({ name: 'bracket', page })
|
||||
await createProjectAndRenameItTest({ name: 'lego', page })
|
||||
|
||||
await test.step('should be shorted by modified initially', async () => {
|
||||
const lastModifiedButton = page.getByRole('button', {
|
||||
name: 'Last Modified',
|
||||
|
@ -1115,6 +1115,102 @@ sketch002 = startSketchOn(extrude001, 'END')
|
||||
).toHaveAttribute('aria-pressed', 'true')
|
||||
}).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', () => {
|
||||
|
@ -521,7 +521,6 @@ test(
|
||||
const startXPx = 600
|
||||
|
||||
// Equip the rectangle tool
|
||||
await page.getByRole('button', { name: 'line Line', exact: true }).click()
|
||||
await page
|
||||
.getByRole('button', { name: 'rectangle Corner rectangle', exact: true })
|
||||
.click()
|
||||
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 50 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: 51 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: 73 KiB After Width: | Height: | Size: 74 KiB |
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 65 KiB |
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 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 { uuidv4 } from 'lib/utils'
|
||||
import { TEST_CODE_GIZMO } from './storageStates'
|
||||
|
||||
test.beforeEach(async ({ context, page }, testInfo) => {
|
||||
_test.beforeEach(async ({ context, page }, testInfo) => {
|
||||
await setup(context, page, testInfo)
|
||||
})
|
||||
|
||||
test.afterEach(async ({ page }, testInfo) => {
|
||||
_test.afterEach(async ({ page }, testInfo) => {
|
||||
await tearDown(page, testInfo)
|
||||
})
|
||||
|
||||
test.describe('Testing Gizmo', () => {
|
||||
_test.describe('Testing Gizmo', () => {
|
||||
const cases = [
|
||||
{
|
||||
testDescription: 'top view',
|
||||
@ -57,7 +57,7 @@ test.describe('Testing Gizmo', () => {
|
||||
expectedCameraTarget,
|
||||
testDescription,
|
||||
} of cases) {
|
||||
test(`check ${testDescription}`, async ({ page, browserName }) => {
|
||||
_test(`check ${testDescription}`, async ({ page, browserName }) => {
|
||||
const u = await getUtils(page)
|
||||
await page.addInitScript((TEST_CODE_GIZMO) => {
|
||||
localStorage.setItem('persistCode', TEST_CODE_GIZMO)
|
||||
@ -117,30 +117,30 @@ test.describe('Testing Gizmo', () => {
|
||||
|
||||
await Promise.all([
|
||||
// position
|
||||
expect(page.getByTestId('cam-x-position')).toHaveValue(
|
||||
_expect(page.getByTestId('cam-x-position')).toHaveValue(
|
||||
expectedCameraPosition.x.toString()
|
||||
),
|
||||
expect(page.getByTestId('cam-y-position')).toHaveValue(
|
||||
_expect(page.getByTestId('cam-y-position')).toHaveValue(
|
||||
expectedCameraPosition.y.toString()
|
||||
),
|
||||
expect(page.getByTestId('cam-z-position')).toHaveValue(
|
||||
_expect(page.getByTestId('cam-z-position')).toHaveValue(
|
||||
expectedCameraPosition.z.toString()
|
||||
),
|
||||
// target
|
||||
expect(page.getByTestId('cam-x-target')).toHaveValue(
|
||||
_expect(page.getByTestId('cam-x-target')).toHaveValue(
|
||||
expectedCameraTarget.x.toString()
|
||||
),
|
||||
expect(page.getByTestId('cam-y-target')).toHaveValue(
|
||||
_expect(page.getByTestId('cam-y-target')).toHaveValue(
|
||||
expectedCameraTarget.y.toString()
|
||||
),
|
||||
expect(page.getByTestId('cam-z-target')).toHaveValue(
|
||||
_expect(page.getByTestId('cam-z-target')).toHaveValue(
|
||||
expectedCameraTarget.z.toString()
|
||||
),
|
||||
])
|
||||
})
|
||||
}
|
||||
|
||||
test('Context menu and popover menu', async ({ page }) => {
|
||||
_test('Context menu and popover menu', async ({ page }) => {
|
||||
const testCase = {
|
||||
testDescription: 'Right view',
|
||||
expectedCameraPosition: { x: 5660.02, y: -152, z: 26 },
|
||||
@ -196,7 +196,7 @@ test.describe('Testing Gizmo', () => {
|
||||
const buttonToTest = page.getByRole('button', {
|
||||
name: testCase.testDescription,
|
||||
})
|
||||
await expect(buttonToTest).toBeVisible()
|
||||
await _expect(buttonToTest).toBeVisible()
|
||||
await buttonToTest.click()
|
||||
|
||||
// Now assert we've moved to the correct view
|
||||
@ -215,23 +215,23 @@ test.describe('Testing Gizmo', () => {
|
||||
|
||||
await Promise.all([
|
||||
// position
|
||||
expect(page.getByTestId('cam-x-position')).toHaveValue(
|
||||
_expect(page.getByTestId('cam-x-position')).toHaveValue(
|
||||
testCase.expectedCameraPosition.x.toString()
|
||||
),
|
||||
expect(page.getByTestId('cam-y-position')).toHaveValue(
|
||||
_expect(page.getByTestId('cam-y-position')).toHaveValue(
|
||||
testCase.expectedCameraPosition.y.toString()
|
||||
),
|
||||
expect(page.getByTestId('cam-z-position')).toHaveValue(
|
||||
_expect(page.getByTestId('cam-z-position')).toHaveValue(
|
||||
testCase.expectedCameraPosition.z.toString()
|
||||
),
|
||||
// target
|
||||
expect(page.getByTestId('cam-x-target')).toHaveValue(
|
||||
_expect(page.getByTestId('cam-x-target')).toHaveValue(
|
||||
testCase.expectedCameraTarget.x.toString()
|
||||
),
|
||||
expect(page.getByTestId('cam-y-target')).toHaveValue(
|
||||
_expect(page.getByTestId('cam-y-target')).toHaveValue(
|
||||
testCase.expectedCameraTarget.y.toString()
|
||||
),
|
||||
expect(page.getByTestId('cam-z-target')).toHaveValue(
|
||||
_expect(page.getByTestId('cam-z-target')).toHaveValue(
|
||||
testCase.expectedCameraTarget.z.toString()
|
||||
),
|
||||
])
|
||||
@ -242,8 +242,60 @@ test.describe('Testing Gizmo', () => {
|
||||
const gizmoPopoverButton = page.getByRole('button', {
|
||||
name: 'view settings',
|
||||
})
|
||||
await expect(gizmoPopoverButton).toBeVisible()
|
||||
await _expect(gizmoPopoverButton).toBeVisible()
|
||||
await gizmoPopoverButton.click()
|
||||
await expect(buttonToTest).toBeVisible()
|
||||
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 ({
|
||||
page,
|
||||
}) => {
|
||||
/**
|
||||
* If the line tool is clicked when the state is 'No Points' it will exit Sketch mode.
|
||||
* This is the same exact workflow as pressing ESC.
|
||||
*
|
||||
* To continue to test this workflow, we now enter sketch mode and place a single point before exiting the line tool.
|
||||
*/
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
|
||||
@ -1228,6 +1234,7 @@ extrude001 = extrude(50, sketch001)
|
||||
200
|
||||
)
|
||||
|
||||
// Clicks the XZ Plane in the page
|
||||
await page.mouse.click(700, 200)
|
||||
|
||||
await expect(page.locator('.cm-content')).toHaveText(
|
||||
@ -1236,6 +1243,11 @@ extrude001 = extrude(50, sketch001)
|
||||
|
||||
await page.waitForTimeout(600)
|
||||
|
||||
// Place a point because the line tool will exit if no points are pressed
|
||||
await page.mouse.click(650, 200)
|
||||
await page.waitForTimeout(600)
|
||||
|
||||
// Code before exiting the tool
|
||||
let previousCodeContent = await page.locator('.cm-content').innerText()
|
||||
|
||||
// deselect the line tool by clicking it
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
executorInputPath,
|
||||
} from './test-utils'
|
||||
import { SaveSettingsPayload, SettingsLevel } from 'lib/settings/settingsTypes'
|
||||
import { SETTINGS_FILE_NAME } from 'lib/constants'
|
||||
import {
|
||||
TEST_SETTINGS_KEY,
|
||||
TEST_SETTINGS_CORRUPTED,
|
||||
@ -343,7 +344,7 @@ test.describe('Testing settings', () => {
|
||||
|
||||
// Selectors and constants
|
||||
const errorHeading = page.getByRole('heading', {
|
||||
name: 'An unextected error occurred',
|
||||
name: 'An unexpected error occurred',
|
||||
})
|
||||
const projectDirLink = page.getByText('Loaded from')
|
||||
|
||||
@ -372,7 +373,7 @@ test.describe('Testing settings', () => {
|
||||
|
||||
// Selectors and constants
|
||||
const errorHeading = page.getByRole('heading', {
|
||||
name: 'An unextected error occurred',
|
||||
name: 'An unexpected error occurred',
|
||||
})
|
||||
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(
|
||||
`Closing settings modal should go back to the original file being viewed`,
|
||||
{ tag: '@electron' },
|
||||
|
@ -32,10 +32,10 @@ win:
|
||||
arch:
|
||||
- x64
|
||||
- arm64
|
||||
- target: msi
|
||||
arch:
|
||||
- x64
|
||||
- arm64
|
||||
# - target: msi
|
||||
# arch:
|
||||
# - x64
|
||||
# - arm64
|
||||
signingHashAlgorithms:
|
||||
- sha256
|
||||
sign: "./sign-win.js"
|
||||
@ -47,9 +47,9 @@ win:
|
||||
mimeType: text/vnd.zoo.kcl
|
||||
description: Zoo KCL File
|
||||
role: Editor
|
||||
msi:
|
||||
oneClick: false
|
||||
perMachine: true
|
||||
# msi:
|
||||
# oneClick: false
|
||||
# perMachine: true
|
||||
nsis:
|
||||
oneClick: false
|
||||
perMachine: true
|
||||
@ -73,3 +73,5 @@ publish:
|
||||
- provider: generic
|
||||
url: https://dl.zoo.dev/releases/modeling-app
|
||||
channel: latest
|
||||
releaseInfo:
|
||||
releaseNotesFile: release-notes.md
|
||||
|
5
interface.d.ts
vendored
@ -23,7 +23,6 @@ export interface IElectronAPI {
|
||||
callback: (eventType: string, path: string) => void
|
||||
) => void
|
||||
watchFileOff: (path: string) => void
|
||||
watchFileObliterate: () => void
|
||||
readFile: (path: string) => ReturnType<fs.readFile>
|
||||
writeFile: (
|
||||
path: string,
|
||||
@ -70,9 +69,13 @@ export interface IElectronAPI {
|
||||
kittycad: (access: string, args: any) => any
|
||||
listMachines: () => Promise<MachinesListing>
|
||||
getMachineApiIp: () => Promise<string | null>
|
||||
onUpdateDownloadStart: (
|
||||
callback: (value: { version: string }) => void
|
||||
) => Electron.IpcRenderer
|
||||
onUpdateDownloaded: (
|
||||
callback: (value: string) => void
|
||||
) => Electron.IpcRenderer
|
||||
onUpdateError: (callback: (value: { error: Error }) => void) => Electron
|
||||
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!",
|
||||
"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": [
|
||||
"id",
|
||||
"machine_type",
|
||||
"make_model"
|
||||
"make_model",
|
||||
"state"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
@ -143,6 +152,67 @@
|
||||
},
|
||||
"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": {
|
||||
"description": "Specific technique by which this Machine takes a design, and produces a real-world 3D object.",
|
||||
"oneOf": [
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "zoo-modeling-app",
|
||||
"version": "0.25.5",
|
||||
"version": "0.25.6",
|
||||
"private": true,
|
||||
"productName": "Zoo Modeling App",
|
||||
"author": {
|
||||
@ -26,7 +26,7 @@
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@headlessui/react": "^1.7.19",
|
||||
"@headlessui/tailwindcss": "^0.2.0",
|
||||
"@kittycad/lib": "^2.0.1",
|
||||
"@kittycad/lib": "2.0.7",
|
||||
"@lezer/highlight": "^1.2.1",
|
||||
"@lezer/lr": "^1.4.1",
|
||||
"@react-hook/resize-observer": "^2.0.1",
|
||||
@ -36,6 +36,7 @@
|
||||
"@xstate/inspect": "^0.8.0",
|
||||
"@xstate/react": "^4.1.1",
|
||||
"bonjour-service": "^1.2.1",
|
||||
"chokidar": "^4.0.1",
|
||||
"codemirror": "^6.0.1",
|
||||
"decamelize": "^6.0.0",
|
||||
"electron-squirrel-startup": "^1.0.1",
|
||||
|
@ -893,6 +893,7 @@ export class CameraControls {
|
||||
type: 'zoom_to_fit',
|
||||
object_ids: [], // leave empty to zoom to all objects
|
||||
padding: 0.2, // padding around the objects
|
||||
animated: false, // don't animate the zoom for now
|
||||
},
|
||||
})
|
||||
}
|
||||
|
@ -408,6 +408,7 @@ export async function deleteSegment({
|
||||
|
||||
const testExecute = await executeAst({
|
||||
ast: modifiedAst,
|
||||
idGenerator: kclManager.execState.idGenerator,
|
||||
useFakeExecutor: true,
|
||||
engineCommandManager: engineCommandManager,
|
||||
})
|
||||
|
@ -391,12 +391,14 @@ export class SceneEntities {
|
||||
const { truncatedAst, programMemoryOverride, variableDeclarationName } =
|
||||
prepared
|
||||
|
||||
const { programMemory } = await executeAst({
|
||||
const { execState } = await executeAst({
|
||||
ast: truncatedAst,
|
||||
useFakeExecutor: true,
|
||||
engineCommandManager: this.engineCommandManager,
|
||||
programMemoryOverride,
|
||||
idGenerator: kclManager.execState.idGenerator,
|
||||
})
|
||||
const programMemory = execState.memory
|
||||
const sketch = sketchFromPathToNode({
|
||||
pathToNode: sketchPathToNode,
|
||||
ast: maybeModdedAst,
|
||||
@ -801,12 +803,14 @@ export class SceneEntities {
|
||||
updateRectangleSketch(sketchInit, x, y, tags[0])
|
||||
}
|
||||
|
||||
const { programMemory } = await executeAst({
|
||||
const { execState } = await executeAst({
|
||||
ast: truncatedAst,
|
||||
useFakeExecutor: true,
|
||||
engineCommandManager: this.engineCommandManager,
|
||||
programMemoryOverride,
|
||||
idGenerator: kclManager.execState.idGenerator,
|
||||
})
|
||||
const programMemory = execState.memory
|
||||
this.sceneProgramMemory = programMemory
|
||||
const sketch = sketchFromKclValue(
|
||||
programMemory.get(variableDeclarationName),
|
||||
@ -848,12 +852,14 @@ export class SceneEntities {
|
||||
await kclManager.executeAstMock(_ast)
|
||||
sceneInfra.modelingSend({ type: 'Finish rectangle' })
|
||||
|
||||
const { programMemory } = await executeAst({
|
||||
const { execState } = await executeAst({
|
||||
ast: _ast,
|
||||
useFakeExecutor: true,
|
||||
engineCommandManager: this.engineCommandManager,
|
||||
programMemoryOverride,
|
||||
idGenerator: kclManager.execState.idGenerator,
|
||||
})
|
||||
const programMemory = execState.memory
|
||||
|
||||
// Prepare to update the THREEjs scene
|
||||
this.sceneProgramMemory = programMemory
|
||||
@ -965,12 +971,14 @@ export class SceneEntities {
|
||||
modded = moddedResult.modifiedAst
|
||||
}
|
||||
|
||||
const { programMemory } = await executeAst({
|
||||
const { execState } = await executeAst({
|
||||
ast: modded,
|
||||
useFakeExecutor: true,
|
||||
engineCommandManager: this.engineCommandManager,
|
||||
programMemoryOverride,
|
||||
idGenerator: kclManager.execState.idGenerator,
|
||||
})
|
||||
const programMemory = execState.memory
|
||||
this.sceneProgramMemory = programMemory
|
||||
const sketch = sketchFromKclValue(
|
||||
programMemory.get(variableDeclarationName),
|
||||
@ -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
|
||||
// plus this would be the truncated ast being recast, it would be wrong
|
||||
codeManager.updateCodeEditor(code)
|
||||
const { programMemory } = await executeAst({
|
||||
const { execState } = await executeAst({
|
||||
ast: truncatedAst,
|
||||
useFakeExecutor: true,
|
||||
engineCommandManager: this.engineCommandManager,
|
||||
programMemoryOverride,
|
||||
idGenerator: kclManager.execState.idGenerator,
|
||||
})
|
||||
const programMemory = execState.memory
|
||||
this.sceneProgramMemory = programMemory
|
||||
|
||||
const maybeSketch = programMemory.get(variableDeclarationName)
|
||||
|
@ -157,7 +157,7 @@ export function useCalc({
|
||||
engineCommandManager,
|
||||
useFakeExecutor: true,
|
||||
programMemoryOverride: kclManager.programMemory.clone(),
|
||||
}).then(({ programMemory }) => {
|
||||
}).then(({ execState }) => {
|
||||
const resultDeclaration = ast.body.find(
|
||||
(a) =>
|
||||
a.type === 'VariableDeclaration' &&
|
||||
@ -166,7 +166,7 @@ export function useCalc({
|
||||
const init =
|
||||
resultDeclaration?.type === 'VariableDeclaration' &&
|
||||
resultDeclaration?.declarations?.[0]?.init
|
||||
const result = programMemory?.get('__result__')?.value
|
||||
const result = execState.memory?.get('__result__')?.value
|
||||
setCalcResult(typeof result === 'number' ? String(result) : 'NAN')
|
||||
init && setValueNode(init)
|
||||
})
|
||||
|
@ -91,7 +91,7 @@ function CommandBarSelectionInput({
|
||||
<form id="arg-form" onSubmit={handleSubmit}>
|
||||
<label
|
||||
className={
|
||||
'relative flex items-center mx-4 my-4 ' +
|
||||
'relative flex flex-col mx-4 my-4 ' +
|
||||
(!hasSubmitted || canSubmitSelection || 'text-destroy-50')
|
||||
}
|
||||
>
|
||||
@ -100,13 +100,18 @@ function CommandBarSelectionInput({
|
||||
: `Please select ${
|
||||
arg.multiple ? 'one or more ' : 'one '
|
||||
}${getSemanticSelectionType(arg.selectionTypes).join(' or ')}`}
|
||||
{arg.warningMessage && (
|
||||
<p className="text-warn-80 bg-warn-10 px-2 py-1 rounded-sm mt-3 mr-2 -mb-2 w-full text-sm cursor-default">
|
||||
{arg.warningMessage}
|
||||
</p>
|
||||
)}
|
||||
<input
|
||||
id="selection"
|
||||
name="selection"
|
||||
ref={inputRef}
|
||||
required
|
||||
placeholder="Select an entity with your mouse"
|
||||
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
|
||||
className="absolute inset-0 w-full h-full opacity-0 cursor-default"
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Backspace') {
|
||||
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 { CustomIcon } from './CustomIcon'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
|
||||
const CANVAS_SIZE = 80
|
||||
const FRUSTUM_SIZE = 0.5
|
||||
@ -62,6 +63,7 @@ export default function Gizmo() {
|
||||
const raycasterIntersect = useRef<Intersection<Object3D> | null>(null)
|
||||
const cameraPassiveUpdateTimer = useRef(0)
|
||||
const raycasterPassiveUpdateTimer = useRef(0)
|
||||
const { send: modelingSend } = useModelingContext()
|
||||
const menuItems = useMemo(
|
||||
() => [
|
||||
...Object.entries(axisNamesSemantic).map(([axisName, axisSemantic]) => (
|
||||
@ -76,6 +78,7 @@ export default function Gizmo() {
|
||||
{axisSemantic} view
|
||||
</ContextMenuItem>
|
||||
)),
|
||||
<ContextMenuDivider />,
|
||||
<ContextMenuItem
|
||||
onClick={() => {
|
||||
sceneInfra.camControls.resetCameraPosition().catch(reportRejection)
|
||||
@ -83,6 +86,13 @@ export default function Gizmo() {
|
||||
>
|
||||
Reset view
|
||||
</ContextMenuItem>,
|
||||
<ContextMenuItem
|
||||
onClick={() => {
|
||||
modelingSend({ type: 'Center camera on selection' })
|
||||
}}
|
||||
>
|
||||
Center view on selection
|
||||
</ContextMenuItem>,
|
||||
<ContextMenuDivider />,
|
||||
<ContextMenuItemRefresh />,
|
||||
],
|
||||
|
@ -83,6 +83,7 @@ import {
|
||||
} from 'lang/std/engineConnection'
|
||||
import { submitAndAwaitTextToKcl } from 'lib/textToCad'
|
||||
import { useFileContext } from 'hooks/useFileContext'
|
||||
import { uuidv4 } from 'lib/utils'
|
||||
|
||||
type MachineContext<T extends AnyStateMachine> = {
|
||||
state: StateFrom<T>
|
||||
@ -148,6 +149,13 @@ export const ModelingMachineProvider = ({
|
||||
},
|
||||
'sketch exit execute': ({ context: { store } }) => {
|
||||
;(async () => {
|
||||
// When cancelling the sketch mode we should disable sketch mode within the engine.
|
||||
await engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: { type: 'sketch_mode_disable' },
|
||||
})
|
||||
|
||||
sceneInfra.camControls.syncDirection = 'clientToEngine'
|
||||
|
||||
if (cameraProjection.current === 'perspective') {
|
||||
@ -243,6 +251,17 @@ export const ModelingMachineProvider = ({
|
||||
return {}
|
||||
},
|
||||
}),
|
||||
'Center camera on selection': () => {
|
||||
engineCommandManager
|
||||
.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'default_camera_center_to_selection',
|
||||
},
|
||||
})
|
||||
.catch(reportRejection)
|
||||
},
|
||||
'Set sketchDetails': assign(({ context: { sketchDetails }, event }) => {
|
||||
if (event.type !== 'Delete segment') return {}
|
||||
if (!sketchDetails) return {}
|
||||
@ -1037,6 +1056,11 @@ export const ModelingMachineProvider = ({
|
||||
modelingSend({ type: 'Delete selection' })
|
||||
})
|
||||
|
||||
// Allow ctrl+alt+c to center to selection
|
||||
useHotkeys(['mod + alt + c'], () => {
|
||||
modelingSend({ type: 'Center camera on selection' })
|
||||
})
|
||||
|
||||
useStateMachineCommands({
|
||||
machineId: 'modeling',
|
||||
state: modelingState,
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { DebugFeatureTree } from 'components/DebugFeatureTree'
|
||||
import { AstExplorer } from '../../AstExplorer'
|
||||
import { EngineCommands } from '../../EngineCommands'
|
||||
import { CamDebugSettings } from 'clientSideScene/ClientSideSceneComp'
|
||||
@ -12,6 +13,7 @@ export const DebugPane = () => {
|
||||
<EngineCommands />
|
||||
<CamDebugSettings />
|
||||
<AstExplorer />
|
||||
<DebugFeatureTree />
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
|
@ -29,8 +29,8 @@ describe('processMemory', () => {
|
||||
|> lineTo([2.15, 4.32], %)
|
||||
// |> rx(90, %)`
|
||||
const ast = parse(code)
|
||||
const programMemory = await enginelessExecutor(ast, ProgramMemory.empty())
|
||||
const output = processMemory(programMemory)
|
||||
const execState = await enginelessExecutor(ast, ProgramMemory.empty())
|
||||
const output = processMemory(execState.memory)
|
||||
expect(output.myVar).toEqual(5)
|
||||
expect(output.otherVar).toEqual(3)
|
||||
expect(output).toEqual({
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { trap } from 'lib/trap'
|
||||
import { useMachine } from '@xstate/react'
|
||||
import { useNavigate, useRouteLoaderData, useLocation } from 'react-router-dom'
|
||||
import { PATHS } from 'lib/paths'
|
||||
import { authMachine, TOKEN_PERSIST_KEY } from '../machines/authMachine'
|
||||
import withBaseUrl from '../lib/withBaseURL'
|
||||
import React, { createContext, useEffect } from 'react'
|
||||
import React, { createContext, useEffect, useState } from 'react'
|
||||
import useStateMachineCommands from '../hooks/useStateMachineCommands'
|
||||
import { settingsMachine } from 'machines/settingsMachine'
|
||||
import { toast } from 'react-hot-toast'
|
||||
@ -15,7 +16,6 @@ import {
|
||||
} from 'lib/theme'
|
||||
import decamelize from 'decamelize'
|
||||
import { Actor, AnyStateMachine, ContextFrom, Prop, StateFrom } from 'xstate'
|
||||
import { isDesktop } from 'lib/isDesktop'
|
||||
import { authCommandBarConfig } from 'lib/commandBarConfigs/authCommandConfig'
|
||||
import {
|
||||
kclManager,
|
||||
@ -33,8 +33,14 @@ import {
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { Command } from 'lib/commandTypes'
|
||||
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 { getAppSettingsFilePath } from 'lib/desktop'
|
||||
import { isDesktop } from 'lib/isDesktop'
|
||||
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
|
||||
|
||||
type MachineContext<T extends AnyStateMachine> = {
|
||||
state: StateFrom<T>
|
||||
@ -99,6 +105,9 @@ export const SettingsAuthProviderBase = ({
|
||||
const location = useLocation()
|
||||
const navigate = useNavigate()
|
||||
const { commandBarSend } = useCommandsContext()
|
||||
const [settingsPath, setSettingsPath] = useState<string | undefined>(
|
||||
undefined
|
||||
)
|
||||
|
||||
const [settingsState, settingsSend, settingsActor] = useMachine(
|
||||
settingsMachine.provide({
|
||||
@ -191,7 +200,11 @@ export const SettingsAuthProviderBase = ({
|
||||
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
|
||||
saveSettings(context, loadedProject?.project?.path)
|
||||
},
|
||||
@ -201,6 +214,23 @@ export const SettingsAuthProviderBase = ({
|
||||
)
|
||||
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
|
||||
// They're treated slightly differently than other commands
|
||||
// Because their state machine doesn't have a meaningful .nextEvents,
|
||||
|
@ -2,6 +2,7 @@ import { styleTags, tags as t } from '@lezer/highlight'
|
||||
|
||||
export const kclHighlight = styleTags({
|
||||
'fn var let const': t.definitionKeyword,
|
||||
'if else': t.controlKeyword,
|
||||
return: t.controlKeyword,
|
||||
'true false': t.bool,
|
||||
nil: t.null,
|
||||
|
@ -16,7 +16,7 @@
|
||||
|
||||
statement[@isGroup=Statement] {
|
||||
FunctionDeclaration { kw<"fn"> VariableDefinition Equals ParamList Arrow Body } |
|
||||
VariableDeclaration { (kw<"var"> | kw<"let"> | kw<"const">) VariableDefinition Equals expression } |
|
||||
VariableDeclaration { (kw<"var"> | kw<"let"> | kw<"const">)? VariableDefinition Equals expression } |
|
||||
ReturnStatement { kw<"return"> expression } |
|
||||
ExpressionStatement { expression }
|
||||
}
|
||||
@ -40,6 +40,7 @@ expression[@isGroup=Expression] {
|
||||
} |
|
||||
UnaryExpression { UnaryOp expression } |
|
||||
ParenthesizedExpression { "(" expression ")" } |
|
||||
IfExpression { kw<"if"> expression Body kw<"else"> Body } |
|
||||
CallExpression { expression !call ArgumentList } |
|
||||
ArrayExpression { "[" commaSep<expression | IntegerRange { expression !range ".." expression }> "]" } |
|
||||
ObjectExpression { "{" commaSep<ObjectProperty> "}" } |
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { isDesktop } from 'lib/isDesktop'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
import { useEffect, useState, useRef } from 'react'
|
||||
|
||||
type Path = string
|
||||
@ -11,13 +12,13 @@ type Path = string
|
||||
// watcher.addListener(() => { ... }).
|
||||
|
||||
export const useFileSystemWatcher = (
|
||||
callback: (path: Path) => void,
|
||||
callback: (path: Path) => Promise<void>,
|
||||
dependencyArray: Path[]
|
||||
): void => {
|
||||
// Track a ref to the callback. This is how we get the callback updated
|
||||
// across the NodeJS<->Browser boundary.
|
||||
const callbackRef = useRef<{ fn: (path: Path) => void }>({
|
||||
fn: (_path) => {},
|
||||
const callbackRef = useRef<{ fn: (path: Path) => Promise<void> }>({
|
||||
fn: async (_path) => {},
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
@ -35,7 +36,9 @@ export const useFileSystemWatcher = (
|
||||
if (!isDesktop()) 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,
|
||||
// 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.
|
||||
@ -53,6 +59,8 @@ export const useFileSystemWatcher = (
|
||||
// The hook is useless on web.
|
||||
if (!isDesktop()) return
|
||||
|
||||
if (!hasDiff) return
|
||||
|
||||
const [pathsRemoved, pathsRemaining] = difference(
|
||||
dependencyArrayTracked,
|
||||
dependencyArray
|
||||
@ -62,10 +70,10 @@ export const useFileSystemWatcher = (
|
||||
}
|
||||
const [pathsAdded] = difference(dependencyArray, dependencyArrayTracked)
|
||||
for (let path of pathsAdded) {
|
||||
window.electron.watchFileOn(path, (_eventType: string, path: Path) =>
|
||||
callbackRef.current.fn(path)
|
||||
)
|
||||
window.electron.watchFileOn(path, (_eventType: string, path: Path) => {
|
||||
callbackRef.current.fn(path).catch(reportRejection)
|
||||
})
|
||||
}
|
||||
setDependencyArrayTracked(pathsRemaining.concat(pathsAdded))
|
||||
}, [difference(dependencyArray, dependencyArrayTracked)[0].length !== 0])
|
||||
}, [hasDiff])
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import ModalContainer from 'react-modal-promise'
|
||||
import { isDesktop } from 'lib/isDesktop'
|
||||
import { AppStreamProvider } from 'AppState'
|
||||
import { ToastUpdate } from 'components/ToastUpdate'
|
||||
import { AUTO_UPDATER_TOAST_ID } from 'lib/constants'
|
||||
|
||||
// uncomment for xstate inspector
|
||||
// import { DEV } from 'env'
|
||||
@ -53,7 +54,22 @@ root.render(
|
||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||
reportWebVitals()
|
||||
|
||||
isDesktop() &&
|
||||
if (isDesktop()) {
|
||||
// Listen for update download progress to begin
|
||||
// to show a loading toast.
|
||||
window.electron.onUpdateDownloadStart(() => {
|
||||
const message = `Downloading app update...`
|
||||
console.log(message)
|
||||
toast.loading(message, { id: AUTO_UPDATER_TOAST_ID })
|
||||
})
|
||||
// Listen for update download errors to show
|
||||
// an error toast and clear the loading toast.
|
||||
window.electron.onUpdateError(({ error }) => {
|
||||
console.error(error)
|
||||
toast.error('An error occurred while downloading the update.', {
|
||||
id: AUTO_UPDATER_TOAST_ID,
|
||||
})
|
||||
})
|
||||
window.electron.onUpdateDownloaded((version: string) => {
|
||||
const message = `A new update (${version}) was downloaded and will be available next time you open the app.`
|
||||
console.log(message)
|
||||
@ -64,6 +80,7 @@ isDesktop() &&
|
||||
window.electron.appRestart()
|
||||
},
|
||||
}),
|
||||
{ duration: 30000 }
|
||||
{ duration: 30000, id: AUTO_UPDATER_TOAST_ID }
|
||||
)
|
||||
})
|
||||
}
|
||||
|
@ -8,6 +8,8 @@ import { EXECUTE_AST_INTERRUPT_ERROR_MESSAGE } from 'lib/constants'
|
||||
|
||||
import {
|
||||
CallExpression,
|
||||
emptyExecState,
|
||||
ExecState,
|
||||
initPromise,
|
||||
parse,
|
||||
PathToNode,
|
||||
@ -42,6 +44,7 @@ export class KclManager {
|
||||
},
|
||||
digest: null,
|
||||
}
|
||||
private _execState: ExecState = emptyExecState()
|
||||
private _programMemory: ProgramMemory = ProgramMemory.empty()
|
||||
lastSuccessfulProgramMemory: ProgramMemory = ProgramMemory.empty()
|
||||
private _logs: string[] = []
|
||||
@ -72,11 +75,21 @@ export class KclManager {
|
||||
get 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._programMemoryCallBack(programMemory)
|
||||
}
|
||||
|
||||
set execState(execState) {
|
||||
this._execState = execState
|
||||
this.programMemory = execState.memory
|
||||
}
|
||||
|
||||
get execState() {
|
||||
return this._execState
|
||||
}
|
||||
|
||||
get logs() {
|
||||
return this._logs
|
||||
}
|
||||
@ -253,8 +266,9 @@ export class KclManager {
|
||||
// Make sure we clear before starting again. End session will do this.
|
||||
this.engineCommandManager?.endSession()
|
||||
await this.ensureWasmInit()
|
||||
const { logs, errors, programMemory, isInterrupted } = await executeAst({
|
||||
const { logs, errors, execState, isInterrupted } = await executeAst({
|
||||
ast,
|
||||
idGenerator: this.execState.idGenerator,
|
||||
engineCommandManager: this.engineCommandManager,
|
||||
})
|
||||
|
||||
@ -264,7 +278,7 @@ export class KclManager {
|
||||
this.lints = await lintAst({ ast: ast })
|
||||
|
||||
sceneInfra.modelingSend({ type: 'code edit during sketch' })
|
||||
defaultSelectionFilter(programMemory, this.engineCommandManager)
|
||||
defaultSelectionFilter(execState.memory, this.engineCommandManager)
|
||||
|
||||
if (args.zoomToFit) {
|
||||
let zoomObjectId: string | undefined = ''
|
||||
@ -282,6 +296,7 @@ export class KclManager {
|
||||
type: 'zoom_to_fit',
|
||||
object_ids: zoomObjectId ? [zoomObjectId] : [], // leave empty to zoom to all objects
|
||||
padding: 0.1, // padding around the objects
|
||||
animated: false, // don't animate the zoom for now
|
||||
},
|
||||
})
|
||||
}
|
||||
@ -294,12 +309,20 @@ export class KclManager {
|
||||
this._cancelTokens.delete(currentExecutionId)
|
||||
return
|
||||
}
|
||||
|
||||
// Exit sketch mode if the AST is empty
|
||||
if (this._isAstEmpty(ast)) {
|
||||
await this.disableSketchMode()
|
||||
}
|
||||
|
||||
this.logs = logs
|
||||
// Do not add the errors since the program was interrupted and the error is not a real KCL error
|
||||
this.addKclErrors(isInterrupted ? [] : errors)
|
||||
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) {
|
||||
this.lastSuccessfulProgramMemory = programMemory
|
||||
this.lastSuccessfulProgramMemory = execState.memory
|
||||
}
|
||||
this.ast = { ...ast }
|
||||
this._executeCallback()
|
||||
@ -337,17 +360,19 @@ export class KclManager {
|
||||
await codeManager.writeToFile()
|
||||
this._ast = { ...newAst }
|
||||
|
||||
const { logs, errors, programMemory } = await executeAst({
|
||||
const { logs, errors, execState } = await executeAst({
|
||||
ast: newAst,
|
||||
idGenerator: this.execState.idGenerator,
|
||||
engineCommandManager: this.engineCommandManager,
|
||||
useFakeExecutor: true,
|
||||
})
|
||||
|
||||
this._logs = logs
|
||||
this._kclErrors = errors
|
||||
this._programMemory = programMemory
|
||||
this._execState = execState
|
||||
this._programMemory = execState.memory
|
||||
if (!errors.length) {
|
||||
this.lastSuccessfulProgramMemory = programMemory
|
||||
this.lastSuccessfulProgramMemory = execState.memory
|
||||
}
|
||||
if (updates !== 'artifactRanges') return
|
||||
|
||||
@ -552,6 +577,24 @@ export class KclManager {
|
||||
defaultSelectionFilter() {
|
||||
defaultSelectionFilter(this.programMemory, this.engineCommandManager)
|
||||
}
|
||||
|
||||
/**
|
||||
* We can send a single command of 'enable_sketch_mode' or send this in a batched request.
|
||||
* When there is no code in the KCL editor we should be sending 'sketch_mode_disable' since any previous half finished
|
||||
* code could leave the state of the application in sketch mode on the engine side.
|
||||
*/
|
||||
async disableSketchMode() {
|
||||
await this.engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: { type: 'sketch_mode_disable' },
|
||||
})
|
||||
}
|
||||
|
||||
// Determines if there is no KCL code which means it is executing a blank KCL file
|
||||
_isAstEmpty(ast: Program) {
|
||||
return ast.start === 0 && ast.end === 0 && ast.body.length === 0
|
||||
}
|
||||
}
|
||||
|
||||
function defaultSelectionFilter(
|
||||
|
@ -14,9 +14,9 @@ const mySketch001 = startSketchOn('XY')
|
||||
|> lineTo([-1.59, -1.54], %)
|
||||
|> lineTo([0.46, -5.82], %)
|
||||
// |> rx(45, %)`
|
||||
const programMemory = await enginelessExecutor(parse(code))
|
||||
const execState = await enginelessExecutor(parse(code))
|
||||
// @ts-ignore
|
||||
const sketch001 = programMemory?.get('mySketch001')
|
||||
const sketch001 = execState.memory.get('mySketch001')
|
||||
expect(sketch001).toEqual({
|
||||
type: 'UserVal',
|
||||
__meta: [{ sourceRange: [46, 71] }],
|
||||
@ -68,9 +68,9 @@ const mySketch001 = startSketchOn('XY')
|
||||
|> lineTo([0.46, -5.82], %)
|
||||
// |> rx(45, %)
|
||||
|> extrude(2, %)`
|
||||
const programMemory = await enginelessExecutor(parse(code))
|
||||
const execState = await enginelessExecutor(parse(code))
|
||||
// @ts-ignore
|
||||
const sketch001 = programMemory?.get('mySketch001')
|
||||
const sketch001 = execState.memory.get('mySketch001')
|
||||
expect(sketch001).toEqual({
|
||||
type: 'Solid',
|
||||
id: expect.any(String),
|
||||
@ -148,9 +148,10 @@ const sk2 = startSketchOn('XY')
|
||||
|> extrude(2, %)
|
||||
|
||||
`
|
||||
const programMemory = await enginelessExecutor(parse(code))
|
||||
const execState = await enginelessExecutor(parse(code))
|
||||
const programMemory = execState.memory
|
||||
// @ts-ignore
|
||||
const geos = [programMemory?.get('theExtrude'), programMemory?.get('sk2')]
|
||||
const geos = [programMemory.get('theExtrude'), programMemory.get('sk2')]
|
||||
expect(geos).toEqual([
|
||||
{
|
||||
type: 'Solid',
|
||||
|
@ -443,6 +443,6 @@ async function exe(
|
||||
) {
|
||||
const ast = parse(code)
|
||||
|
||||
const result = await enginelessExecutor(ast, programMemory)
|
||||
return result
|
||||
const execState = await enginelessExecutor(ast, programMemory)
|
||||
return execState.memory
|
||||
}
|
||||
|
@ -4,11 +4,14 @@ import {
|
||||
ProgramMemory,
|
||||
programMemoryInit,
|
||||
kclLint,
|
||||
emptyExecState,
|
||||
ExecState,
|
||||
} from 'lang/wasm'
|
||||
import { enginelessExecutor } from 'lib/testHelpers'
|
||||
import { EngineCommandManager } from 'lang/std/engineConnection'
|
||||
import { KCLError } from 'lang/errors'
|
||||
import { Diagnostic } from '@codemirror/lint'
|
||||
import { IdGenerator } from 'wasm-lib/kcl/bindings/IdGenerator'
|
||||
|
||||
export type ToolTip =
|
||||
| 'lineTo'
|
||||
@ -47,16 +50,18 @@ export async function executeAst({
|
||||
engineCommandManager,
|
||||
useFakeExecutor = false,
|
||||
programMemoryOverride,
|
||||
idGenerator,
|
||||
}: {
|
||||
ast: Program
|
||||
engineCommandManager: EngineCommandManager
|
||||
useFakeExecutor?: boolean
|
||||
programMemoryOverride?: ProgramMemory
|
||||
idGenerator?: IdGenerator
|
||||
isInterrupted?: boolean
|
||||
}): Promise<{
|
||||
logs: string[]
|
||||
errors: KCLError[]
|
||||
programMemory: ProgramMemory
|
||||
execState: ExecState
|
||||
isInterrupted: boolean
|
||||
}> {
|
||||
try {
|
||||
@ -65,15 +70,21 @@ export async function executeAst({
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
engineCommandManager.startNewSession()
|
||||
}
|
||||
const programMemory = await (useFakeExecutor
|
||||
const execState = await (useFakeExecutor
|
||||
? enginelessExecutor(ast, programMemoryOverride || programMemoryInit())
|
||||
: _executor(ast, programMemoryInit(), engineCommandManager, false))
|
||||
: _executor(
|
||||
ast,
|
||||
programMemoryInit(),
|
||||
idGenerator,
|
||||
engineCommandManager,
|
||||
false
|
||||
))
|
||||
|
||||
await engineCommandManager.waitForAllCommands()
|
||||
return {
|
||||
logs: [],
|
||||
errors: [],
|
||||
programMemory,
|
||||
execState,
|
||||
isInterrupted: false,
|
||||
}
|
||||
} catch (e: any) {
|
||||
@ -89,7 +100,7 @@ export async function executeAst({
|
||||
return {
|
||||
errors: [e],
|
||||
logs: [],
|
||||
programMemory: ProgramMemory.empty(),
|
||||
execState: emptyExecState(),
|
||||
isInterrupted,
|
||||
}
|
||||
} else {
|
||||
@ -97,7 +108,7 @@ export async function executeAst({
|
||||
return {
|
||||
logs: [e],
|
||||
errors: [],
|
||||
programMemory: ProgramMemory.empty(),
|
||||
execState: emptyExecState(),
|
||||
isInterrupted,
|
||||
}
|
||||
}
|
||||
|
@ -220,11 +220,11 @@ yo2 = hmm([identifierGuy + 5])`
|
||||
it('should move a binary expression into a new variable', async () => {
|
||||
const ast = parse(code)
|
||||
if (err(ast)) throw ast
|
||||
const programMemory = await enginelessExecutor(ast)
|
||||
const execState = await enginelessExecutor(ast)
|
||||
const startIndex = code.indexOf('100 + 100') + 1
|
||||
const { modifiedAst } = moveValueIntoNewVariable(
|
||||
ast,
|
||||
programMemory,
|
||||
execState.memory,
|
||||
[startIndex, startIndex],
|
||||
'newVar'
|
||||
)
|
||||
@ -235,11 +235,11 @@ yo2 = hmm([identifierGuy + 5])`
|
||||
it('should move a value into a new variable', async () => {
|
||||
const ast = parse(code)
|
||||
if (err(ast)) throw ast
|
||||
const programMemory = await enginelessExecutor(ast)
|
||||
const execState = await enginelessExecutor(ast)
|
||||
const startIndex = code.indexOf('2.8') + 1
|
||||
const { modifiedAst } = moveValueIntoNewVariable(
|
||||
ast,
|
||||
programMemory,
|
||||
execState.memory,
|
||||
[startIndex, startIndex],
|
||||
'newVar'
|
||||
)
|
||||
@ -250,11 +250,11 @@ yo2 = hmm([identifierGuy + 5])`
|
||||
it('should move a callExpression into a new variable', async () => {
|
||||
const ast = parse(code)
|
||||
if (err(ast)) throw ast
|
||||
const programMemory = await enginelessExecutor(ast)
|
||||
const execState = await enginelessExecutor(ast)
|
||||
const startIndex = code.indexOf('def(')
|
||||
const { modifiedAst } = moveValueIntoNewVariable(
|
||||
ast,
|
||||
programMemory,
|
||||
execState.memory,
|
||||
[startIndex, startIndex],
|
||||
'newVar'
|
||||
)
|
||||
@ -265,11 +265,11 @@ yo2 = hmm([identifierGuy + 5])`
|
||||
it('should move a binary expression with call expression into a new variable', async () => {
|
||||
const ast = parse(code)
|
||||
if (err(ast)) throw ast
|
||||
const programMemory = await enginelessExecutor(ast)
|
||||
const execState = await enginelessExecutor(ast)
|
||||
const startIndex = code.indexOf('jkl(') + 1
|
||||
const { modifiedAst } = moveValueIntoNewVariable(
|
||||
ast,
|
||||
programMemory,
|
||||
execState.memory,
|
||||
[startIndex, startIndex],
|
||||
'newVar'
|
||||
)
|
||||
@ -280,11 +280,11 @@ yo2 = hmm([identifierGuy + 5])`
|
||||
it('should move a identifier into a new variable', async () => {
|
||||
const ast = parse(code)
|
||||
if (err(ast)) throw ast
|
||||
const programMemory = await enginelessExecutor(ast)
|
||||
const execState = await enginelessExecutor(ast)
|
||||
const startIndex = code.indexOf('identifierGuy +') + 1
|
||||
const { modifiedAst } = moveValueIntoNewVariable(
|
||||
ast,
|
||||
programMemory,
|
||||
execState.memory,
|
||||
[startIndex, startIndex],
|
||||
'newVar'
|
||||
)
|
||||
@ -465,7 +465,7 @@ describe('Testing deleteSegmentFromPipeExpression', () => {
|
||||
|> line([306.21, 198.87], %)`
|
||||
const ast = parse(code)
|
||||
if (err(ast)) throw ast
|
||||
const programMemory = await enginelessExecutor(ast)
|
||||
const execState = await enginelessExecutor(ast)
|
||||
const lineOfInterest = 'line([306.21, 198.85], %, $a)'
|
||||
const range: [number, number] = [
|
||||
code.indexOf(lineOfInterest),
|
||||
@ -475,7 +475,7 @@ describe('Testing deleteSegmentFromPipeExpression', () => {
|
||||
const modifiedAst = deleteSegmentFromPipeExpression(
|
||||
[],
|
||||
ast,
|
||||
programMemory,
|
||||
execState.memory,
|
||||
code,
|
||||
pathToNode
|
||||
)
|
||||
@ -543,7 +543,7 @@ ${!replace1 ? ` |> ${line}\n` : ''} |> angledLine([-65, ${
|
||||
const code = makeCode(line)
|
||||
const ast = parse(code)
|
||||
if (err(ast)) throw ast
|
||||
const programMemory = await enginelessExecutor(ast)
|
||||
const execState = await enginelessExecutor(ast)
|
||||
const lineOfInterest = line
|
||||
const range: [number, number] = [
|
||||
code.indexOf(lineOfInterest),
|
||||
@ -554,7 +554,7 @@ ${!replace1 ? ` |> ${line}\n` : ''} |> angledLine([-65, ${
|
||||
const modifiedAst = deleteSegmentFromPipeExpression(
|
||||
dependentSegments,
|
||||
ast,
|
||||
programMemory,
|
||||
execState.memory,
|
||||
code,
|
||||
pathToNode
|
||||
)
|
||||
@ -632,7 +632,7 @@ describe('Testing removeSingleConstraintInfo', () => {
|
||||
const ast = parse(code)
|
||||
if (err(ast)) throw ast
|
||||
|
||||
const programMemory = await enginelessExecutor(ast)
|
||||
const execState = await enginelessExecutor(ast)
|
||||
const lineOfInterest = expectedFinish.split('(')[0] + '('
|
||||
const range: [number, number] = [
|
||||
code.indexOf(lineOfInterest) + 1,
|
||||
@ -661,7 +661,7 @@ describe('Testing removeSingleConstraintInfo', () => {
|
||||
pathToNode,
|
||||
argPosition,
|
||||
ast,
|
||||
programMemory
|
||||
execState.memory
|
||||
)
|
||||
if (!mod) return new Error('mod is undefined')
|
||||
const recastCode = recast(mod.modifiedAst)
|
||||
@ -686,7 +686,7 @@ describe('Testing removeSingleConstraintInfo', () => {
|
||||
const ast = parse(code)
|
||||
if (err(ast)) throw ast
|
||||
|
||||
const programMemory = await enginelessExecutor(ast)
|
||||
const execState = await enginelessExecutor(ast)
|
||||
const lineOfInterest = expectedFinish.split('(')[0] + '('
|
||||
const range: [number, number] = [
|
||||
code.indexOf(lineOfInterest) + 1,
|
||||
@ -711,7 +711,7 @@ describe('Testing removeSingleConstraintInfo', () => {
|
||||
pathToNode,
|
||||
argPosition,
|
||||
ast,
|
||||
programMemory
|
||||
execState.memory
|
||||
)
|
||||
if (!mod) return new Error('mod is undefined')
|
||||
const recastCode = recast(mod.modifiedAst)
|
||||
@ -882,7 +882,7 @@ sketch002 = startSketchOn({
|
||||
// const lineOfInterest = 'line([-2.94, 2.7], %)'
|
||||
const ast = parse(codeBefore)
|
||||
if (err(ast)) throw ast
|
||||
const programMemory = await enginelessExecutor(ast)
|
||||
const execState = await enginelessExecutor(ast)
|
||||
|
||||
// deleteFromSelection
|
||||
const range: [number, number] = [
|
||||
@ -895,7 +895,7 @@ sketch002 = startSketchOn({
|
||||
range,
|
||||
type,
|
||||
},
|
||||
programMemory,
|
||||
execState.memory,
|
||||
async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
return {
|
||||
|
@ -45,11 +45,11 @@ variableBelowShouldNotBeIncluded = 3
|
||||
const rangeStart = code.indexOf('// selection-range-7ish-before-this') - 7
|
||||
const ast = parse(code)
|
||||
if (err(ast)) throw ast
|
||||
const programMemory = await enginelessExecutor(ast)
|
||||
const execState = await enginelessExecutor(ast)
|
||||
|
||||
const { variables, bodyPath, insertIndex } = findAllPreviousVariables(
|
||||
ast,
|
||||
programMemory,
|
||||
execState.memory,
|
||||
[rangeStart, rangeStart]
|
||||
)
|
||||
expect(variables).toEqual([
|
||||
@ -351,11 +351,11 @@ part001 = startSketchAt([-1.41, 3.46])
|
||||
const ast = parse(exampleCode)
|
||||
if (err(ast)) throw ast
|
||||
|
||||
const programMemory = await enginelessExecutor(ast)
|
||||
const execState = await enginelessExecutor(ast)
|
||||
const result = hasExtrudeSketch({
|
||||
ast,
|
||||
selection: { type: 'default', range: [100, 101] },
|
||||
programMemory,
|
||||
programMemory: execState.memory,
|
||||
})
|
||||
expect(result).toEqual(true)
|
||||
})
|
||||
@ -370,11 +370,11 @@ part001 = startSketchAt([-1.41, 3.46])
|
||||
const ast = parse(exampleCode)
|
||||
if (err(ast)) throw ast
|
||||
|
||||
const programMemory = await enginelessExecutor(ast)
|
||||
const execState = await enginelessExecutor(ast)
|
||||
const result = hasExtrudeSketch({
|
||||
ast,
|
||||
selection: { type: 'default', range: [100, 101] },
|
||||
programMemory,
|
||||
programMemory: execState.memory,
|
||||
})
|
||||
expect(result).toEqual(true)
|
||||
})
|
||||
@ -383,11 +383,11 @@ part001 = startSketchAt([-1.41, 3.46])
|
||||
const ast = parse(exampleCode)
|
||||
if (err(ast)) throw ast
|
||||
|
||||
const programMemory = await enginelessExecutor(ast)
|
||||
const execState = await enginelessExecutor(ast)
|
||||
const result = hasExtrudeSketch({
|
||||
ast,
|
||||
selection: { type: 'default', range: [10, 11] },
|
||||
programMemory,
|
||||
programMemory: execState.memory,
|
||||
})
|
||||
expect(result).toEqual(false)
|
||||
})
|
||||
|
@ -117,11 +117,11 @@ describe('testing changeSketchArguments', () => {
|
||||
const ast = parse(code)
|
||||
if (err(ast)) return ast
|
||||
|
||||
const programMemory = await enginelessExecutor(ast)
|
||||
const execState = await enginelessExecutor(ast)
|
||||
const sourceStart = code.indexOf(lineToChange)
|
||||
const changeSketchArgsRetVal = changeSketchArguments(
|
||||
ast,
|
||||
programMemory,
|
||||
execState.memory,
|
||||
{
|
||||
type: 'sourceRange',
|
||||
sourceRange: [sourceStart, sourceStart + lineToChange.length],
|
||||
@ -150,12 +150,12 @@ mySketch001 = startSketchOn('XY')
|
||||
const ast = parse(code)
|
||||
if (err(ast)) return ast
|
||||
|
||||
const programMemory = await enginelessExecutor(ast)
|
||||
const execState = await enginelessExecutor(ast)
|
||||
const sourceStart = code.indexOf(lineToChange)
|
||||
expect(sourceStart).toBe(89)
|
||||
const newSketchLnRetVal = addNewSketchLn({
|
||||
node: ast,
|
||||
programMemory,
|
||||
programMemory: execState.memory,
|
||||
input: {
|
||||
type: 'straight-segment',
|
||||
from: [0, 0],
|
||||
@ -186,7 +186,7 @@ mySketch001 = startSketchOn('XY')
|
||||
|
||||
const modifiedAst2 = addCloseToPipe({
|
||||
node: ast,
|
||||
programMemory,
|
||||
programMemory: execState.memory,
|
||||
pathToNode: [
|
||||
['body', ''],
|
||||
[0, 'index'],
|
||||
@ -230,7 +230,7 @@ describe('testing addTagForSketchOnFace', () => {
|
||||
const pathToNode = getNodePathFromSourceRange(ast, sourceRange)
|
||||
const sketchOnFaceRetVal = addTagForSketchOnFace(
|
||||
{
|
||||
// previousProgramMemory: programMemory, // redundant?
|
||||
// previousProgramMemory: execState.memory, // redundant?
|
||||
pathToNode,
|
||||
node: ast,
|
||||
},
|
||||
|
@ -34,7 +34,7 @@ async function testingSwapSketchFnCall({
|
||||
const ast = parse(inputCode)
|
||||
if (err(ast)) return Promise.reject(ast)
|
||||
|
||||
const programMemory = await enginelessExecutor(ast)
|
||||
const execState = await enginelessExecutor(ast)
|
||||
const selections = {
|
||||
codeBasedSelections: [range],
|
||||
otherSelections: [],
|
||||
@ -45,7 +45,7 @@ async function testingSwapSketchFnCall({
|
||||
return Promise.reject(new Error('transformInfos undefined'))
|
||||
const ast2 = transformAstSketchLines({
|
||||
ast,
|
||||
programMemory,
|
||||
programMemory: execState.memory,
|
||||
selectionRanges: selections,
|
||||
transformInfos,
|
||||
referenceSegName: '',
|
||||
@ -360,10 +360,10 @@ part001 = startSketchOn('XY')
|
||||
|> line([2.14, 1.35], %) // normal-segment
|
||||
|> xLine(3.54, %)`
|
||||
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 sg = sketchFromKclValue(
|
||||
programMemory.get('part001'),
|
||||
execState.memory.get('part001'),
|
||||
'part001'
|
||||
) as Sketch
|
||||
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 () => {
|
||||
const programMemory = await enginelessExecutor(parse(code))
|
||||
const execState = await enginelessExecutor(parse(code))
|
||||
const index = code.indexOf('// segment-in-start') - 7
|
||||
const _segment = getSketchSegmentFromSourceRange(
|
||||
sketchFromKclValue(programMemory.get('part001'), 'part001') as Sketch,
|
||||
sketchFromKclValue(execState.memory.get('part001'), 'part001') as Sketch,
|
||||
[index, index]
|
||||
)
|
||||
if (err(_segment)) throw _segment
|
||||
|
@ -9,7 +9,7 @@ import {
|
||||
getConstraintLevelFromSourceRange,
|
||||
} from './sketchcombos'
|
||||
import { ToolTip } from 'lang/langHelpers'
|
||||
import { Selections } from 'lib/selections'
|
||||
import { Selection, Selections } from 'lib/selections'
|
||||
import { err } from 'lib/trap'
|
||||
import { enginelessExecutor } from '../../lib/testHelpers'
|
||||
|
||||
@ -96,6 +96,86 @@ function makeSelections(
|
||||
}
|
||||
|
||||
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
|
||||
myVar2 = 5
|
||||
myVar3 = 6
|
||||
@ -220,7 +300,7 @@ part001 = startSketchOn('XY')
|
||||
}
|
||||
})
|
||||
|
||||
const programMemory = await enginelessExecutor(ast)
|
||||
const execState = await enginelessExecutor(ast)
|
||||
const transformInfos = getTransformInfos(
|
||||
makeSelections(selectionRanges.slice(1)),
|
||||
ast,
|
||||
@ -231,7 +311,7 @@ part001 = startSketchOn('XY')
|
||||
ast,
|
||||
selectionRanges: makeSelections(selectionRanges),
|
||||
transformInfos,
|
||||
programMemory,
|
||||
programMemory: execState.memory,
|
||||
})
|
||||
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(
|
||||
makeSelections(selectionRanges),
|
||||
ast,
|
||||
@ -322,7 +402,7 @@ part001 = startSketchOn('XY')
|
||||
ast,
|
||||
selectionRanges: makeSelections(selectionRanges),
|
||||
transformInfos,
|
||||
programMemory,
|
||||
programMemory: execState.memory,
|
||||
referenceSegName: '',
|
||||
})
|
||||
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(
|
||||
makeSelections(selectionRanges),
|
||||
ast,
|
||||
@ -384,7 +464,7 @@ part001 = startSketchOn('XY')
|
||||
ast,
|
||||
selectionRanges: makeSelections(selectionRanges),
|
||||
transformInfos,
|
||||
programMemory,
|
||||
programMemory: execState.memory,
|
||||
referenceSegName: '',
|
||||
})
|
||||
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(
|
||||
makeSelections(selectionRanges.slice(1)),
|
||||
ast,
|
||||
@ -481,7 +561,7 @@ async function helperThing(
|
||||
ast,
|
||||
selectionRanges: makeSelections(selectionRanges),
|
||||
transformInfos,
|
||||
programMemory,
|
||||
programMemory: execState.memory,
|
||||
})
|
||||
|
||||
if (err(newAst)) return Promise.reject(newAst)
|
||||
|
@ -1559,7 +1559,15 @@ export function transformSecondarySketchLinesTagFirst({
|
||||
}
|
||||
| Error {
|
||||
// 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)
|
||||
if (err(_tag)) return _tag
|
||||
@ -1569,7 +1577,7 @@ export function transformSecondarySketchLinesTagFirst({
|
||||
ast: modifiedAst,
|
||||
selectionRanges: {
|
||||
...selectionRanges,
|
||||
codeBasedSelections: selectionRanges.codeBasedSelections.slice(1),
|
||||
codeBasedSelections: secondarySelections,
|
||||
},
|
||||
referencedSegmentRange: primarySelection,
|
||||
transformInfos,
|
||||
|
@ -17,9 +17,9 @@ describe('testing angledLineThatIntersects', () => {
|
||||
offset: ${offset},
|
||||
}, %, $yo2)
|
||||
intersect = segEndX(yo2)`
|
||||
const mem = await enginelessExecutor(parse(code('-1')))
|
||||
expect(mem.get('intersect')?.value).toBe(1 + Math.sqrt(2))
|
||||
const execState = await enginelessExecutor(parse(code('-1')))
|
||||
expect(execState.memory.get('intersect')?.value).toBe(1 + Math.sqrt(2))
|
||||
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 { ProjectConfiguration } from 'wasm-lib/kcl/bindings/ProjectConfiguration'
|
||||
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 { 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][]
|
||||
|
||||
interface Memory {
|
||||
[key: string]: KclValue
|
||||
export interface ExecState {
|
||||
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
|
||||
|
||||
interface Environment {
|
||||
bindings: Memory
|
||||
parent: EnvironmentRef | null
|
||||
}
|
||||
|
||||
function emptyEnvironment(): Environment {
|
||||
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
|
||||
* 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) {
|
||||
const env = this.environments[envRef]
|
||||
if (env.bindings.hasOwnProperty(name)) {
|
||||
return env.bindings[name]
|
||||
return env.bindings[name] ?? null
|
||||
}
|
||||
if (!env.parent) {
|
||||
break
|
||||
@ -260,6 +282,7 @@ export class ProgramMemory {
|
||||
}
|
||||
|
||||
for (const [name, value] of Object.entries(env.bindings)) {
|
||||
if (value === undefined) continue
|
||||
// Check the predicate.
|
||||
if (!predicate(value)) {
|
||||
continue
|
||||
@ -293,6 +316,7 @@ export class ProgramMemory {
|
||||
while (true) {
|
||||
const env = this.environments[envRef]
|
||||
for (const [name, value] of Object.entries(env.bindings)) {
|
||||
if (value === undefined) continue
|
||||
// Don't include shadowed variables.
|
||||
if (!map.has(name)) {
|
||||
map.set(name, value)
|
||||
@ -356,9 +380,10 @@ export function sketchFromKclValue(
|
||||
export const executor = async (
|
||||
node: Program,
|
||||
programMemory: ProgramMemory | Error = ProgramMemory.empty(),
|
||||
idGenerator: IdGenerator = defaultIdGenerator(),
|
||||
engineCommandManager: EngineCommandManager,
|
||||
isMock: boolean = false
|
||||
): Promise<ProgramMemory> => {
|
||||
): Promise<ExecState> => {
|
||||
if (err(programMemory)) return Promise.reject(programMemory)
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
@ -366,6 +391,7 @@ export const executor = async (
|
||||
const _programMemory = await _executor(
|
||||
node,
|
||||
programMemory,
|
||||
idGenerator,
|
||||
engineCommandManager,
|
||||
isMock
|
||||
)
|
||||
@ -378,9 +404,10 @@ export const executor = async (
|
||||
export const _executor = async (
|
||||
node: Program,
|
||||
programMemory: ProgramMemory | Error = ProgramMemory.empty(),
|
||||
idGenerator: IdGenerator = defaultIdGenerator(),
|
||||
engineCommandManager: EngineCommandManager,
|
||||
isMock: boolean
|
||||
): Promise<ProgramMemory> => {
|
||||
): Promise<ExecState> => {
|
||||
if (err(programMemory)) return Promise.reject(programMemory)
|
||||
|
||||
try {
|
||||
@ -392,15 +419,16 @@ export const _executor = async (
|
||||
baseUnit =
|
||||
(await getSettingsState)()?.modeling.defaultUnit.current || 'mm'
|
||||
}
|
||||
const memory: RawProgramMemory = await execute_wasm(
|
||||
const execState: RawExecState = await execute_wasm(
|
||||
JSON.stringify(node),
|
||||
JSON.stringify(programMemory.toRaw()),
|
||||
JSON.stringify(idGenerator),
|
||||
baseUnit,
|
||||
engineCommandManager,
|
||||
fileSystemManager,
|
||||
isMock
|
||||
)
|
||||
return ProgramMemory.fromRaw(memory)
|
||||
return execStateFromRaw(execState)
|
||||
} catch (e: any) {
|
||||
console.log(e)
|
||||
const parsed: RustKclError = JSON.parse(e.toString())
|
||||
|
@ -281,6 +281,8 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
||||
multiple: true,
|
||||
required: true,
|
||||
skip: false,
|
||||
warningMessage:
|
||||
'Fillets cannot touch other fillets yet. This is under development.',
|
||||
},
|
||||
radius: {
|
||||
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
|
||||
machineContext?: C
|
||||
) => boolean)
|
||||
warningMessage?: string
|
||||
skip?: boolean
|
||||
/** For showing a summary display of the current value, such as in
|
||||
* the command bar's header
|
||||
@ -189,6 +190,7 @@ export type CommandArgument<
|
||||
) => boolean)
|
||||
skip?: boolean
|
||||
machineActor?: Actor<T>
|
||||
warningMessage?: string
|
||||
/** For showing a summary display of the current value, such as in
|
||||
* 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',
|
||||
localFallback: '/kcl-samples-manifest-fallback.json',
|
||||
} 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,
|
||||
machineActor,
|
||||
valueSummary: arg.valueSummary,
|
||||
warningMessage: arg.warningMessage ?? '',
|
||||
} satisfies Omit<CommandArgument<O, T>, 'inputType'>
|
||||
|
||||
if (arg.inputType === 'options') {
|
||||
|
@ -379,7 +379,7 @@ const getAppFolderName = () => {
|
||||
return window.electron.packageJson.name
|
||||
}
|
||||
|
||||
const getAppSettingsFilePath = async () => {
|
||||
export const getAppSettingsFilePath = async () => {
|
||||
const isTestEnv = window.electron.process.env.IS_PLAYWRIGHT === 'true'
|
||||
const testSettingsPath = window.electron.process.env.TEST_SETTINGS_FILE_KEY
|
||||
const appConfig = await window.electron.getPath('appData')
|
||||
|
13
src/lib/machine-api.d.ts
vendored
@ -126,6 +126,8 @@ export interface components {
|
||||
*
|
||||
* What "close" means is up to you! */
|
||||
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. */
|
||||
MachineMakeModel: {
|
||||
@ -136,6 +138,17 @@ export interface components {
|
||||
/** @description The unique serial number of the connected Machine. */
|
||||
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. */
|
||||
MachineType: 'Stereolithography' | 'FusedDeposition' | 'Cnc'
|
||||
/** @description The response from the `/ping` endpoint. */
|
||||
|
@ -145,6 +145,13 @@ export const interactionMap: Record<
|
||||
description:
|
||||
'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': [
|
||||
{
|
||||
|
@ -177,14 +177,14 @@ export async function loadAndValidateSettings(
|
||||
|
||||
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
|
||||
if (onDesktop) {
|
||||
settings.app.projectDirectory.default = await getInitialDefaultDir()
|
||||
}
|
||||
|
||||
setSettingsAtLevel(
|
||||
settings,
|
||||
settingsNext = setSettingsAtLevel(
|
||||
settingsNext,
|
||||
'user',
|
||||
configurationToSettingsPayload(appSettingsPayload)
|
||||
)
|
||||
@ -199,8 +199,8 @@ export async function loadAndValidateSettings(
|
||||
return Promise.reject(new Error('Invalid project settings'))
|
||||
|
||||
const projectSettingsPayload = projectSettings
|
||||
setSettingsAtLevel(
|
||||
settings,
|
||||
settingsNext = setSettingsAtLevel(
|
||||
settingsNext,
|
||||
'project',
|
||||
projectConfigurationToSettingsPayload(projectSettingsPayload)
|
||||
)
|
||||
@ -208,7 +208,7 @@ export async function loadAndValidateSettings(
|
||||
|
||||
// Return the settings object
|
||||
return {
|
||||
settings,
|
||||
settings: settingsNext,
|
||||
configuration: appSettingsPayload,
|
||||
}
|
||||
}
|
||||
|
@ -49,6 +49,7 @@ if (typeof window !== 'undefined') {
|
||||
type: 'zoom_to_fit',
|
||||
object_ids: [], // leave empty to zoom to all objects
|
||||
padding: 0.2, // padding around the objects
|
||||
animated: false, // don't animate the zoom for now
|
||||
},
|
||||
})
|
||||
}
|
||||
|
@ -1,4 +1,11 @@
|
||||
import { Program, ProgramMemory, _executor, SourceRange } from '../lang/wasm'
|
||||
import {
|
||||
Program,
|
||||
ProgramMemory,
|
||||
_executor,
|
||||
SourceRange,
|
||||
ExecState,
|
||||
defaultIdGenerator,
|
||||
} from '../lang/wasm'
|
||||
import {
|
||||
EngineCommandManager,
|
||||
EngineCommandManagerEvents,
|
||||
@ -9,6 +16,7 @@ import { v4 as uuidv4 } from 'uuid'
|
||||
import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
|
||||
import { err, reportRejection } from 'lib/trap'
|
||||
import { toSync } from './utils'
|
||||
import { IdGenerator } from 'wasm-lib/kcl/bindings/IdGenerator'
|
||||
|
||||
type WebSocketResponse = Models['WebSocketResponse_type']
|
||||
|
||||
@ -77,8 +85,9 @@ class MockEngineCommandManager {
|
||||
|
||||
export async function enginelessExecutor(
|
||||
ast: Program | Error,
|
||||
pm: ProgramMemory | Error = ProgramMemory.empty()
|
||||
): Promise<ProgramMemory> {
|
||||
pm: ProgramMemory | Error = ProgramMemory.empty(),
|
||||
idGenerator: IdGenerator = defaultIdGenerator()
|
||||
): Promise<ExecState> {
|
||||
if (err(ast)) return Promise.reject(ast)
|
||||
if (err(pm)) return Promise.reject(pm)
|
||||
|
||||
@ -88,15 +97,22 @@ export async function enginelessExecutor(
|
||||
}) as any as EngineCommandManager
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
mockEngineCommandManager.startNewSession()
|
||||
const programMemory = await _executor(ast, pm, mockEngineCommandManager, true)
|
||||
const execState = await _executor(
|
||||
ast,
|
||||
pm,
|
||||
idGenerator,
|
||||
mockEngineCommandManager,
|
||||
true
|
||||
)
|
||||
await mockEngineCommandManager.waitForAllCommands()
|
||||
return programMemory
|
||||
return execState
|
||||
}
|
||||
|
||||
export async function executor(
|
||||
ast: Program,
|
||||
pm: ProgramMemory = ProgramMemory.empty()
|
||||
): Promise<ProgramMemory> {
|
||||
pm: ProgramMemory = ProgramMemory.empty(),
|
||||
idGenerator: IdGenerator = defaultIdGenerator()
|
||||
): Promise<ExecState> {
|
||||
const engineCommandManager = new EngineCommandManager()
|
||||
engineCommandManager.start({
|
||||
setIsStreamReady: () => {},
|
||||
@ -117,14 +133,15 @@ export async function executor(
|
||||
toSync(async () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
engineCommandManager.startNewSession()
|
||||
const programMemory = await _executor(
|
||||
const execState = await _executor(
|
||||
ast,
|
||||
pm,
|
||||
idGenerator,
|
||||
engineCommandManager,
|
||||
false
|
||||
)
|
||||
await engineCommandManager.waitForAllCommands()
|
||||
resolve(programMemory)
|
||||
resolve(execState)
|
||||
}, reportRejection)
|
||||
)
|
||||
})
|
||||
|
@ -249,7 +249,7 @@ export async function submitAndAwaitTextToKcl({
|
||||
|
||||
export async function sendTelemetry(
|
||||
id: string,
|
||||
feedback: Models['AiFeedback_type'],
|
||||
feedback: Models['MlFeedback_type'],
|
||||
token?: string
|
||||
): Promise<void> {
|
||||
const url =
|
||||
|
@ -295,15 +295,24 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
||||
'break',
|
||||
{
|
||||
id: 'line',
|
||||
onClick: ({ modelingState, modelingSend }) =>
|
||||
modelingSend({
|
||||
type: 'change tool',
|
||||
data: {
|
||||
tool: !modelingState.matches({ Sketch: 'Line tool' })
|
||||
? 'line'
|
||||
: 'none',
|
||||
},
|
||||
}),
|
||||
onClick: ({ modelingState, modelingSend }) => {
|
||||
if (modelingState.matches({ Sketch: { 'Line tool': 'No Points' } })) {
|
||||
// Exit the sketch state if there are no points and they press ESC
|
||||
modelingSend({
|
||||
type: 'Cancel',
|
||||
})
|
||||
} else {
|
||||
// Exit the tool if there are points and they press ESC
|
||||
modelingSend({
|
||||
type: 'change tool',
|
||||
data: {
|
||||
tool: !modelingState.matches({ Sketch: 'Line tool' })
|
||||
? 'line'
|
||||
: 'none',
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
icon: 'line',
|
||||
status: 'available',
|
||||
disabled: (state) =>
|
||||
|
@ -97,7 +97,7 @@ export function useCalculateKclExpression({
|
||||
})
|
||||
if (trap(error, { suppress: true })) return
|
||||
}
|
||||
const { programMemory } = await executeAst({
|
||||
const { execState } = await executeAst({
|
||||
ast,
|
||||
engineCommandManager,
|
||||
useFakeExecutor: true,
|
||||
@ -111,7 +111,7 @@ export function useCalculateKclExpression({
|
||||
const init =
|
||||
resultDeclaration?.type === 'VariableDeclaration' &&
|
||||
resultDeclaration?.declarations?.[0]?.init
|
||||
const result = programMemory?.get('__result__')?.value
|
||||
const result = execState.memory?.get('__result__')?.value
|
||||
setCalcResult(typeof result === 'number' ? String(result) : 'NAN')
|
||||
init && setValueNode(init)
|
||||
}
|
||||
|
@ -252,6 +252,9 @@ export type ModelingMachineEvent =
|
||||
type: 'Set Segment Overlays'
|
||||
data: SegmentOverlayPayload
|
||||
}
|
||||
| {
|
||||
type: 'Center camera on selection'
|
||||
}
|
||||
| {
|
||||
type: 'Delete segment'
|
||||
data: PathToNode
|
||||
@ -663,6 +666,7 @@ export const modelingMachine = setup({
|
||||
|
||||
const testExecute = await executeAst({
|
||||
ast: modifiedAst,
|
||||
idGenerator: kclManager.execState.idGenerator,
|
||||
useFakeExecutor: true,
|
||||
engineCommandManager,
|
||||
})
|
||||
@ -938,6 +942,7 @@ export const modelingMachine = setup({
|
||||
'Set selection': () => {},
|
||||
'Set mouse state': () => {},
|
||||
'Set Segment Overlays': () => {},
|
||||
'Center camera on selection': () => {},
|
||||
'Engine export': () => {},
|
||||
'Submit to Text-to-CAD API': () => {},
|
||||
'Set sketchDetails': () => {},
|
||||
@ -2105,6 +2110,10 @@ export const modelingMachine = setup({
|
||||
reenter: false,
|
||||
actions: 'Set Segment Overlays',
|
||||
},
|
||||
'Center camera on selection': {
|
||||
reenter: false,
|
||||
actions: 'Center camera on selection',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -19,7 +19,7 @@ export const settingsMachine = setup({
|
||||
types: {
|
||||
context: {} as ReturnType<typeof createSettings>,
|
||||
input: {} as ReturnType<typeof createSettings>,
|
||||
events: {} as
|
||||
events: {} as (
|
||||
| WildcardSetEvent<SettingsPaths>
|
||||
| SetEventTypes
|
||||
| {
|
||||
@ -34,7 +34,8 @@ export const settingsMachine = setup({
|
||||
type: 'Reset settings'
|
||||
level: SettingsLevel
|
||||
}
|
||||
| { type: 'Set all settings'; settings: typeof settings },
|
||||
| { type: 'Set all settings'; settings: typeof settings }
|
||||
) & { doNotPersist?: boolean },
|
||||
},
|
||||
actions: {
|
||||
setEngineTheme: () => {},
|
||||
|
20
src/main.ts
@ -261,10 +261,30 @@ app.on('ready', () => {
|
||||
autoUpdater.checkForUpdates().catch(reportRejection)
|
||||
}, fifteenMinutes)
|
||||
|
||||
autoUpdater.on('error', (error) => {
|
||||
console.error('updater-error', error)
|
||||
mainWindow?.webContents.send('updater-error', error)
|
||||
})
|
||||
|
||||
autoUpdater.on('update-available', (info) => {
|
||||
console.log('update-available', info)
|
||||
})
|
||||
|
||||
autoUpdater.prependOnceListener('download-progress', (progress) => {
|
||||
// For now, we'll send nothing and just start a loading spinner.
|
||||
// See below for a TODO to send progress data to the renderer.
|
||||
console.log('update-download-start', {
|
||||
version: '',
|
||||
})
|
||||
mainWindow?.webContents.send('update-download-start', progress)
|
||||
})
|
||||
|
||||
autoUpdater.on('download-progress', (progress) => {
|
||||
// TODO: in a future PR (https://github.com/KittyCAD/modeling-app/issues/3994)
|
||||
// send this data to mainWindow to show a progress bar for the download.
|
||||
console.log('download-progress', progress)
|
||||
})
|
||||
|
||||
autoUpdater.on('update-downloaded', (info) => {
|
||||
console.log('update-downloaded', info)
|
||||
mainWindow?.webContents.send('update-downloaded', info.version)
|
||||
|
@ -5,6 +5,7 @@ import os from 'node:os'
|
||||
import fsSync from 'node:fs'
|
||||
import packageJson from '../package.json'
|
||||
import { MachinesListing } from 'lib/machineManager'
|
||||
import chokidar from 'chokidar'
|
||||
|
||||
const open = (args: any) => ipcRenderer.invoke('dialog.showOpenDialog', args)
|
||||
const save = (args: any) => ipcRenderer.invoke('dialog.showSaveDialog', args)
|
||||
@ -15,44 +16,34 @@ const startDeviceFlow = (host: string): Promise<string> =>
|
||||
ipcRenderer.invoke('startDeviceFlow', host)
|
||||
const loginWithDeviceFlow = (): Promise<string> =>
|
||||
ipcRenderer.invoke('loginWithDeviceFlow')
|
||||
const onUpdateDownloadStart = (
|
||||
callback: (value: { version: string }) => void
|
||||
) => ipcRenderer.on('update-download-start', (_event, value) => callback(value))
|
||||
const onUpdateDownloaded = (callback: (value: string) => void) =>
|
||||
ipcRenderer.on('update-downloaded', (_event, value) => callback(value))
|
||||
const onUpdateError = (callback: (value: Error) => void) =>
|
||||
ipcRenderer.on('update-error', (_event, value) => callback(value))
|
||||
const appRestart = () => ipcRenderer.invoke('app.restart')
|
||||
|
||||
const isMac = os.platform() === 'darwin'
|
||||
const isWindows = os.platform() === 'win32'
|
||||
const isLinux = os.platform() === 'linux'
|
||||
|
||||
let fsWatchListeners = new Map<
|
||||
string,
|
||||
{
|
||||
watcher: fsSync.FSWatcher
|
||||
callback: (eventType: string, path: string) => void
|
||||
}
|
||||
>()
|
||||
let fsWatchListeners = new Map<string, ReturnType<typeof chokidar.watch>>()
|
||||
|
||||
const watchFileOn = (
|
||||
path: string,
|
||||
callback: (eventType: string, path: string) => void
|
||||
) => {
|
||||
const watcher = fsSync.watch(path)
|
||||
watcher.on('change', callback)
|
||||
fsWatchListeners.set(path, { watcher, callback })
|
||||
const watchFileOn = (path: string, callback: (path: string) => void) => {
|
||||
const watcherMaybe = fsWatchListeners.get(path)
|
||||
if (watcherMaybe) return
|
||||
const watcher = chokidar.watch(path)
|
||||
watcher.on('all', callback)
|
||||
fsWatchListeners.set(path, watcher)
|
||||
}
|
||||
const watchFileOff = (path: string) => {
|
||||
const entry = fsWatchListeners.get(path)
|
||||
if (!entry) return
|
||||
const { watcher, callback } = entry
|
||||
watcher.off('change', callback)
|
||||
watcher.close()
|
||||
const watcher = fsWatchListeners.get(path)
|
||||
if (!watcher) return
|
||||
watcher.unwatch(path)
|
||||
fsWatchListeners.delete(path)
|
||||
}
|
||||
const watchFileObliterate = () => {
|
||||
for (let [pathAsKey] of fsWatchListeners) {
|
||||
watchFileOff(pathAsKey)
|
||||
}
|
||||
fsWatchListeners = new Map()
|
||||
}
|
||||
const readFile = (path: string) => fs.readFile(path, 'utf-8')
|
||||
// It seems like from the node source code this does not actually block but also
|
||||
// don't trust me on that (jess).
|
||||
@ -103,7 +94,6 @@ contextBridge.exposeInMainWorld('electron', {
|
||||
// exported.
|
||||
watchFileOn,
|
||||
watchFileOff,
|
||||
watchFileObliterate,
|
||||
readFile,
|
||||
writeFile,
|
||||
exists,
|
||||
@ -159,6 +149,8 @@ contextBridge.exposeInMainWorld('electron', {
|
||||
kittycad,
|
||||
listMachines,
|
||||
getMachineApiIp,
|
||||
onUpdateDownloadStart,
|
||||
onUpdateDownloaded,
|
||||
onUpdateError,
|
||||
appRestart,
|
||||
})
|
||||
|
@ -176,7 +176,7 @@ const Home = () => {
|
||||
|
||||
// Re-read projects listing if the projectDir has any updates.
|
||||
useFileSystemWatcher(
|
||||
() => {
|
||||
async () => {
|
||||
setProjectsLoaderTrigger(projectsLoaderTrigger + 1)
|
||||
},
|
||||
projectsDir ? [projectsDir] : []
|
||||
|
81
src/wasm-lib/Cargo.lock
generated
@ -434,9 +434,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.19"
|
||||
version = "4.5.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7be5744db7978a28d9df86a214130d106a89ce49644cbc4e3f0c22c3fba30615"
|
||||
checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@ -444,9 +444,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.19"
|
||||
version = "4.5.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a5fbc17d3ef8278f55b282b2a2e75ae6f6c7d4bb70ed3d0382375104bfafdb4b"
|
||||
checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@ -934,9 +934,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.30"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0"
|
||||
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
@ -949,9 +949,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.30"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
|
||||
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
@ -959,15 +959,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.30"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
|
||||
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
|
||||
|
||||
[[package]]
|
||||
name = "futures-executor"
|
||||
version = "0.3.30"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d"
|
||||
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
@ -976,15 +976,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-io"
|
||||
version = "0.3.30"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
|
||||
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.30"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
|
||||
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -993,21 +993,21 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.30"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"
|
||||
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.30"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
|
||||
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.30"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
|
||||
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
@ -1533,16 +1533,16 @@ checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.70"
|
||||
version = "0.3.71"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a"
|
||||
checksum = "0cb94a0ffd3f3ee755c20f7d8752f45cac88605a4dcf808abcff72873296ec7b"
|
||||
dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kcl-lib"
|
||||
version = "0.2.20"
|
||||
version = "0.2.21"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"approx 0.5.1",
|
||||
@ -1966,12 +1966,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.20.1"
|
||||
version = "1.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82881c4be219ab5faaf2ad5e5e5ecdff8c66bd7402ca3160975c93b24961afd1"
|
||||
dependencies = [
|
||||
"portable-atomic",
|
||||
]
|
||||
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
|
||||
|
||||
[[package]]
|
||||
name = "oncemutex"
|
||||
@ -3910,9 +3907,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.93"
|
||||
version = "0.2.94"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5"
|
||||
checksum = "ef073ced962d62984fb38a36e5fdc1a2b23c9e0e1fa0689bb97afa4202ef6887"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
@ -3921,9 +3918,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-backend"
|
||||
version = "0.2.93"
|
||||
version = "0.2.94"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b"
|
||||
checksum = "c4bfab14ef75323f4eb75fa52ee0a3fb59611977fd3240da19b2cf36ff85030e"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"log",
|
||||
@ -3936,9 +3933,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-futures"
|
||||
version = "0.4.43"
|
||||
version = "0.4.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed"
|
||||
checksum = "65471f79c1022ffa5291d33520cbbb53b7687b01c2f8e83b57d102eed7ed479d"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"futures-core",
|
||||
@ -3949,9 +3946,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.93"
|
||||
version = "0.2.94"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf"
|
||||
checksum = "a7bec9830f60924d9ceb3ef99d55c155be8afa76954edffbb5936ff4509474e7"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
@ -3959,9 +3956,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.93"
|
||||
version = "0.2.94"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836"
|
||||
checksum = "4c74f6e152a76a2ad448e223b0fc0b6b5747649c3d769cc6bf45737bf97d0ed6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -3972,9 +3969,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.93"
|
||||
version = "0.2.94"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484"
|
||||
checksum = "a42f6c679374623f295a8623adfe63d9284091245c3504bde47c17a3ce2777d9"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-lib"
|
||||
|
@ -20,7 +20,7 @@ tokio = { version = "1.40.0", features = ["sync"] }
|
||||
toml = "0.8.19"
|
||||
uuid = { version = "1.10.0", features = ["v4", "js", "serde"] }
|
||||
wasm-bindgen = "0.2.91"
|
||||
wasm-bindgen-futures = "0.4.42"
|
||||
wasm-bindgen-futures = "0.4.44"
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = "1"
|
||||
@ -35,10 +35,10 @@ uuid = { version = "1.10.0", features = ["v4", "js", "serde"] }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
console_error_panic_hook = "0.1.7"
|
||||
futures = "0.3.30"
|
||||
js-sys = "0.3.69"
|
||||
futures = "0.3.31"
|
||||
js-sys = "0.3.71"
|
||||
tower-lsp = { version = "0.20.0", default-features = false, features = ["runtime-agnostic"] }
|
||||
wasm-bindgen-futures = { version = "0.4.41", features = ["futures-core-03-stream"] }
|
||||
wasm-bindgen-futures = { version = "0.4.44", features = ["futures-core-03-stream"] }
|
||||
wasm-streams = "0.4.1"
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies.web-sys]
|
||||
|
@ -14,7 +14,7 @@ proc-macro = true
|
||||
[dependencies]
|
||||
Inflector = "0.11.4"
|
||||
convert_case = "0.6.0"
|
||||
once_cell = "1.19.0"
|
||||
once_cell = "1.20.2"
|
||||
proc-macro2 = "1"
|
||||
quote = "1"
|
||||
regex = "1.10"
|
||||
|
@ -753,6 +753,7 @@ fn generate_code_block_test(fn_name: &str, code_block: &str, index: usize) -> pr
|
||||
let tokens = crate::token::lexer(#code_block).unwrap();
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let program = parser.ast().unwrap();
|
||||
let id_generator = crate::executor::IdGenerator::default();
|
||||
let ctx = crate::executor::ExecutorContext {
|
||||
engine: std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().await.unwrap())),
|
||||
fs: std::sync::Arc::new(crate::fs::FileManager::new()),
|
||||
@ -761,7 +762,7 @@ fn generate_code_block_test(fn_name: &str, code_block: &str, index: usize) -> pr
|
||||
context_type: crate::executor::ContextType::Mock,
|
||||
};
|
||||
|
||||
ctx.run(&program, None).await.unwrap();
|
||||
ctx.run(&program, None, id_generator).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||
|
@ -5,6 +5,7 @@ mod test_examples_someFn {
|
||||
let tokens = crate::token::lexer("someFn()").unwrap();
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let program = parser.ast().unwrap();
|
||||
let id_generator = crate::executor::IdGenerator::default();
|
||||
let ctx = crate::executor::ExecutorContext {
|
||||
engine: std::sync::Arc::new(Box::new(
|
||||
crate::engine::conn_mock::EngineConnection::new()
|
||||
@ -16,7 +17,7 @@ mod test_examples_someFn {
|
||||
settings: Default::default(),
|
||||
context_type: crate::executor::ContextType::Mock,
|
||||
};
|
||||
ctx.run(&program, None).await.unwrap();
|
||||
ctx.run(&program, None, id_generator).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||
|
@ -5,6 +5,7 @@ mod test_examples_someFn {
|
||||
let tokens = crate::token::lexer("someFn()").unwrap();
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let program = parser.ast().unwrap();
|
||||
let id_generator = crate::executor::IdGenerator::default();
|
||||
let ctx = crate::executor::ExecutorContext {
|
||||
engine: std::sync::Arc::new(Box::new(
|
||||
crate::engine::conn_mock::EngineConnection::new()
|
||||
@ -16,7 +17,7 @@ mod test_examples_someFn {
|
||||
settings: Default::default(),
|
||||
context_type: crate::executor::ContextType::Mock,
|
||||
};
|
||||
ctx.run(&program, None).await.unwrap();
|
||||
ctx.run(&program, None, id_generator).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||
|
@ -5,6 +5,7 @@ mod test_examples_show {
|
||||
let tokens = crate::token::lexer("This is another code block.\nyes sirrr.\nshow").unwrap();
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let program = parser.ast().unwrap();
|
||||
let id_generator = crate::executor::IdGenerator::default();
|
||||
let ctx = crate::executor::ExecutorContext {
|
||||
engine: std::sync::Arc::new(Box::new(
|
||||
crate::engine::conn_mock::EngineConnection::new()
|
||||
@ -16,7 +17,7 @@ mod test_examples_show {
|
||||
settings: Default::default(),
|
||||
context_type: crate::executor::ContextType::Mock,
|
||||
};
|
||||
ctx.run(&program, None).await.unwrap();
|
||||
ctx.run(&program, None, id_generator).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||
@ -38,6 +39,7 @@ mod test_examples_show {
|
||||
let tokens = crate::token::lexer("This is code.\nIt does other shit.\nshow").unwrap();
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let program = parser.ast().unwrap();
|
||||
let id_generator = crate::executor::IdGenerator::default();
|
||||
let ctx = crate::executor::ExecutorContext {
|
||||
engine: std::sync::Arc::new(Box::new(
|
||||
crate::engine::conn_mock::EngineConnection::new()
|
||||
@ -49,7 +51,7 @@ mod test_examples_show {
|
||||
settings: Default::default(),
|
||||
context_type: crate::executor::ContextType::Mock,
|
||||
};
|
||||
ctx.run(&program, None).await.unwrap();
|
||||
ctx.run(&program, None, id_generator).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||
|
@ -5,6 +5,7 @@ mod test_examples_show {
|
||||
let tokens = crate::token::lexer("This is code.\nIt does other shit.\nshow").unwrap();
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let program = parser.ast().unwrap();
|
||||
let id_generator = crate::executor::IdGenerator::default();
|
||||
let ctx = crate::executor::ExecutorContext {
|
||||
engine: std::sync::Arc::new(Box::new(
|
||||
crate::engine::conn_mock::EngineConnection::new()
|
||||
@ -16,7 +17,7 @@ mod test_examples_show {
|
||||
settings: Default::default(),
|
||||
context_type: crate::executor::ContextType::Mock,
|
||||
};
|
||||
ctx.run(&program, None).await.unwrap();
|
||||
ctx.run(&program, None, id_generator).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||
|
@ -6,6 +6,7 @@ mod test_examples_my_func {
|
||||
crate::token::lexer("This is another code block.\nyes sirrr.\nmyFunc").unwrap();
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let program = parser.ast().unwrap();
|
||||
let id_generator = crate::executor::IdGenerator::default();
|
||||
let ctx = crate::executor::ExecutorContext {
|
||||
engine: std::sync::Arc::new(Box::new(
|
||||
crate::engine::conn_mock::EngineConnection::new()
|
||||
@ -17,7 +18,7 @@ mod test_examples_my_func {
|
||||
settings: Default::default(),
|
||||
context_type: crate::executor::ContextType::Mock,
|
||||
};
|
||||
ctx.run(&program, None).await.unwrap();
|
||||
ctx.run(&program, None, id_generator).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||
@ -39,6 +40,7 @@ mod test_examples_my_func {
|
||||
let tokens = crate::token::lexer("This is code.\nIt does other shit.\nmyFunc").unwrap();
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let program = parser.ast().unwrap();
|
||||
let id_generator = crate::executor::IdGenerator::default();
|
||||
let ctx = crate::executor::ExecutorContext {
|
||||
engine: std::sync::Arc::new(Box::new(
|
||||
crate::engine::conn_mock::EngineConnection::new()
|
||||
@ -50,7 +52,7 @@ mod test_examples_my_func {
|
||||
settings: Default::default(),
|
||||
context_type: crate::executor::ContextType::Mock,
|
||||
};
|
||||
ctx.run(&program, None).await.unwrap();
|
||||
ctx.run(&program, None, id_generator).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||
|
@ -6,6 +6,7 @@ mod test_examples_line_to {
|
||||
crate::token::lexer("This is another code block.\nyes sirrr.\nlineTo").unwrap();
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let program = parser.ast().unwrap();
|
||||
let id_generator = crate::executor::IdGenerator::default();
|
||||
let ctx = crate::executor::ExecutorContext {
|
||||
engine: std::sync::Arc::new(Box::new(
|
||||
crate::engine::conn_mock::EngineConnection::new()
|
||||
@ -17,7 +18,7 @@ mod test_examples_line_to {
|
||||
settings: Default::default(),
|
||||
context_type: crate::executor::ContextType::Mock,
|
||||
};
|
||||
ctx.run(&program, None).await.unwrap();
|
||||
ctx.run(&program, None, id_generator).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||
@ -39,6 +40,7 @@ mod test_examples_line_to {
|
||||
let tokens = crate::token::lexer("This is code.\nIt does other shit.\nlineTo").unwrap();
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let program = parser.ast().unwrap();
|
||||
let id_generator = crate::executor::IdGenerator::default();
|
||||
let ctx = crate::executor::ExecutorContext {
|
||||
engine: std::sync::Arc::new(Box::new(
|
||||
crate::engine::conn_mock::EngineConnection::new()
|
||||
@ -50,7 +52,7 @@ mod test_examples_line_to {
|
||||
settings: Default::default(),
|
||||
context_type: crate::executor::ContextType::Mock,
|
||||
};
|
||||
ctx.run(&program, None).await.unwrap();
|
||||
ctx.run(&program, None, id_generator).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||
|
@ -5,6 +5,7 @@ mod test_examples_min {
|
||||
let tokens = crate::token::lexer("This is another code block.\nyes sirrr.\nmin").unwrap();
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let program = parser.ast().unwrap();
|
||||
let id_generator = crate::executor::IdGenerator::default();
|
||||
let ctx = crate::executor::ExecutorContext {
|
||||
engine: std::sync::Arc::new(Box::new(
|
||||
crate::engine::conn_mock::EngineConnection::new()
|
||||
@ -16,7 +17,7 @@ mod test_examples_min {
|
||||
settings: Default::default(),
|
||||
context_type: crate::executor::ContextType::Mock,
|
||||
};
|
||||
ctx.run(&program, None).await.unwrap();
|
||||
ctx.run(&program, None, id_generator).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||
@ -38,6 +39,7 @@ mod test_examples_min {
|
||||
let tokens = crate::token::lexer("This is code.\nIt does other shit.\nmin").unwrap();
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let program = parser.ast().unwrap();
|
||||
let id_generator = crate::executor::IdGenerator::default();
|
||||
let ctx = crate::executor::ExecutorContext {
|
||||
engine: std::sync::Arc::new(Box::new(
|
||||
crate::engine::conn_mock::EngineConnection::new()
|
||||
@ -49,7 +51,7 @@ mod test_examples_min {
|
||||
settings: Default::default(),
|
||||
context_type: crate::executor::ContextType::Mock,
|
||||
};
|
||||
ctx.run(&program, None).await.unwrap();
|
||||
ctx.run(&program, None, id_generator).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||
|
@ -5,6 +5,7 @@ mod test_examples_show {
|
||||
let tokens = crate::token::lexer("This is code.\nIt does other shit.\nshow").unwrap();
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let program = parser.ast().unwrap();
|
||||
let id_generator = crate::executor::IdGenerator::default();
|
||||
let ctx = crate::executor::ExecutorContext {
|
||||
engine: std::sync::Arc::new(Box::new(
|
||||
crate::engine::conn_mock::EngineConnection::new()
|
||||
@ -16,7 +17,7 @@ mod test_examples_show {
|
||||
settings: Default::default(),
|
||||
context_type: crate::executor::ContextType::Mock,
|
||||
};
|
||||
ctx.run(&program, None).await.unwrap();
|
||||
ctx.run(&program, None, id_generator).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||
|
@ -5,6 +5,7 @@ mod test_examples_import {
|
||||
let tokens = crate::token::lexer("This is code.\nIt does other shit.\nimport").unwrap();
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let program = parser.ast().unwrap();
|
||||
let id_generator = crate::executor::IdGenerator::default();
|
||||
let ctx = crate::executor::ExecutorContext {
|
||||
engine: std::sync::Arc::new(Box::new(
|
||||
crate::engine::conn_mock::EngineConnection::new()
|
||||
@ -16,7 +17,7 @@ mod test_examples_import {
|
||||
settings: Default::default(),
|
||||
context_type: crate::executor::ContextType::Mock,
|
||||
};
|
||||
ctx.run(&program, None).await.unwrap();
|
||||
ctx.run(&program, None, id_generator).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||
|
@ -5,6 +5,7 @@ mod test_examples_import {
|
||||
let tokens = crate::token::lexer("This is code.\nIt does other shit.\nimport").unwrap();
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let program = parser.ast().unwrap();
|
||||
let id_generator = crate::executor::IdGenerator::default();
|
||||
let ctx = crate::executor::ExecutorContext {
|
||||
engine: std::sync::Arc::new(Box::new(
|
||||
crate::engine::conn_mock::EngineConnection::new()
|
||||
@ -16,7 +17,7 @@ mod test_examples_import {
|
||||
settings: Default::default(),
|
||||
context_type: crate::executor::ContextType::Mock,
|
||||
};
|
||||
ctx.run(&program, None).await.unwrap();
|
||||
ctx.run(&program, None, id_generator).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||
|
@ -5,6 +5,7 @@ mod test_examples_import {
|
||||
let tokens = crate::token::lexer("This is code.\nIt does other shit.\nimport").unwrap();
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let program = parser.ast().unwrap();
|
||||
let id_generator = crate::executor::IdGenerator::default();
|
||||
let ctx = crate::executor::ExecutorContext {
|
||||
engine: std::sync::Arc::new(Box::new(
|
||||
crate::engine::conn_mock::EngineConnection::new()
|
||||
@ -16,7 +17,7 @@ mod test_examples_import {
|
||||
settings: Default::default(),
|
||||
context_type: crate::executor::ContextType::Mock,
|
||||
};
|
||||
ctx.run(&program, None).await.unwrap();
|
||||
ctx.run(&program, None, id_generator).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||
|
@ -5,6 +5,7 @@ mod test_examples_show {
|
||||
let tokens = crate::token::lexer("This is code.\nIt does other shit.\nshow").unwrap();
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let program = parser.ast().unwrap();
|
||||
let id_generator = crate::executor::IdGenerator::default();
|
||||
let ctx = crate::executor::ExecutorContext {
|
||||
engine: std::sync::Arc::new(Box::new(
|
||||
crate::engine::conn_mock::EngineConnection::new()
|
||||
@ -16,7 +17,7 @@ mod test_examples_show {
|
||||
settings: Default::default(),
|
||||
context_type: crate::executor::ContextType::Mock,
|
||||
};
|
||||
ctx.run(&program, None).await.unwrap();
|
||||
ctx.run(&program, None, id_generator).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||
|
@ -5,6 +5,7 @@ mod test_examples_some_function {
|
||||
let tokens = crate::token::lexer("someFunction()").unwrap();
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let program = parser.ast().unwrap();
|
||||
let id_generator = crate::executor::IdGenerator::default();
|
||||
let ctx = crate::executor::ExecutorContext {
|
||||
engine: std::sync::Arc::new(Box::new(
|
||||
crate::engine::conn_mock::EngineConnection::new()
|
||||
@ -16,7 +17,7 @@ mod test_examples_some_function {
|
||||
settings: Default::default(),
|
||||
context_type: crate::executor::ContextType::Mock,
|
||||
};
|
||||
ctx.run(&program, None).await.unwrap();
|
||||
ctx.run(&program, None, id_generator).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||
|
@ -166,15 +166,19 @@ async fn snapshot_endpoint(body: Bytes, state: ExecutorContext) -> Response<Body
|
||||
Err(e) => return bad_request(format!("Parse error: {e}")),
|
||||
};
|
||||
eprintln!("Executing {test_name}");
|
||||
let mut id_generator = kcl_lib::executor::IdGenerator::default();
|
||||
// This is a shitty source range, I don't know what else to use for it though.
|
||||
// There's no actual KCL associated with this reset_scene call.
|
||||
if let Err(e) = state.reset_scene(kcl_lib::executor::SourceRange::default()).await {
|
||||
if let Err(e) = state
|
||||
.reset_scene(&mut id_generator, kcl_lib::executor::SourceRange::default())
|
||||
.await
|
||||
{
|
||||
return kcl_err(e);
|
||||
}
|
||||
// Let users know if the test is taking a long time.
|
||||
let (done_tx, done_rx) = oneshot::channel::<()>();
|
||||
let timer = time_until(done_rx);
|
||||
let snapshot = match state.execute_and_prepare_snapshot(&program).await {
|
||||
let snapshot = match state.execute_and_prepare_snapshot(&program, id_generator).await {
|
||||
Ok(sn) => sn,
|
||||
Err(e) => return kcl_err(e),
|
||||
};
|
||||
|
@ -1,6 +1,9 @@
|
||||
use anyhow::Result;
|
||||
use indexmap::IndexMap;
|
||||
use kcl_lib::{errors::KclError, executor::DefaultPlanes};
|
||||
use kcl_lib::{
|
||||
errors::KclError,
|
||||
executor::{DefaultPlanes, IdGenerator},
|
||||
};
|
||||
use kittycad_modeling_cmds::{
|
||||
self as kcmc,
|
||||
id::ModelingCmdId,
|
||||
@ -357,7 +360,11 @@ impl kcl_lib::engine::EngineManager for EngineConnection {
|
||||
self.batch_end.clone()
|
||||
}
|
||||
|
||||
async fn default_planes(&self, source_range: kcl_lib::executor::SourceRange) -> Result<DefaultPlanes, KclError> {
|
||||
async fn default_planes(
|
||||
&self,
|
||||
id_generator: &mut IdGenerator,
|
||||
source_range: kcl_lib::executor::SourceRange,
|
||||
) -> Result<DefaultPlanes, KclError> {
|
||||
if NEED_PLANES {
|
||||
{
|
||||
let opt = self.default_planes.read().await.as_ref().cloned();
|
||||
@ -366,7 +373,7 @@ impl kcl_lib::engine::EngineManager for EngineConnection {
|
||||
}
|
||||
} // drop the read lock
|
||||
|
||||
let new_planes = self.new_default_planes(source_range).await?;
|
||||
let new_planes = self.new_default_planes(id_generator, source_range).await?;
|
||||
*self.default_planes.write().await = Some(new_planes.clone());
|
||||
|
||||
Ok(new_planes)
|
||||
@ -375,7 +382,11 @@ impl kcl_lib::engine::EngineManager for EngineConnection {
|
||||
}
|
||||
}
|
||||
|
||||
async fn clear_scene_post_hook(&self, _source_range: kcl_lib::executor::SourceRange) -> Result<(), KclError> {
|
||||
async fn clear_scene_post_hook(
|
||||
&self,
|
||||
_id_generator: &mut IdGenerator,
|
||||
_source_range: kcl_lib::executor::SourceRange,
|
||||
) -> Result<(), KclError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
use anyhow::Result;
|
||||
use kcl_lib::executor::ExecutorContext;
|
||||
use kcl_lib::executor::{ExecutorContext, IdGenerator};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
@ -23,7 +23,7 @@ pub async fn kcl_to_engine_core(code: &str) -> Result<String> {
|
||||
settings: Default::default(),
|
||||
context_type: kcl_lib::executor::ContextType::MockCustomForwarded,
|
||||
};
|
||||
let _memory = ctx.run(&program, None).await?;
|
||||
let _memory = ctx.run(&program, None, IdGenerator::default()).await?;
|
||||
|
||||
let result = result.lock().expect("mutex lock").clone();
|
||||
Ok(result)
|
||||
|
@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "kcl-lib"
|
||||
description = "KittyCAD Language implementation and tools"
|
||||
version = "0.2.20"
|
||||
version = "0.2.21"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/KittyCAD/modeling-app"
|
||||
@ -16,13 +16,13 @@ async-recursion = "1.1.1"
|
||||
async-trait = "0.1.83"
|
||||
base64 = "0.22.1"
|
||||
chrono = "0.4.38"
|
||||
clap = { version = "4.5.19", default-features = false, optional = true, features = ["std", "derive"] }
|
||||
clap = { version = "4.5.20", default-features = false, optional = true, features = ["std", "derive"] }
|
||||
convert_case = "0.6.0"
|
||||
dashmap = "6.1.0"
|
||||
databake = { version = "0.1.8", features = ["derive"] }
|
||||
derive-docs = { version = "0.1.29", path = "../derive-docs" }
|
||||
form_urlencoded = "1.2.1"
|
||||
futures = { version = "0.3.30" }
|
||||
futures = { version = "0.3.31" }
|
||||
git_rev = "0.1.0"
|
||||
gltf-json = "1.4.1"
|
||||
http = { workspace = true }
|
||||
@ -53,11 +53,11 @@ winnow = "0.6.18"
|
||||
zip = { version = "2.0.0", default-features = false }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
js-sys = { version = "0.3.69" }
|
||||
js-sys = { version = "0.3.71" }
|
||||
tokio = { version = "1.40.0", features = ["sync", "time"] }
|
||||
tower-lsp = { version = "0.20.0", default-features = false, features = ["runtime-agnostic"] }
|
||||
wasm-bindgen = "0.2.91"
|
||||
wasm-bindgen-futures = "0.4.42"
|
||||
wasm-bindgen-futures = "0.4.44"
|
||||
web-sys = { version = "0.3.69", features = ["console"] }
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
|
@ -787,12 +787,14 @@ fn test_generate_stdlib_json_schema() {
|
||||
let stdlib = StdLib::new();
|
||||
let combined = stdlib.combined();
|
||||
|
||||
let mut json_data = vec![];
|
||||
|
||||
for key in combined.keys().sorted() {
|
||||
let internal_fn = combined.get(key).unwrap();
|
||||
json_data.push(internal_fn.to_json().unwrap());
|
||||
}
|
||||
let json_data: Vec<_> = combined
|
||||
.keys()
|
||||
.sorted()
|
||||
.map(|key| {
|
||||
let internal_fn = combined.get(key).unwrap();
|
||||
internal_fn.to_json().unwrap()
|
||||
})
|
||||
.collect();
|
||||
expectorate::assert_contents(
|
||||
"../../../docs/kcl/std.json",
|
||||
&serde_json::to_string_pretty(&json_data).unwrap(),
|
||||
|
@ -83,6 +83,8 @@ impl StdLibFnArg {
|
||||
return Ok(Some((index, format!("${{{}:{}}}", index, "myTag"))));
|
||||
} else if self.type_ == "[KclValue]" && self.required {
|
||||
return Ok(Some((index, format!("${{{}:{}}}", index, "[0..9]"))));
|
||||
} else if self.type_ == "KclValue" && self.required {
|
||||
return Ok(Some((index, format!("${{{}:{}}}", index, "3"))));
|
||||
}
|
||||
get_autocomplete_snippet_from_schema(&self.schema.schema.clone().into(), index)
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ use tokio_tungstenite::tungstenite::Message as WsMsg;
|
||||
use crate::{
|
||||
engine::EngineManager,
|
||||
errors::{KclError, KclErrorDetails},
|
||||
executor::DefaultPlanes,
|
||||
executor::{DefaultPlanes, IdGenerator},
|
||||
};
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
@ -314,7 +314,11 @@ impl EngineManager for EngineConnection {
|
||||
self.batch_end.clone()
|
||||
}
|
||||
|
||||
async fn default_planes(&self, source_range: crate::executor::SourceRange) -> Result<DefaultPlanes, KclError> {
|
||||
async fn default_planes(
|
||||
&self,
|
||||
id_generator: &mut IdGenerator,
|
||||
source_range: crate::executor::SourceRange,
|
||||
) -> Result<DefaultPlanes, KclError> {
|
||||
{
|
||||
let opt = self.default_planes.read().await.as_ref().cloned();
|
||||
if let Some(planes) = opt {
|
||||
@ -322,15 +326,19 @@ impl EngineManager for EngineConnection {
|
||||
}
|
||||
} // drop the read lock
|
||||
|
||||
let new_planes = self.new_default_planes(source_range).await?;
|
||||
let new_planes = self.new_default_planes(id_generator, source_range).await?;
|
||||
*self.default_planes.write().await = Some(new_planes.clone());
|
||||
|
||||
Ok(new_planes)
|
||||
}
|
||||
|
||||
async fn clear_scene_post_hook(&self, source_range: crate::executor::SourceRange) -> Result<(), KclError> {
|
||||
async fn clear_scene_post_hook(
|
||||
&self,
|
||||
id_generator: &mut IdGenerator,
|
||||
source_range: crate::executor::SourceRange,
|
||||
) -> Result<(), KclError> {
|
||||
// Remake the default planes, since they would have been removed after the scene was cleared.
|
||||
let new_planes = self.new_default_planes(source_range).await?;
|
||||
let new_planes = self.new_default_planes(id_generator, source_range).await?;
|
||||
*self.default_planes.write().await = Some(new_planes);
|
||||
|
||||
Ok(())
|
||||
|