Compare commits

...

31 Commits

Author SHA1 Message Date
60e187bd3e Fix defined but never used warnings (#3045) 2024-07-17 04:26:14 +00:00
c64175425b Updater smoke-test instructions (#3044)
updater smoke test instructions
2024-07-17 14:00:05 +10:00
36464e6984 fillet ui follow up (#3035) 2024-07-17 03:58:48 +00:00
2f0002e53c Cut release v0.24.2 (#3042) 2024-07-17 13:55:17 +10:00
482833c88f Lf94/pause improvements (#3032)
* Add stream idle mode as a setting (default is off)

* Add pause icon
2024-07-17 12:45:11 +10:00
d9d0a72306 Remove unused function ProgramMemory::get_tags (#3033)
Seems to be unused since #2941.
2024-07-16 15:12:05 -04:00
65cd9fab64 Revert the snapshots. (#3039)
* Revert "Compute the AST digest in the LSP (#3037)"

This reverts commit 5e41e382ce.

* Compute the AST digest in the LSP (#3037)

This is a slow-roll to calling this in more places; but this is
non-critical, so if this breaks on some unexpected AST or what have you,
we're not breaking anything except the LSP (which we'll see pretty
quickly) while also testing it on all user input.

If something goes south, please feel free to revert this commit.

Signed-off-by: Paul Tagliamonte <paul@zoo.dev>

---------

Signed-off-by: Paul Tagliamonte <paul@zoo.dev>
2024-07-16 13:16:51 -04:00
5e41e382ce Compute the AST digest in the LSP (#3037)
This is a slow-roll to calling this in more places; but this is
non-critical, so if this breaks on some unexpected AST or what have you,
we're not breaking anything except the LSP (which we'll see pretty
quickly) while also testing it on all user input.

If something goes south, please feel free to revert this commit.

Signed-off-by: Paul Tagliamonte <paul@zoo.dev>
2024-07-16 12:13:17 -04:00
1e3cb00092 Move Args into its own module (#3027) 2024-07-16 07:45:43 -05:00
d1a2bd01ca Lower threshold for 2020 tests (#3030)
* Lower threshold for 2020 tests

Now that the tests zoom into the model and center it before taking a
snapshot, they should be less sensitive.

* Genuine, nontrivial changes to the integration test images
2024-07-15 17:41:41 -05:00
aca13d087b add in benchmarks for digest code (#3031)
(initial results look good!)
2024-07-15 13:45:55 -04:00
fcdde3e482 Bump syn from 2.0.70 to 2.0.71 in /src/wasm-lib (#3029)
Bumps [syn](https://github.com/dtolnay/syn) from 2.0.70 to 2.0.71.
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/2.0.70...2.0.71)

---
updated-dependencies:
- dependency-name: syn
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-15 09:26:18 -07:00
a1df3d0ffc Fillet UI (#2718)
* draft: fillet ast mod + test

* Kurt's rejig

* playwright

* update button enable logic

* remove fillet button in production build

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* trigger CI

* fix typo

* give a way to turn on fillets

---------

Co-authored-by: max-mrgrsk <margorskyi@gmail.com>
Co-authored-by: max-mrgrsk <156543465+max-mrgrsk@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-07-15 19:20:32 +10:00
1852e6167b Fix argument persistence and accidental submission command bar bugs (#3021) 2024-07-14 18:15:34 -04:00
29bf77bb82 Show descriptions for generated commands, make them look better and sort better (#3023) 2024-07-12 17:48:38 -04:00
e81b614523 Lf94/save settings between reconnects (#2997)
* Keep settings between reconnects

* Set idle timeout to 2 minutes

* Put idle behind flags

* Remove pauses

* Fix online->offline->online

* Revert "Remove pauses"

This reverts commit 267ef4ff4b86f2d8014bfb2a8e8a633adc8001dc.

* ci

* call correct setmediastream
2024-07-12 20:42:23 +00:00
5a5fe3bb95 Add sketch tools back to the command bar (#3008)
* Make machine command type names more explicit

* Prepare "change tool" event for command bar

* Make it so that state machine events can each map to multiple command configs

* Make commands with all skippable args possible

* Add back the tools to the command bar

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* Update to use new `groupId` property name

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* Oops didn't save this other instance of `ownerMachine`

* Add a playwright test

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-07-12 16:16:26 -04:00
0710f6e5f2 Add format code to the command palette (#3001)
* Add format code to the command palette

* Fix to use renamed groupId parameter

* Add icon to format code command

* Fix to remove commands during teardown

* Fix dependencies

* Change formatting
2024-07-12 17:08:01 +00:00
c9d5633647 Bump thiserror from 1.0.61 to 1.0.62 in /src/wasm-lib (#3016)
Bumps [thiserror](https://github.com/dtolnay/thiserror) from 1.0.61 to 1.0.62.
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/1.0.61...1.0.62)

---
updated-dependencies:
- dependency-name: thiserror
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-12 09:11:49 -07:00
f9419a98b5 Cut release v0.24.1 (#3014) 2024-07-11 21:51:58 -04:00
999f72bccf mediaStream (#3013)
* mediaStream

* make vitest happy

* fmt
2024-07-11 20:57:27 -04:00
9dbe74e008 cleaner hack (#3012) 2024-07-12 09:41:39 +10:00
88d9cdc52b Codemirror deferrers (#3006)
* Force document update requests when necessary

* fix up codemirror deferrers

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* lock

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fixups kcl/index

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix copilot

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* docs

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: Marijn Haverbeke <marijn@haverbeke.berlin>
2024-07-11 16:05:19 -07:00
2dd1f0f213 refactor: Rename ownerMachine to groupId in Command (#3010)
* refactor: Rename ownerMachine to groupId in Command

Commands don't need to be part of a state machine.

* Fix formatting
2024-07-11 18:10:47 -04:00
b971f3ecf4 Fix CUT_RELEASE_PR eval in ci.yml (#3003) 2024-07-11 08:19:33 -04:00
2198bd7580 Rename function to use standard abbreviation (#2965) 2024-07-11 11:52:26 +00:00
5fa1497b75 Don't navigate when Backspace/Delete is pressed on the home screen (#2987) 2024-07-11 07:50:59 -04:00
ff86e41283 Roll your own Playwright retries (#3002)
* roll you own playwright retries

* tweak

* tweak

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* add retries for ubuntu too

* Revert "A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)"

This reverts commit 327cc196cd.

* Revert "add retries for ubuntu too"

This reverts commit db877748e2.

* add retries for ubuntu too

* whoopsie

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-07-11 17:37:59 +10:00
08e4c03ca7 send failing test to axiom (#2996)
* send failing test to axiom (ubuntu)

* forgot always

* rename

* Update .github/workflows/playwright.yml

Co-authored-by: Adam Sunderland <adam@kittycad.io>

* update to indivdual lines of json

* another fix

* tweak output

* log macos too

---------

Co-authored-by: Adam Sunderland <adam@kittycad.io>
2024-07-11 14:32:36 +10:00
c654582137 Build tauri updater test bundles on 'Cut release' PRs (#2927)
* WIP: Automate tauri updater tests
Fixes #2926

* Same product name

* Tweak uploads

* Add cat

* Fix macos universal builds for updater

* New artifact name

* Revert "New artifact name"

This reverts commit 61defcab18.

* Final check

* Clean up
2024-07-10 18:41:07 -04:00
6c2fa95a32 Fix perspective camera toggle in debug pane to update immediately (#2969) 2024-07-10 17:50:25 -04:00
114 changed files with 3757 additions and 1821 deletions

View File

@ -13,6 +13,7 @@ on:
# Will checkout the last commit from the default branch (main as of 2023-10-04) # Will checkout the last commit from the default branch (main as of 2023-10-04)
env: env:
CUT_RELEASE_PR: ${{ github.event_name == 'pull_request' && (contains(github.event.pull_request.title, 'Cut release v')) }}
BUILD_RELEASE: ${{ github.event_name == 'release' || github.event_name == 'schedule' || github.event_name == 'pull_request' && (contains(github.event.pull_request.title, 'Cut release v')) }} BUILD_RELEASE: ${{ github.event_name == 'release' || github.event_name == 'schedule' || github.event_name == 'pull_request' && (contains(github.event.pull_request.title, 'Cut release v')) }}
concurrency: concurrency:
@ -110,8 +111,14 @@ jobs:
echo "$(jq --arg name 'Zoo Modeling App (Nightly)' \ echo "$(jq --arg name 'Zoo Modeling App (Nightly)' \
'.productName=$name' src-tauri/tauri.release.conf.json --indent 2)" > src-tauri/tauri.release.conf.json '.productName=$name' src-tauri/tauri.release.conf.json --indent 2)" > src-tauri/tauri.release.conf.json
- name: Set updater test version
if: ${{ env.CUT_RELEASE_PR == 'true' }}
run: |
echo "$(jq --arg url 'https://dl.zoo.dev/releases/modeling-app/test/last_update.json' \
'.plugins.updater.endpoints[]=$url' src-tauri/tauri.release.conf.json --indent 2)" > src-tauri/tauri.release.conf.json
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v3
if: github.event_name == 'schedule' if: ${{ github.event_name == 'schedule' || env.CUT_RELEASE_PR == 'true' }}
with: with:
path: | path: |
package.json package.json
@ -377,6 +384,30 @@ jobs:
E2E_TAURI_ENABLED: true E2E_TAURI_ENABLED: true
TS_NODE_COMPILER_OPTIONS: '{"module": "commonjs"}' TS_NODE_COMPILER_OPTIONS: '{"module": "commonjs"}'
- uses: actions/download-artifact@v3
if: ${{ env.CUT_RELEASE_PR == 'true' }}
- name: Copy updated .json file for updater test
if: ${{ env.CUT_RELEASE_PR == 'true' }}
run: |
ls -l artifact
cp artifact/src-tauri/tauri.release.conf.json src-tauri/tauri.release.conf.json
cat src-tauri/tauri.release.conf.json
- name: Build the app (release, updater test)
if: ${{ env.CUT_RELEASE_PR == 'true' && matrix.os != 'ubuntu-latest' }}
env:
TAURI_CONF_ARGS: "-c ${{ matrix.os == 'windows-latest' && 'src-tauri\\tauri.release.conf.json' || 'src-tauri/tauri.release.conf.json' }}"
TAURI_BUNDLE_ARGS: "-b ${{ matrix.os == 'windows-latest' && 'msi' || 'dmg' }}"
run: "yarn tauri build ${{ env.TAURI_CONF_ARGS }} ${{ env.TAURI_BUNDLE_ARGS }} ${{ env.TAURI_ARGS_MACOS }}"
- uses: actions/upload-artifact@v3
if: ${{ env.CUT_RELEASE_PR == 'true' && matrix.os != 'ubuntu-latest' }}
with:
path: "${{ matrix.os == 'macos-14' && 'src-tauri/target/universal-apple-darwin/release/bundle/dmg/*.dmg' || 'src-tauri/target/release/bundle/msi/*.msi' }}"
name: updater-test
publish-apps-release: publish-apps-release:
permissions: permissions:
contents: write contents: write

View File

@ -83,6 +83,20 @@ jobs:
uses: Swatinem/rust-cache@v2 uses: Swatinem/rust-cache@v2
with: with:
workspaces: './src/wasm-lib' workspaces: './src/wasm-lib'
- name: Install vector
run: |
curl --proto '=https' --tlsv1.2 -sSfL https://sh.vector.dev > /tmp/vector.sh
chmod +x /tmp/vector.sh
/tmp/vector.sh -y -no-modify-path
mkdir -p /tmp/vector
cp .github/workflows/vector.toml /tmp/vector.toml
sed -i "s#GITHUB_WORKFLOW#${GITHUB_WORKFLOW}#g" /tmp/vector.toml
sed -i "s#GITHUB_REPOSITORY#${GITHUB_REPOSITORY}#g" /tmp/vector.toml
sed -i "s#GITHUB_SHA#${GITHUB_SHA}#g" /tmp/vector.toml
sed -i "s#GITHUB_REF_NAME#${GITHUB_REF_NAME}#g" /tmp/vector.toml
sed -i "s#GH_ACTIONS_AXIOM_TOKEN#${{secrets.GH_ACTIONS_AXIOM_TOKEN}}#g" /tmp/vector.toml
cat /tmp/vector.toml
${HOME}/.vector/bin/vector --config /tmp/vector.toml &
- name: Build Wasm (because rust diff) - name: Build Wasm (because rust diff)
if: needs.check-rust-changes.outputs.rust-changed == 'true' if: needs.check-rust-changes.outputs.rust-changed == 'true'
run: yarn build:wasm run: yarn build:wasm
@ -139,27 +153,60 @@ jobs:
with: with:
name: test-results-ubuntu-${{ github.sha }} name: test-results-ubuntu-${{ github.sha }}
path: test-results/ path: test-results/
- name: Run ubuntu/chrome flow retry failures - name: Run ubuntu/chrome flow (with retries)
id: retry id: retry
if: always() if: always()
run: | run: |
if [[ -d "test-results" ]]; if [[ ! -f "test-results/.last-run.json" ]]; then
then if [[ $(ls -1 "test-results" | wc -l) != "0" ]]; # if no last run artifact, than run plawright normally
then echo "retried=true" >> $GITHUB_OUTPUT; echo "run playwright normally"
else echo "retried=false" >> $GITHUB_OUTPUT; exit 0; yarn playwright test --project="Google Chrome" e2e/playwright/flow-tests.spec.ts || true
fi; # # send to axiom
else echo "retried=false" >> $GITHUB_OUTPUT; exit 0; node playwrightProcess.mjs | tee /tmp/github-actions.log > /dev/null 2>&1
fi; fi
yarn playwright test --project="Google Chrome" --last-failed e2e/playwright/flow-tests.spec.ts
env: retry=1
CI: true max_retrys=4
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
- name: Run ubuntu/chrome flow # retry failed tests, doing our own retries because using inbuilt playwright retries causes connection issues
if: steps.retry.outputs.retried == 'false' while [[ $retry -le $max_retrys ]]; do
run: yarn playwright test --project="Google Chrome" e2e/playwright/flow-tests.spec.ts if [[ -f "test-results/.last-run.json" ]]; then
failed_tests=$(jq '.failedTests | length' test-results/.last-run.json)
if [[ $failed_tests -gt 0 ]]; then
echo "retried=true" >>$GITHUB_OUTPUT
echo "run playwright with last failed tests and retry $retry"
yarn playwright test --project="Google Chrome" --last-failed e2e/playwright/flow-tests.spec.ts || true
# send to axiom
node playwrightProcess.mjs | tee /tmp/github-actions.log > /dev/null 2>&1
retry=$((retry + 1))
else
echo "retried=false" >>$GITHUB_OUTPUT
exit 0
fi
else
echo "retried=false" >>$GITHUB_OUTPUT
exit 0
fi
done
echo "retried=false" >>$GITHUB_OUTPUT
if [[ -f "test-results/.last-run.json" ]]; then
failed_tests=$(jq '.failedTests | length' test-results/.last-run.json)
if [[ $failed_tests -gt 0 ]]; then
# if it still fails after 3 retrys, then fail the job
exit 1
fi
fi
exit 0
env: env:
CI: true CI: true
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }} token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
- name: send to axiom
if: always()
shell: bash
run: |
node playwrightProcess.mjs | tee /tmp/github-actions.log
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
if: always() if: always()
with: with:
@ -226,6 +273,20 @@ jobs:
uses: Swatinem/rust-cache@v2 uses: Swatinem/rust-cache@v2
with: with:
workspaces: './src/wasm-lib' workspaces: './src/wasm-lib'
- name: Install vector
run: |
curl --proto '=https' --tlsv1.2 -sSfL https://sh.vector.dev > /tmp/vector.sh
chmod +x /tmp/vector.sh
/tmp/vector.sh -y -no-modify-path
mkdir -p /tmp/vector
cp .github/workflows/vector.toml /tmp/vector.toml
sed -i "" "s#GITHUB_WORKFLOW#${GITHUB_WORKFLOW}#g" /tmp/vector.toml
sed -i "" "s#GITHUB_REPOSITORY#${GITHUB_REPOSITORY}#g" /tmp/vector.toml
sed -i "" "s#GITHUB_SHA#${GITHUB_SHA}#g" /tmp/vector.toml
sed -i "" "s#GITHUB_REF_NAME#${GITHUB_REF_NAME}#g" /tmp/vector.toml
sed -i "" "s#GH_ACTIONS_AXIOM_TOKEN#${{secrets.GH_ACTIONS_AXIOM_TOKEN}}#g" /tmp/vector.toml
cat /tmp/vector.toml
${HOME}/.vector/bin/vector --config /tmp/vector.toml &
- name: Build Wasm (because rust diff) - name: Build Wasm (because rust diff)
if: needs.check-rust-changes.outputs.rust-changed == 'true' if: needs.check-rust-changes.outputs.rust-changed == 'true'
run: yarn build:wasm run: yarn build:wasm
@ -241,26 +302,52 @@ jobs:
with: with:
name: test-results-macos-${{ github.sha }} name: test-results-macos-${{ github.sha }}
path: test-results/ path: test-results/
- name: Run macos/safari flow retry failures - name: Run macos/safari flow (with retries)
id: retry id: retry
if: always() if: always()
run: | run: |
if [[ -d "test-results" ]]; if [[ ! -f "test-results/.last-run.json" ]]; then
then if [[ $(ls -1 "test-results" | wc -l) != "0" ]]; # if no last run artifact, than run plawright normally
then echo "retried=true" >> $GITHUB_OUTPUT; echo "run playwright normally"
else echo "retried=false" >> $GITHUB_OUTPUT; exit 0; yarn playwright test --project="webkit" e2e/playwright/flow-tests.spec.ts || true
fi; # # send to axiom
else echo "retried=false" >> $GITHUB_OUTPUT; exit 0; node playwrightProcess.mjs | tee /tmp/github-actions.log > /dev/null 2>&1
fi; fi
yarn playwright test --project="webkit" --last-failed e2e/playwright/flow-tests.spec.ts
env: retry=1
CI: true max_retrys=4
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
- name: Run macos/safari flow # retry failed tests, doing our own retries because using inbuilt playwright retries causes connection issues
if: steps.retry.outputs.retried == 'false' while [[ $retry -le $max_retrys ]]; do
# webkit doesn't work on Ubuntu because of the same reason tauri doesn't (webRTC issues) if [[ -f "test-results/.last-run.json" ]]; then
# TODO remove this and the matrix and run all tests on ubuntu when this is fixed failed_tests=$(jq '.failedTests | length' test-results/.last-run.json)
run: yarn playwright test --project="webkit" e2e/playwright/flow-tests.spec.ts if [[ $failed_tests -gt 0 ]]; then
echo "retried=true" >>$GITHUB_OUTPUT
echo "run playwright with last failed tests and retry $retry"
yarn playwright test --project="webkit" --last-failed e2e/playwright/flow-tests.spec.ts || true
# send to axiom
node playwrightProcess.mjs | tee /tmp/github-actions.log > /dev/null 2>&1
retry=$((retry + 1))
else
echo "retried=false" >>$GITHUB_OUTPUT
exit 0
fi
else
echo "retried=false" >>$GITHUB_OUTPUT
exit 0
fi
done
echo "retried=false" >>$GITHUB_OUTPUT
if [[ -f "test-results/.last-run.json" ]]; then
failed_tests=$(jq '.failedTests | length' test-results/.last-run.json)
if [[ $failed_tests -gt 0 ]]; then
# if it still fails after 3 retrys, then fail the job
exit 1
fi
fi
exit 0
env: env:
CI: true CI: true
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }} token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}

View File

@ -135,9 +135,18 @@ After it runs you should just need to push the push the branch and open a PR (it
The PR may serve as a place to discuss the human-readable changelog and extra QA. The PR may serve as a place to discuss the human-readable changelog and extra QA.
2. Merge the PR 2. Smoke test the artifact from the above PR
We don't have a strict process, but click around and check for anything obvious
One of the artifacts is called 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 v0.99.99, install it and check that the app comes back at that version (on both macOS and Windows).
3. Merge the PR
4. Profit (A new Action kicks in at https://github.com/KittyCAD/modeling-app/actions if the PR was correctly named)
3. Profit (A new Action kicks in at https://github.com/KittyCAD/modeling-app/actions if the PR was correctly named)
## Fuzzing the parser ## Fuzzing the parser

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 KiB

After

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 KiB

After

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 KiB

After

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 KiB

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 KiB

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 KiB

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 KiB

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 KiB

After

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 KiB

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 KiB

After

Width:  |  Height:  |  Size: 171 KiB

View File

@ -3099,6 +3099,49 @@ const sketch002 = startSketchOn(extrude001, $seg01)
).not.toBeDisabled() ).not.toBeDisabled()
}) })
test('Fillet button states test', async ({ page }) => {
const u = await getUtils(page)
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`const sketch001 = startSketchOn('XZ')
|> startProfileAt([-5, -5], %)
|> line([0, 10], %)
|> line([10, 0], %)
|> line([0, -10], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)`
)
})
await page.setViewportSize({ width: 1000, height: 500 })
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
const selectSegment = () => page.getByText(`line([10, 0], %)`).click()
const selectClose = () => page.getByText(`close(%)`).click()
const clickEmpty = () => page.mouse.click(950, 100)
// expect fillet button without any bodies in the scene
await selectSegment()
await expect(page.getByRole('button', { name: 'Fillet' })).toBeDisabled()
await clickEmpty()
await expect(page.getByRole('button', { name: 'Fillet' })).toBeDisabled()
// test fillet button with the body in the scene
const codeToAdd = `${await u.codeLocator.allInnerTexts()}
const extrude001 = extrude(10, sketch001)`
await u.codeLocator.fill(codeToAdd)
await selectSegment()
await expect(page.getByRole('button', { name: 'Fillet' })).toBeEnabled()
await selectClose()
await expect(page.getByRole('button', { name: 'Fillet' })).toBeDisabled()
await clickEmpty()
await expect(page.getByRole('button', { name: 'Fillet' })).toBeEnabled()
})
const removeAfterFirstParenthesis = (inputString: string) => { const removeAfterFirstParenthesis = (inputString: string) => {
const index = inputString.indexOf('(') const index = inputString.indexOf('(')
if (index !== -1) { if (index !== -1) {
@ -3500,11 +3543,62 @@ test.describe('Command bar tests', () => {
`const extrude001 = extrude(${KCL_DEFAULT_LENGTH}, sketch001)` `const extrude001 = extrude(${KCL_DEFAULT_LENGTH}, sketch001)`
) )
}) })
test('Command bar works and can change a setting', async ({ page }) => {
test('Fillet from command bar', async ({ page }) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`const sketch001 = startSketchOn('XY')
|> startProfileAt([-5, -5], %)
|> line([0, 10], %)
|> line([10, 0], %)
|> line([0, -10], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const extrude001 = extrude(-10, sketch001)`
)
})
const u = await getUtils(page)
await page.setViewportSize({ width: 1000, height: 500 })
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
const selectSegment = () => page.getByText(`line([0, -10], %)`).click()
await selectSegment()
await page.waitForTimeout(100)
await page.getByRole('button', { name: 'Fillet' }).click()
await page.waitForTimeout(100)
await page.keyboard.press('Enter')
await page.waitForTimeout(100)
await page.keyboard.press('Enter')
await page.waitForTimeout(100)
await expect(page.locator('.cm-activeLine')).toContainText(
`fillet({ radius: ${KCL_DEFAULT_LENGTH}, tags: [seg01] }, %)`
)
})
test('Command bar can change a setting, and switch back and forth between arguments', async ({
page,
}) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
const commandBarButton = page.getByRole('button', { name: 'Commands' })
const cmdSearchBar = page.getByPlaceholder('Search commands')
const themeOption = page.getByRole('option', {
name: 'theme',
exact: false,
})
const commandLevelArgButton = page.getByRole('button', { name: 'level' })
const commandThemeArgButton = page.getByRole('button', { name: 'value' })
// This selector changes after we set the setting
let commandOptionInput = page.getByPlaceholder('Select an option')
await expect( await expect(
page.getByRole('button', { name: 'Start Sketch' }) page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled() ).not.toBeDisabled()
@ -3515,23 +3609,17 @@ test.describe('Command bar tests', () => {
.or(page.getByRole('button', { name: '⌘K' })) .or(page.getByRole('button', { name: '⌘K' }))
.click() .click()
let cmdSearchBar = page.getByPlaceholder('Search commands')
await expect(cmdSearchBar).toBeVisible() await expect(cmdSearchBar).toBeVisible()
await page.keyboard.press('Escape') await page.keyboard.press('Escape')
cmdSearchBar = page.getByPlaceholder('Search commands')
await expect(cmdSearchBar).not.toBeVisible() await expect(cmdSearchBar).not.toBeVisible()
// Now try the same, but with the keyboard shortcut, check focus // Now try the same, but with the keyboard shortcut, check focus
await page.keyboard.press('Meta+K') await page.keyboard.press('Meta+K')
cmdSearchBar = page.getByPlaceholder('Search commands')
await expect(cmdSearchBar).toBeVisible() await expect(cmdSearchBar).toBeVisible()
await expect(cmdSearchBar).toBeFocused() await expect(cmdSearchBar).toBeFocused()
// Try typing in the command bar // Try typing in the command bar
await page.keyboard.type('theme') await cmdSearchBar.fill('theme')
const themeOption = page.getByRole('option', {
name: 'Settings · app · theme',
})
await expect(themeOption).toBeVisible() await expect(themeOption).toBeVisible()
await themeOption.click() await themeOption.click()
const themeInput = page.getByPlaceholder('Select an option') const themeInput = page.getByPlaceholder('Select an option')
@ -3553,6 +3641,24 @@ test.describe('Command bar tests', () => {
).toBeVisible() ).toBeVisible()
// Check that the theme changed // Check that the theme changed
await expect(page.locator('body')).not.toHaveClass(`body-bg dark`) await expect(page.locator('body')).not.toHaveClass(`body-bg dark`)
commandOptionInput = page.getByPlaceholder('system')
// Test case for https://github.com/KittyCAD/modeling-app/issues/2882
await commandBarButton.click()
await cmdSearchBar.focus()
await cmdSearchBar.fill('theme')
await themeOption.click()
await expect(commandThemeArgButton).toBeDisabled()
await commandOptionInput.focus()
await commandOptionInput.fill('lig')
await commandLevelArgButton.click()
await expect(commandLevelArgButton).toBeDisabled()
// Test case for https://github.com/KittyCAD/modeling-app/issues/2881
await commandThemeArgButton.click()
await expect(commandThemeArgButton).toBeDisabled()
await expect(commandLevelArgButton).toHaveText('level: project')
}) })
test('Command bar keybinding works from code editor and can change a setting', async ({ test('Command bar keybinding works from code editor and can change a setting', async ({
@ -3577,7 +3683,7 @@ test.describe('Command bar tests', () => {
await expect(cmdSearchBar).toBeFocused() await expect(cmdSearchBar).toBeFocused()
// Try typing in the command bar // Try typing in the command bar
await page.keyboard.type('theme') await cmdSearchBar.fill('theme')
const themeOption = page.getByRole('option', { const themeOption = page.getByRole('option', {
name: 'Settings · app · theme', name: 'Settings · app · theme',
}) })
@ -3648,7 +3754,9 @@ test.describe('Command bar tests', () => {
await page.mouse.click(700, 200) await page.mouse.click(700, 200)
// Assert that we're on the distance step // Assert that we're on the distance step
await expect(page.getByRole('button', { name: 'distance' })).toBeDisabled() await expect(
page.getByRole('button', { name: 'distance', exact: false })
).toBeDisabled()
// Assert that the an alternative variable name is chosen, // Assert that the an alternative variable name is chosen,
// since the default variable name is already in use (distance) // since the default variable name is already in use (distance)
@ -3663,11 +3771,12 @@ test.describe('Command bar tests', () => {
// Review step and argument hotkeys // Review step and argument hotkeys
await expect(submitButton).toBeEnabled() await expect(submitButton).toBeEnabled()
await page.keyboard.press('Backspace') await expect(submitButton).toBeFocused()
await submitButton.press('Backspace')
// Assert we're back on the distance step // Assert we're back on the distance step
await expect( await expect(
page.getByRole('button', { name: 'Distance 5', exact: false }) page.getByRole('button', { name: 'distance', exact: false })
).toBeDisabled() ).toBeDisabled()
await continueButton.click() await continueButton.click()
@ -3691,6 +3800,47 @@ const extrude001 = extrude(distance001, sketch001)`.replace(
) // remove newlines ) // remove newlines
) )
}) })
test('Can switch between sketch tools via command bar', async ({ page }) => {
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
const sketchButton = page.getByRole('button', { name: 'Start Sketch' })
const cmdBarButton = page.getByRole('button', { name: 'Commands' })
const rectangleToolCommand = page.getByRole('option', {
name: 'Rectangle',
})
const rectangleToolButton = page.getByRole('button', { name: 'Rectangle' })
const lineToolCommand = page.getByRole('option', { name: 'Line' })
const lineToolButton = page.getByRole('button', { name: 'Line' })
const arcToolCommand = page.getByRole('option', { name: 'Tangential Arc' })
const arcToolButton = page.getByRole('button', { name: 'Tangential Arc' })
// Start a sketch
await sketchButton.click()
await page.mouse.click(700, 200)
// Switch between sketch tools via the command bar
await expect(lineToolButton).toHaveAttribute('aria-pressed', 'true')
await cmdBarButton.click()
await rectangleToolCommand.click()
await expect(rectangleToolButton).toHaveAttribute('aria-pressed', 'true')
await cmdBarButton.click()
await lineToolCommand.click()
await expect(lineToolButton).toHaveAttribute('aria-pressed', 'true')
// Click in the scene a couple times to draw a line
// so tangential arc is valid
await page.mouse.click(700, 200)
await page.mouse.move(700, 300, { steps: 5 })
await page.mouse.click(700, 300)
// switch to tangential arc via command bar
await cmdBarButton.click()
await arcToolCommand.click()
await expect(arcToolButton).toHaveAttribute('aria-pressed', 'true')
})
}) })
test.describe('Regression tests', () => { test.describe('Regression tests', () => {
@ -4642,10 +4792,10 @@ test.describe('Sketch tests', () => {
// click extrude // click extrude
await page.getByRole('button', { name: 'Extrude' }).click() await page.getByRole('button', { name: 'Extrude' }).click()
// sketch selection should already have been made. "Selection 1 face" only show up when the selection has been made already // sketch selection should already have been made. "Selection: 1 face" only show up when the selection has been made already
// otherwise the cmdbar would be waiting for a selection. // otherwise the cmdbar would be waiting for a selection.
await expect( await expect(
page.getByRole('button', { name: 'Selection 1 face' }) page.getByRole('button', { name: 'selection : 1 face', exact: false })
).toBeVisible() ).toBeVisible()
}) })
test("Existing sketch with bad code delete user's code", async ({ page }) => { test("Existing sketch with bad code delete user's code", async ({ page }) => {
@ -7726,6 +7876,31 @@ test('Basic default modeling and sketch hotkeys work', async ({ page }) => {
await expect(page.locator('.cm-content')).toContainText('extrude(') await expect(page.locator('.cm-content')).toContainText('extrude(')
}) })
test('Delete key does not navigate back', async ({ page }) => {
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await page.waitForURL('**/file/**', { waitUntil: 'domcontentloaded' })
const settingsButton = page.getByRole('link', {
name: 'Settings',
exact: false,
})
const settingsCloseButton = page.getByTestId('settings-close-button')
await settingsButton.click()
await expect(page.url()).toContain('/settings')
// Make sure that delete doesn't go back from settings
await page.keyboard.press('Delete')
await expect(page.url()).toContain('/settings')
// Now close the settings and try delete again,
// make sure it doesn't go back to settings
await settingsCloseButton.click()
await page.keyboard.press('Delete')
await expect(page.url()).not.toContain('/settings')
})
test('Sketch on face', async ({ page }) => { test('Sketch on face', async ({ page }) => {
test.setTimeout(90_000) test.setTimeout(90_000)
const u = await getUtils(page) const u = await getUtils(page)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

View File

@ -1,6 +1,6 @@
{ {
"name": "untitled-app", "name": "untitled-app",
"version": "0.24.0", "version": "0.24.2",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@codemirror/autocomplete": "^6.17.0", "@codemirror/autocomplete": "^6.17.0",

View File

@ -30,7 +30,7 @@ import { URI } from 'vscode-uri'
import { LanguageServerClient } from '../client' import { LanguageServerClient } from '../client'
import { CompletionItemKindMap } from './autocomplete' import { CompletionItemKindMap } from './autocomplete'
import { addToken, SemanticToken } from './semantic-tokens' import { addToken, SemanticToken } from './semantic-tokens'
import { deferExecution, posToOffset, formatMarkdownContents } from './util' import { posToOffset, formatMarkdownContents } from './util'
import lspAutocompleteExt from './autocomplete' import lspAutocompleteExt from './autocomplete'
import lspHoverExt from './hover' import lspHoverExt from './hover'
import lspFormatExt from './format' import lspFormatExt from './format'
@ -93,23 +93,10 @@ export class LanguageServerPlugin implements PluginValue {
private doSemanticTokens: boolean = false private doSemanticTokens: boolean = false
private doFoldingRanges: boolean = false private doFoldingRanges: boolean = false
private _defferer = deferExecution((code: string) => { // When a doc update needs to be sent to the server, this holds the
try { // timeout handle for it. When null, the server has the up-to-date
// Update the state (not the editor) with the new code. // document.
this.client.textDocumentDidChange({ private sendScheduled: number | null = null
textDocument: {
uri: this.getDocUri(),
version: this.documentVersion++,
},
contentChanges: [{ text: code }],
})
this.requestSemanticTokens()
this.updateFoldingRanges()
} catch (e) {
console.error(e)
}
}, this.changesDelay)
constructor(options: LanguageServerOptions, private view: EditorView) { constructor(options: LanguageServerOptions, private view: EditorView) {
this.client = options.client this.client = options.client
@ -152,14 +139,9 @@ export class LanguageServerPlugin implements PluginValue {
} }
update(viewUpdate: ViewUpdate) { update(viewUpdate: ViewUpdate) {
// If the doc didn't change we can return early. if (viewUpdate.docChanged) {
if (!viewUpdate.docChanged) { this.scheduleSendDoc()
return
} }
this.sendChange({
documentText: viewUpdate.state.doc.toString(),
})
} }
destroy() { destroy() {
@ -184,16 +166,6 @@ export class LanguageServerPlugin implements PluginValue {
this.updateFoldingRanges() this.updateFoldingRanges()
} }
async sendChange({ documentText }: { documentText: string }) {
if (!this.client.ready) return
this._defferer(documentText)
}
requestDiagnostics() {
this.sendChange({ documentText: this.getDocText() })
}
async requestHoverTooltip( async requestHoverTooltip(
view: EditorView, view: EditorView,
{ line, character }: { line: number; character: number } { line, character }: { line: number; character: number }
@ -204,7 +176,7 @@ export class LanguageServerPlugin implements PluginValue {
) )
return null return null
this.sendChange({ documentText: this.getDocText() }) this.ensureDocSent()
const result = await this.client.textDocumentHover({ const result = await this.client.textDocumentHover({
textDocument: { uri: this.getDocUri() }, textDocument: { uri: this.getDocUri() },
position: { line, character }, position: { line, character },
@ -227,6 +199,42 @@ export class LanguageServerPlugin implements PluginValue {
return { pos, end, create: (view) => ({ dom }), above: true } return { pos, end, create: (view) => ({ dom }), above: true }
} }
scheduleSendDoc() {
if (this.sendScheduled != null) window.clearTimeout(this.sendScheduled)
this.sendScheduled = window.setTimeout(
() => this.sendDoc(),
this.changesDelay
)
}
sendDoc() {
if (this.sendScheduled != null) {
window.clearTimeout(this.sendScheduled)
this.sendScheduled = null
}
if (!this.client.ready) return
try {
// Update the state (not the editor) with the new code.
this.client.textDocumentDidChange({
textDocument: {
uri: this.getDocUri(),
version: this.documentVersion++,
},
contentChanges: [{ text: this.view.state.doc.toString() }],
})
this.requestSemanticTokens()
this.updateFoldingRanges()
} catch (e) {
console.error(e)
}
}
ensureDocSent() {
if (this.sendScheduled != null) this.sendDoc()
}
async getFoldingRanges(): Promise<LSP.FoldingRange[] | null> { async getFoldingRanges(): Promise<LSP.FoldingRange[] | null> {
if ( if (
!this.doFoldingRanges || !this.doFoldingRanges ||
@ -284,13 +292,7 @@ export class LanguageServerPlugin implements PluginValue {
) )
return null return null
this.client.textDocumentDidChange({ this.ensureDocSent()
textDocument: {
uri: this.getDocUri(),
version: this.documentVersion++,
},
contentChanges: [{ text: this.getDocText() }],
})
const result = await this.client.textDocumentFormatting({ const result = await this.client.textDocumentFormatting({
textDocument: { uri: this.getDocUri() }, textDocument: { uri: this.getDocUri() },
@ -330,9 +332,7 @@ export class LanguageServerPlugin implements PluginValue {
) )
return null return null
this.sendChange({ this.ensureDocSent()
documentText: context.state.doc.toString(),
})
const result = await this.client.textDocumentCompletion({ const result = await this.client.textDocumentCompletion({
textDocument: { uri: this.getDocUri() }, textDocument: { uri: this.getDocUri() },

View File

@ -20,7 +20,10 @@ export default defineConfig({
/* Different amount of parallelism on CI and local. */ /* Different amount of parallelism on CI and local. */
workers: process.env.CI ? 4 : 4, workers: process.env.CI ? 4 : 4,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */ /* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html', reporter: [
[process.env.CI ? 'dot' : 'list'],
['json', { outputFile: './test-results/report.json' }],
],
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: { use: {
/* Base URL to use in actions like `await page.goto('/')`. */ /* Base URL to use in actions like `await page.goto('/')`. */

65
playwrightProcess.mjs Normal file
View File

@ -0,0 +1,65 @@
import { readFileSync } from 'fs'
const data = readFileSync('./test-results/report.json', 'utf8')
// types, but was easier to store and run as normal js
// interface FailedTest {
// name: string;
// projectName: string;
// error: string;
// }
// interface Spec {
// title: string;
// tests: Test[];
// }
// interface Test {
// expectedStatus: 'passed' | 'failed' | 'pending';
// projectName: string;
// title: string;
// results: {
// status: 'passed' | 'failed' | 'pending';
// error: {stack: string}
// }[]
// }
// interface Suite {
// title: string
// suites: Suite[];
// specs: Spec[];
// }
// const processReport = (suites: Suite[]): FailedTest[] => {
// const failedTests: FailedTest[] = []
// const loopSuites = (suites: Suite[], previousName = '') => {
const processReport = (suites) => {
const failedTests = []
const loopSuites = (suites, previousName = '') => {
if (!suites) return
for (const suite of suites) {
const name = (previousName ? `${previousName} -- ` : '') + suite.title
for (const spec of suite.specs) {
for (const test of spec.tests) {
for (const result of test.results) {
if ((result.status !== 'passed') && test.expectedStatus === 'passed') {
failedTests.push({
name: (name + ' -- ' + spec.title) + (test.title ? ` -- ${test.title}` : ''),
status: result.status,
projectName: test.projectName,
error: result.error?.stack,
})
}
}
}
}
loopSuites(suite.suites, name)
}
}
loopSuites(suites)
return failedTests.map(line => JSON.stringify(line)).join('\n')
}
const failedTests = processReport(JSON.parse(data).suites)
// log to stdout to be piped to axiom
console.log(failedTests)

130
src-tauri/Cargo.lock generated
View File

@ -332,7 +332,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -361,13 +361,13 @@ checksum = "fbb36e985947064623dbd357f727af08ffd077f93d696782f3c56365fa2e2799"
[[package]] [[package]]
name = "async-trait" name = "async-trait"
version = "0.1.80" version = "0.1.81"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -407,7 +407,7 @@ checksum = "3c87f3f15e7794432337fc718554eaa4dc8f04c9677a950ffe366f20a162ae42"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -550,7 +550,7 @@ dependencies = [
"proc-macro-crate 3.1.0", "proc-macro-crate 3.1.0",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
"syn_derive", "syn_derive",
] ]
@ -792,9 +792,9 @@ dependencies = [
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.5.7" version = "4.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5db83dced34638ad474f39f250d7fea9598bdd239eaced1bdf45d597da0f433f" checksum = "64acc1846d54c1fe936a78dc189c34e28d3f5afc348403f28ecf53660b9b8462"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
"clap_derive", "clap_derive",
@ -802,9 +802,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_builder" name = "clap_builder"
version = "4.5.7" version = "4.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7e204572485eb3fbf28f871612191521df159bc3e15a9f5064c66dba3a8c05f" checksum = "6fb8393d67ba2e7bfaf28a23458e4e2b543cc73a99595511eb207fdb8aede942"
dependencies = [ dependencies = [
"anstream", "anstream",
"anstyle", "anstyle",
@ -816,14 +816,14 @@ dependencies = [
[[package]] [[package]]
name = "clap_derive" name = "clap_derive"
version = "4.5.5" version = "4.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c780290ccf4fb26629baa7a1081e68ced113f1d3ec302fa5948f1c381ebf06c6" checksum = "2bac35c6dafb060fd4d275d9a4ffae97917c13a6327903a8be2153cd964f7085"
dependencies = [ dependencies = [
"heck 0.5.0", "heck 0.5.0",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -1073,7 +1073,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331"
dependencies = [ dependencies = [
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -1083,7 +1083,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f" checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f"
dependencies = [ dependencies = [
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -1107,7 +1107,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"strsim 0.10.0", "strsim 0.10.0",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -1118,7 +1118,7 @@ checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f"
dependencies = [ dependencies = [
"darling_core", "darling_core",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -1179,7 +1179,7 @@ checksum = "4078275de501a61ceb9e759d37bdd3d7210e654dbc167ac1a3678ef4435ed57b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
"synstructure", "synstructure",
] ]
@ -1216,7 +1216,7 @@ dependencies = [
"regex", "regex",
"serde", "serde",
"serde_tokenstream", "serde_tokenstream",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -1227,7 +1227,7 @@ checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -1288,7 +1288,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -1320,7 +1320,7 @@ checksum = "f2b99bf03862d7f545ebc28ddd33a665b50865f4dfd84031a393823879bd4c54"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -1427,7 +1427,7 @@ checksum = "5c785274071b1b420972453b306eeca06acf4633829db4223b58a2a8c5953bc4"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -1588,7 +1588,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -1704,7 +1704,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -1980,7 +1980,7 @@ dependencies = [
"proc-macro-error", "proc-macro-error",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -2008,7 +2008,7 @@ dependencies = [
"inflections", "inflections",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -2083,7 +2083,7 @@ dependencies = [
"proc-macro-error", "proc-macro-error",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -2571,7 +2571,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-lib" name = "kcl-lib"
version = "0.1.70" version = "0.1.72"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"approx", "approx",
@ -3377,7 +3377,7 @@ dependencies = [
"regex", "regex",
"regex-syntax 0.8.3", "regex-syntax 0.8.3",
"structmeta", "structmeta",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -3496,7 +3496,7 @@ dependencies = [
"phf_shared 0.11.2", "phf_shared 0.11.2",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -3564,7 +3564,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -4438,7 +4438,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"serde_derive_internals", "serde_derive_internals",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -4523,9 +4523,9 @@ dependencies = [
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.203" version = "1.0.204"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
@ -4552,13 +4552,13 @@ dependencies = [
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.203" version = "1.0.204"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -4569,7 +4569,7 @@ checksum = "330f01ce65a3a5fe59a60c82f3c9a024b573b8a6e875bd233fe5f934e71d54e3"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -4602,7 +4602,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -4623,7 +4623,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"serde", "serde",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -4665,7 +4665,7 @@ dependencies = [
"darling", "darling",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -4933,7 +4933,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"structmeta-derive", "structmeta-derive",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -4944,7 +4944,7 @@ checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -4966,7 +4966,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"rustversion", "rustversion",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -4999,9 +4999,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.68" version = "2.0.70"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "901fa70d88b9d6c98022e23b4136f9f3e54e4662c3bc1bd1d84a42a9a0f0c1e9" checksum = "2f0209b68b3613b093e0ec905354eccaedcfe83b8cb37cbdeae64026c3064c16"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -5017,7 +5017,7 @@ dependencies = [
"proc-macro-error", "proc-macro-error",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -5034,7 +5034,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -5251,7 +5251,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"sha2", "sha2",
"syn 2.0.68", "syn 2.0.70",
"tauri-utils", "tauri-utils",
"thiserror", "thiserror",
"time", "time",
@ -5269,7 +5269,7 @@ dependencies = [
"heck 0.5.0", "heck 0.5.0",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
"tauri-codegen", "tauri-codegen",
"tauri-utils", "tauri-utils",
] ]
@ -5642,7 +5642,7 @@ checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -5740,7 +5740,7 @@ checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -5940,7 +5940,7 @@ checksum = "84fd902d4e0b9a4b27f2f440108dc034e1758628a9b702f8ec61ad66355422fa"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -5969,7 +5969,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -6099,7 +6099,7 @@ checksum = "c88cc88fd23b5a04528f3a8436024f20010a16ec18eb23c164b1242f65860130"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
"termcolor", "termcolor",
] ]
@ -6280,9 +6280,9 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "1.9.1" version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5de17fd2f7da591098415cff336e12965a28061ddace43b59cb3c430179c9439" checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314"
dependencies = [ dependencies = [
"getrandom 0.2.14", "getrandom 0.2.14",
"serde", "serde",
@ -6316,7 +6316,7 @@ dependencies = [
"proc-macro-error", "proc-macro-error",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -6415,7 +6415,7 @@ dependencies = [
"once_cell", "once_cell",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -6449,7 +6449,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
"wasm-bindgen-backend", "wasm-bindgen-backend",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -6590,7 +6590,7 @@ checksum = "ac1345798ecd8122468840bcdf1b95e5dc6d2206c5e4b0eafa078d061f59c9bc"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -6696,7 +6696,7 @@ checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -6707,7 +6707,7 @@ checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -7159,7 +7159,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]

View File

@ -80,5 +80,5 @@
} }
}, },
"productName": "Zoo Modeling App", "productName": "Zoo Modeling App",
"version": "0.24.0" "version": "0.24.2"
} }

View File

@ -39,3 +39,32 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => {
</AppStateContext.Provider> </AppStateContext.Provider>
) )
} }
interface AppStream {
mediaStream: MediaStream
setMediaStream: (mediaStream: MediaStream) => void
}
const AppStreamContext = createContext<AppStream>({
mediaStream: undefined as unknown as MediaStream,
setMediaStream: () => {},
})
export const useAppStream = () => useContext(AppStreamContext)
export const AppStreamProvider = ({ children }: { children: ReactNode }) => {
const [mediaStream, setMediaStream] = useState<MediaStream>(
undefined as unknown as MediaStream
)
return (
<AppStreamContext.Provider
value={{
mediaStream,
setMediaStream,
}}
>
{children}
</AppStreamContext.Provider>
)
}

View File

@ -16,6 +16,7 @@ import {
canRectangleTool, canRectangleTool,
isEditingExistingSketch, isEditingExistingSketch,
} from 'machines/modelingMachine' } from 'machines/modelingMachine'
import { DEV } from 'env'
export function Toolbar({ export function Toolbar({
className = '', className = '',
@ -60,7 +61,7 @@ export function Toolbar({
? send('CancelSketch') ? send('CancelSketch')
: send({ : send({
type: 'change tool', type: 'change tool',
data: 'line', data: { tool: 'line' },
}), }),
{ enabled: !disableLineButton, scopes: ['sketch'] } { enabled: !disableLineButton, scopes: ['sketch'] }
) )
@ -75,7 +76,7 @@ export function Toolbar({
? send('CancelSketch') ? send('CancelSketch')
: send({ : send({
type: 'change tool', type: 'change tool',
data: 'tangentialArc', data: { tool: 'tangentialArc' },
}), }),
{ enabled: !disableTangentialArc, scopes: ['sketch'] } { enabled: !disableTangentialArc, scopes: ['sketch'] }
) )
@ -89,7 +90,7 @@ export function Toolbar({
? send('CancelSketch') ? send('CancelSketch')
: send({ : send({
type: 'change tool', type: 'change tool',
data: 'rectangle', data: { tool: 'rectangle' },
}), }),
{ enabled: !disableRectangle, scopes: ['sketch'] } { enabled: !disableRectangle, scopes: ['sketch'] }
) )
@ -114,10 +115,20 @@ export function Toolbar({
() => () =>
commandBarSend({ commandBarSend({
type: 'Find and select command', type: 'Find and select command',
data: { name: 'Extrude', ownerMachine: 'modeling' }, data: { name: 'Extrude', groupId: 'modeling' },
}), }),
{ enabled: !disableAllButtons, scopes: ['modeling'] } { enabled: !disableAllButtons, scopes: ['modeling'] }
) )
const disableFillet = !state.can('Fillet') || disableAllButtons
useHotkeys(
'f',
() =>
commandBarSend({
type: 'Find and select command',
data: { name: 'Fillet', groupId: 'modeling' },
}),
{ enabled: !disableFillet, scopes: ['modeling'] }
)
function handleToolbarButtonsWheelEvent(ev: WheelEvent<HTMLSpanElement>) { function handleToolbarButtonsWheelEvent(ev: WheelEvent<HTMLSpanElement>) {
const span = toolbarButtonsRef.current const span = toolbarButtonsRef.current
@ -263,7 +274,7 @@ export function Toolbar({
? send('CancelSketch') ? send('CancelSketch')
: send({ : send({
type: 'change tool', type: 'change tool',
data: 'line', data: { tool: 'line' },
}) })
} }
aria-pressed={state?.matches('Sketch.Line tool')} aria-pressed={state?.matches('Sketch.Line tool')}
@ -293,7 +304,7 @@ export function Toolbar({
? send('CancelSketch') ? send('CancelSketch')
: send({ : send({
type: 'change tool', type: 'change tool',
data: 'tangentialArc', data: { tool: 'tangentialArc' },
}) })
} }
aria-pressed={state.matches('Sketch.Tangential arc to')} aria-pressed={state.matches('Sketch.Tangential arc to')}
@ -323,7 +334,7 @@ export function Toolbar({
? send('CancelSketch') ? send('CancelSketch')
: send({ : send({
type: 'change tool', type: 'change tool',
data: 'rectangle', data: { tool: 'rectangle' },
}) })
} }
aria-pressed={state.matches('Sketch.Rectangle tool')} aria-pressed={state.matches('Sketch.Rectangle tool')}
@ -378,7 +389,7 @@ export function Toolbar({
onClick={() => onClick={() =>
commandBarSend({ commandBarSend({
type: 'Find and select command', type: 'Find and select command',
data: { name: 'Extrude', ownerMachine: 'modeling' }, data: { name: 'Extrude', groupId: 'modeling' },
}) })
} }
disabled={!state.can('Extrude') || disableAllButtons} disabled={!state.can('Extrude') || disableAllButtons}
@ -404,6 +415,36 @@ export function Toolbar({
</ActionButton> </ActionButton>
</li> </li>
)} )}
{state.matches('idle') && (DEV || (window as any)._enableFillet) && (
<li className="contents">
<ActionButton
className={buttonClassName}
Element="button"
onClick={() =>
commandBarSend({
type: 'Find and select command',
data: { name: 'Fillet', groupId: 'modeling' },
})
}
disabled={disableFillet}
title={disableFillet ? 'fillet' : "edge can't be filleted"}
iconStart={{
icon: 'fillet', // todo: add fillet icon
iconClassName,
bgClassName,
}}
>
Fillet
<Tooltip
delay={1250}
position="bottom"
className="!px-2 !text-xs"
>
Shortcut: F
</Tooltip>
</ActionButton>
</li>
)}
</ul> </ul>
</menu> </menu>
) )

View File

@ -518,9 +518,9 @@ export class CameraControls {
direction.normalize() direction.normalize()
this.camera.position.copy(this.target).addScaledVector(direction, distance) this.camera.position.copy(this.target).addScaledVector(direction, distance)
} }
usePerspectiveCamera = async () => { usePerspectiveCamera = async (forceSend = false) => {
this._usePerspectiveCamera() this._usePerspectiveCamera()
if (this.syncDirection === 'clientToEngine') { if (forceSend || this.syncDirection === 'clientToEngine') {
await this.engineCommandManager.sendSceneCommand({ await this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req', type: 'modeling_cmd_req',
cmd_id: uuidv4(), cmd_id: uuidv4(),

View File

@ -717,7 +717,7 @@ export const CamDebugSettings = () => {
if (camSettings.type === 'perspective') { if (camSettings.type === 'perspective') {
sceneInfra.camControls.useOrthographicCamera() sceneInfra.camControls.useOrthographicCamera()
} else { } else {
sceneInfra.camControls.usePerspectiveCamera() sceneInfra.camControls.usePerspectiveCamera(true)
} }
}} }}
/> />

View File

@ -41,6 +41,7 @@ function CommandArgOptionInput({
) )
const inputRef = useRef<HTMLInputElement>(null) const inputRef = useRef<HTMLInputElement>(null)
const formRef = useRef<HTMLFormElement>(null) const formRef = useRef<HTMLFormElement>(null)
const [shouldSubmitOnChange, setShouldSubmitOnChange] = useState(false)
const [selectedOption, setSelectedOption] = useState< const [selectedOption, setSelectedOption] = useState<
CommandArgumentOption<unknown> CommandArgumentOption<unknown>
>(currentOption || resolvedOptions[0]) >(currentOption || resolvedOptions[0])
@ -82,8 +83,10 @@ function CommandArgOptionInput({
// We deal with the whole option object internally // We deal with the whole option object internally
setSelectedOption(option) setSelectedOption(option)
// But we only submit the value // But we only submit the value itself
onSubmit(option.value) if (shouldSubmitOnChange) {
onSubmit(option.value)
}
} }
function handleSubmit(e: React.FormEvent<HTMLFormElement>) { function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
@ -94,7 +97,18 @@ function CommandArgOptionInput({
} }
return ( return (
<form id="arg-form" onSubmit={handleSubmit} ref={formRef}> <form
id="arg-form"
onSubmit={handleSubmit}
ref={formRef}
onKeyDownCapture={(e) => {
if (e.key === 'Enter') {
setShouldSubmitOnChange(true)
} else {
setShouldSubmitOnChange(false)
}
}}
>
<Combobox <Combobox
value={selectedOption} value={selectedOption}
onChange={handleSelectOption} onChange={handleSelectOption}
@ -118,6 +132,12 @@ function CommandArgOptionInput({
if (event.key === 'Backspace' && !event.currentTarget.value) { if (event.key === 'Backspace' && !event.currentTarget.value) {
stepBack() stepBack()
} }
if (event.key === 'Enter') {
setShouldSubmitOnChange(true)
} else {
setShouldSubmitOnChange(false)
}
}} }}
value={query} value={query}
placeholder={ placeholder={
@ -136,6 +156,9 @@ function CommandArgOptionInput({
<Combobox.Options <Combobox.Options
static static
className="overflow-y-auto max-h-96 cursor-pointer" className="overflow-y-auto max-h-96 cursor-pointer"
onMouseDown={() => {
setShouldSubmitOnChange(true)
}}
> >
{filteredOptions?.map((option) => ( {filteredOptions?.map((option) => (
<Combobox.Option <Combobox.Option

View File

@ -114,6 +114,7 @@ function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
> >
{argName} {argName}
</span> </span>
<span className="sr-only">:&nbsp;</span>
{argValue ? ( {argValue ? (
arg.inputType === 'selection' ? ( arg.inputType === 'selection' ? (
getSelectionTypeDisplayText(argValue as Selections) getSelectionTypeDisplayText(argValue as Selections)

View File

@ -28,6 +28,11 @@ export const CommandBarProvider = ({
Object.keys(context.selectedCommand?.args).length === 0 Object.keys(context.selectedCommand?.args).length === 0
) )
}, },
'All arguments are skippable': (context, _event) => {
return Object.values(context.selectedCommand!.args!).every(
(argConfig) => argConfig.skip
)
},
}, },
}) })

View File

@ -3,6 +3,7 @@ import { useCommandsContext } from 'hooks/useCommandsContext'
import { useKclContext } from 'lang/KclProvider' import { useKclContext } from 'lang/KclProvider'
import { CommandArgument } from 'lib/commandTypes' import { CommandArgument } from 'lib/commandTypes'
import { import {
Selection,
canSubmitSelectionArg, canSubmitSelectionArg,
getSelectionType, getSelectionType,
getSelectionTypeDisplayText, getSelectionTypeDisplayText,
@ -11,6 +12,25 @@ import { modelingMachine } from 'machines/modelingMachine'
import { useCallback, useEffect, useRef, useState } from 'react' import { useCallback, useEffect, useRef, useState } from 'react'
import { StateFrom } from 'xstate' import { StateFrom } from 'xstate'
const semanticEntityNames: { [key: string]: Array<Selection['type']> } = {
face: ['extrude-wall', 'start-cap', 'end-cap'],
edge: ['edge', 'line', 'arc'],
point: ['point', 'line-end', 'line-mid'],
}
function getSemanticSelectionType(selectionType: Array<Selection['type']>) {
const semanticSelectionType = new Set()
selectionType.forEach((type) => {
Object.entries(semanticEntityNames).forEach(([entity, entityTypes]) => {
if (entityTypes.includes(type)) {
semanticSelectionType.add(entity)
}
})
})
return Array.from(semanticSelectionType)
}
const selectionSelector = (snapshot: StateFrom<typeof modelingMachine>) => const selectionSelector = (snapshot: StateFrom<typeof modelingMachine>) =>
snapshot.context.selectionRanges snapshot.context.selectionRanges
@ -85,7 +105,9 @@ function CommandBarSelectionInput({
> >
{canSubmitSelection {canSubmitSelection
? getSelectionTypeDisplayText(selection) + ' selected' ? getSelectionTypeDisplayText(selection) + ' selected'
: `Please select ${arg.multiple ? 'one or more faces' : 'one face'}`} : `Please select ${
arg.multiple ? 'one or more ' : 'one '
}${getSemanticSelectionType(arg.selectionTypes).join(' or ')}`}
<input <input
id="selection" id="selection"
name="selection" name="selection"

View File

@ -70,19 +70,23 @@ function CommandComboBox({
> >
{filteredOptions?.map((option) => ( {filteredOptions?.map((option) => (
<Combobox.Option <Combobox.Option
key={option.name} key={option.groupId + option.name + (option.displayName || '')}
value={option} value={option}
className="flex items-center gap-2 px-4 py-1 first:mt-2 last:mb-2 ui-active:bg-primary/10 dark:ui-active:bg-chalkboard-90" className="flex items-center gap-4 px-4 py-1.5 first:mt-2 last:mb-2 ui-active:bg-primary/10 dark:ui-active:bg-chalkboard-90"
> >
{'icon' in option && option.icon && ( {'icon' in option && option.icon && (
<CustomIcon name={option.icon} className="w-5 h-5" /> <CustomIcon name={option.icon} className="w-5 h-5" />
)} )}
<p className="flex-grow">{option.displayName || option.name} </p> <div className="flex-grow flex flex-col">
{option.description && ( <p className="my-0 leading-tight">
<p className="text-xs text-chalkboard-60 dark:text-chalkboard-40"> {option.displayName || option.name}{' '}
{option.description}
</p> </p>
)} {option.description && (
<p className="my-0 text-xs text-chalkboard-60 dark:text-chalkboard-50">
{option.description}
</p>
)}
</div>
</Combobox.Option> </Combobox.Option>
))} ))}
</Combobox.Options> </Combobox.Options>

View File

@ -131,6 +131,16 @@ const CustomIconMap = {
/> />
</svg> </svg>
), ),
code: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10.7071 5L7.77734 14.7794L8.73527 15.0663L11.665 5.28698L10.7071 5ZM2.35356 9.64644L5.85362 6.14644L6.56072 6.85355L3.41423 10L6.56072 13.1464L5.85362 13.8536L2.35356 10.3536L2 10L2.35356 9.64644ZM17.0607 9.64644L13.5607 6.14644L12.8536 6.85355L16 10L12.8536 13.1464L13.5607 13.8535L17.0607 10.3536L17.4142 10L17.0607 9.64644Z"
fill="currentColor"
/>
</svg>
),
dimension: ( dimension: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path <path
@ -177,6 +187,22 @@ const CustomIconMap = {
/> />
</svg> </svg>
), ),
fillet: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M8 5H5V15H15V12C15 8.13401 11.866 5 8 5ZM5 4H4V5V15V16H5H15H16V15V12C16 7.58172 12.4183 4 8 4H5Z"
fill="currentColor"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M4.5 3.5H5.5H8.5C12.9183 3.5 16.5 7.08172 16.5 11.5V14.5V15.5H16V12C16 7.58172 12.4182 4 7.99996 4H4.5V3.5Z"
fill="currentColor"
/>
</svg>
),
file: ( file: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path <path

View File

@ -42,7 +42,7 @@ import {
import { applyConstraintIntersect } from './Toolbar/Intersect' import { applyConstraintIntersect } from './Toolbar/Intersect'
import { applyConstraintAbsDistance } from './Toolbar/SetAbsDistance' import { applyConstraintAbsDistance } from './Toolbar/SetAbsDistance'
import useStateMachineCommands from 'hooks/useStateMachineCommands' import useStateMachineCommands from 'hooks/useStateMachineCommands'
import { modelingMachineConfig } from 'lib/commandBarConfigs/modelingCommandConfig' import { modelingMachineCommandConfig } from 'lib/commandBarConfigs/modelingCommandConfig'
import { import {
STRAIGHT_SEGMENT, STRAIGHT_SEGMENT,
TANGENTIAL_ARC_TO_SEGMENT, TANGENTIAL_ARC_TO_SEGMENT,
@ -72,6 +72,7 @@ import { uuidv4 } from 'lib/utils'
import { err, trap } from 'lib/trap' import { err, trap } from 'lib/trap'
import { useCommandsContext } from 'hooks/useCommandsContext' import { useCommandsContext } from 'hooks/useCommandsContext'
import { modelingMachineEvent } from 'editor/manager' import { modelingMachineEvent } from 'editor/manager'
import { hasValidFilletSelection } from 'lang/modifyAst/addFillet'
type MachineContext<T extends AnyStateMachine> = { type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T> state: StateFrom<T>
@ -164,7 +165,7 @@ export const ModelingMachineProvider = ({
store.videoElement?.pause() store.videoElement?.pause()
kclManager.executeCode(true).then(() => { kclManager.executeCode(true).then(() => {
if (engineCommandManager.engineConnection?.freezeFrame) return if (engineCommandManager.engineConnection?.idleMode) return
store.videoElement?.play() store.videoElement?.play()
}) })
@ -444,6 +445,12 @@ export const ModelingMachineProvider = ({
if (selectionRanges.codeBasedSelections.length <= 0) return false if (selectionRanges.codeBasedSelections.length <= 0) return false
return true return true
}, },
'has valid fillet selection': ({ selectionRanges }) =>
hasValidFilletSelection({
selectionRanges,
ast: kclManager.ast,
code: codeManager.code,
}),
'Selection is on face': ({ selectionRanges }, { data }) => { 'Selection is on face': ({ selectionRanges }, { data }) => {
if (data?.forceNewSketch) return false if (data?.forceNewSketch) return false
if (!isSingleCursorInPipe(selectionRanges, kclManager.ast)) if (!isSingleCursorInPipe(selectionRanges, kclManager.ast))
@ -494,7 +501,6 @@ export const ModelingMachineProvider = ({
kclManager.ast, kclManager.ast,
data.sketchPathToNode, data.sketchPathToNode,
data.extrudePathToNode, data.extrudePathToNode,
kclManager.programMemory,
data.cap data.cap
) )
if (trap(sketched)) return Promise.reject(sketched) if (trap(sketched)) return Promise.reject(sketched)
@ -920,7 +926,7 @@ export const ModelingMachineProvider = ({
state: modelingState, state: modelingState,
send: modelingSend, send: modelingSend,
actor: modelingActor, actor: modelingActor,
commandBarConfig: modelingMachineConfig, commandBarConfig: modelingMachineCommandConfig,
allCommandsRequireNetwork: true, allCommandsRequireNetwork: true,
// TODO for when sketch tools are in the toolbar: This was added when we used one "Cancel" event, // TODO for when sketch tools are in the toolbar: This was added when we used one "Cancel" event,
// but we need to support "SketchCancel" and basically // but we need to support "SketchCancel" and basically

View File

@ -82,11 +82,11 @@ function ProjectMenuPopover({
}) { }) {
const { commandBarState, commandBarSend } = useCommandsContext() const { commandBarState, commandBarSend } = useCommandsContext()
const { onProjectClose } = useLspContext() const { onProjectClose } = useLspContext()
const exportCommandInfo = { name: 'Export', ownerMachine: 'modeling' } const exportCommandInfo = { name: 'Export', groupId: 'modeling' }
const findCommand = (obj: { name: string; ownerMachine: string }) => const findCommand = (obj: { name: string; groupId: string }) =>
Boolean( Boolean(
commandBarState.context.commands.find( commandBarState.context.commands.find(
(c) => c.name === obj.name && c.ownerMachine === obj.ownerMachine (c) => c.name === obj.name && c.groupId === obj.groupId
) )
) )

View File

@ -6,9 +6,14 @@ import { useModelingContext } from 'hooks/useModelingContext'
import { useNetworkContext } from 'hooks/useNetworkContext' import { useNetworkContext } from 'hooks/useNetworkContext'
import { NetworkHealthState } from 'hooks/useNetworkStatus' import { NetworkHealthState } from 'hooks/useNetworkStatus'
import { ClientSideScene } from 'clientSideScene/ClientSideSceneComp' import { ClientSideScene } from 'clientSideScene/ClientSideSceneComp'
import { butName } from 'lib/cameraControls' import { btnName } from 'lib/cameraControls'
import { sendSelectEventToEngine } from 'lib/selections' import { sendSelectEventToEngine } from 'lib/selections'
import { kclManager, engineCommandManager, sceneInfra } from 'lib/singletons' import { kclManager, engineCommandManager, sceneInfra } from 'lib/singletons'
import { useAppStream } from 'AppState'
import {
EngineConnectionStateType,
DisconnectingType,
} from 'lang/std/engineConnection'
export const Stream = () => { export const Stream = () => {
const [isLoading, setIsLoading] = useState(true) const [isLoading, setIsLoading] = useState(true)
@ -17,13 +22,29 @@ export const Stream = () => {
const videoRef = useRef<HTMLVideoElement>(null) const videoRef = useRef<HTMLVideoElement>(null)
const { settings } = useSettingsAuthContext() const { settings } = useSettingsAuthContext()
const { state, send, context } = useModelingContext() const { state, send, context } = useModelingContext()
const { overallState } = useNetworkContext() const { mediaStream } = useAppStream()
const { overallState, immediateState } = useNetworkContext()
const [isFreezeFrame, setIsFreezeFrame] = useState(false) const [isFreezeFrame, setIsFreezeFrame] = useState(false)
const [isPaused, setIsPaused] = useState(false)
const IDLE = settings.context.app.streamIdleMode.current
const isNetworkOkay = const isNetworkOkay =
overallState === NetworkHealthState.Ok || overallState === NetworkHealthState.Ok ||
overallState === NetworkHealthState.Weak overallState === NetworkHealthState.Weak
useEffect(() => {
if (
immediateState.type === EngineConnectionStateType.Disconnecting &&
immediateState.value.type === DisconnectingType.Pause
) {
setIsPaused(true)
}
if (immediateState.type === EngineConnectionStateType.Connecting) {
setIsPaused(false)
}
}, [immediateState])
// Linux has a default behavior to paste text on middle mouse up // Linux has a default behavior to paste text on middle mouse up
// This adds a listener to block that pasting if the click target // This adds a listener to block that pasting if the click target
// is not a text input, so users can move in the 3D scene with // is not a text input, so users can move in the 3D scene with
@ -51,7 +72,7 @@ export const Stream = () => {
capture: true, capture: true,
}) })
const IDLE_TIME_MS = 1000 * 20 const IDLE_TIME_MS = 1000 * 60 * 2
let timeoutIdIdleA: ReturnType<typeof setTimeout> | undefined = undefined let timeoutIdIdleA: ReturnType<typeof setTimeout> | undefined = undefined
const teardown = () => { const teardown = () => {
@ -60,57 +81,78 @@ export const Stream = () => {
sceneInfra.modelingSend({ type: 'Cancel' }) sceneInfra.modelingSend({ type: 'Cancel' })
// Give video time to pause // Give video time to pause
window.requestAnimationFrame(() => { window.requestAnimationFrame(() => {
engineCommandManager.engineConnection?.tearDown({ freeze: true }) engineCommandManager.tearDown({ idleMode: true })
}) })
} }
// Teardown everything if we go hidden or reconnect const onVisibilityChange = () => {
if (globalThis?.window?.document) { if (globalThis.window.document.visibilityState === 'hidden') {
globalThis.window.document.onvisibilitychange = () => { clearTimeout(timeoutIdIdleA)
if (globalThis.window.document.visibilityState === 'hidden') { timeoutIdIdleA = setTimeout(teardown, IDLE_TIME_MS)
clearTimeout(timeoutIdIdleA) } else if (!engineCommandManager.engineConnection?.isReady()) {
timeoutIdIdleA = setTimeout(teardown, IDLE_TIME_MS) clearTimeout(timeoutIdIdleA)
} else if (!engineCommandManager.engineConnection?.isReady()) { engineCommandManager.engineConnection?.connect(true)
clearTimeout(timeoutIdIdleA)
engineCommandManager.engineConnection?.connect(true)
}
} }
} }
// Teardown everything if we go hidden or reconnect
if (IDLE) {
globalThis?.window?.document?.addEventListener(
'visibilitychange',
onVisibilityChange
)
}
let timeoutIdIdleB: ReturnType<typeof setTimeout> | undefined = undefined let timeoutIdIdleB: ReturnType<typeof setTimeout> | undefined = undefined
const onAnyInput = () => { const onAnyInput = () => {
if (!engineCommandManager.engineConnection?.isReady()) {
engineCommandManager.engineConnection?.connect(true)
}
// Clear both timers // Clear both timers
clearTimeout(timeoutIdIdleA) clearTimeout(timeoutIdIdleA)
clearTimeout(timeoutIdIdleB) clearTimeout(timeoutIdIdleB)
timeoutIdIdleB = setTimeout(teardown, IDLE_TIME_MS) timeoutIdIdleB = setTimeout(teardown, IDLE_TIME_MS)
} }
globalThis?.window?.document?.addEventListener('keydown', onAnyInput) if (IDLE) {
globalThis?.window?.document?.addEventListener('mousemove', onAnyInput) globalThis?.window?.document?.addEventListener('keydown', onAnyInput)
globalThis?.window?.document?.addEventListener('mousedown', onAnyInput) globalThis?.window?.document?.addEventListener('mousemove', onAnyInput)
globalThis?.window?.document?.addEventListener('scroll', onAnyInput) globalThis?.window?.document?.addEventListener('mousedown', onAnyInput)
globalThis?.window?.document?.addEventListener('touchstart', onAnyInput) globalThis?.window?.document?.addEventListener('scroll', onAnyInput)
globalThis?.window?.document?.addEventListener('touchstart', onAnyInput)
}
timeoutIdIdleB = setTimeout(teardown, IDLE_TIME_MS) if (IDLE) {
timeoutIdIdleB = setTimeout(teardown, IDLE_TIME_MS)
}
return () => { return () => {
globalThis?.window?.document?.removeEventListener('paste', handlePaste, { globalThis?.window?.document?.removeEventListener('paste', handlePaste, {
capture: true, capture: true,
}) })
globalThis?.window?.document?.removeEventListener('keydown', onAnyInput) if (IDLE) {
globalThis?.window?.document?.removeEventListener('mousemove', onAnyInput) clearTimeout(timeoutIdIdleA)
globalThis?.window?.document?.removeEventListener('mousedown', onAnyInput) clearTimeout(timeoutIdIdleB)
globalThis?.window?.document?.removeEventListener('scroll', onAnyInput)
globalThis?.window?.document?.removeEventListener( globalThis?.window?.document?.removeEventListener(
'touchstart', 'visibilitychange',
onAnyInput onVisibilityChange
) )
globalThis?.window?.document?.removeEventListener('keydown', onAnyInput)
globalThis?.window?.document?.removeEventListener(
'mousemove',
onAnyInput
)
globalThis?.window?.document?.removeEventListener(
'mousedown',
onAnyInput
)
globalThis?.window?.document?.removeEventListener('scroll', onAnyInput)
globalThis?.window?.document?.removeEventListener(
'touchstart',
onAnyInput
)
}
} }
}, []) }, [IDLE])
useEffect(() => { useEffect(() => {
setIsFirstRender(kclManager.isFirstRender) setIsFirstRender(kclManager.isFirstRender)
@ -124,10 +166,10 @@ export const Stream = () => {
) )
return return
if (!videoRef.current) return if (!videoRef.current) return
if (!context.store?.mediaStream) return if (!mediaStream) return
// Do not immediately play the stream! // Do not immediately play the stream!
videoRef.current.srcObject = context.store.mediaStream videoRef.current.srcObject = mediaStream
videoRef.current.pause() videoRef.current.pause()
send({ send({
@ -136,7 +178,7 @@ export const Stream = () => {
videoElement: videoRef.current, videoElement: videoRef.current,
}, },
}) })
}, [context.store?.mediaStream]) }, [mediaStream])
const handleMouseDown: MouseEventHandler<HTMLDivElement> = (e) => { const handleMouseDown: MouseEventHandler<HTMLDivElement> = (e) => {
if (!isNetworkOkay) return if (!isNetworkOkay) return
@ -172,7 +214,7 @@ export const Stream = () => {
if (state.matches('Sketch')) return if (state.matches('Sketch')) return
if (state.matches('Sketch no face')) return if (state.matches('Sketch no face')) return
if (!context.store?.didDragInStream && butName(e).left) { if (!context.store?.didDragInStream && btnName(e).left) {
sendSelectEventToEngine( sendSelectEventToEngine(
e, e,
videoRef.current, videoRef.current,
@ -233,6 +275,32 @@ export const Stream = () => {
<ClientSideScene <ClientSideScene
cameraControls={settings.context.modeling.mouseControls.current} cameraControls={settings.context.modeling.mouseControls.current}
/> />
{isPaused && (
<div className="text-center absolute inset-0">
<div
className="flex flex-col items-center justify-center h-screen"
data-testid="paused"
>
<div className="border-primary border p-2 rounded-sm">
<svg
width="8"
height="12"
viewBox="0 0 8 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M2 12V0H0V12H2ZM8 12V0H6V12H8Z"
fill="var(--primary)"
/>
</svg>
</div>
<p className="text-base mt-2 text-primary bold">Paused</p>
</div>
</div>
)}
{(!isNetworkOkay || isLoading || isFirstRender) && !isFreezeFrame && ( {(!isNetworkOkay || isLoading || isFirstRender) && !isFreezeFrame && (
<div className="text-center absolute inset-0"> <div className="text-center absolute inset-0">
<Loading> <Loading>

View File

@ -195,14 +195,15 @@ export class CompletionRequester implements PluginValue {
private queuedUids: string[] = [] private queuedUids: string[] = []
private _deffererCodeUpdate = deferExecution(() => {
this.requestCompletions()
}, changesDelay)
private _deffererUserSelect = deferExecution(() => { private _deffererUserSelect = deferExecution(() => {
this.rejectSuggestionCommand() this.rejectSuggestionCommand()
}, changesDelay) }, changesDelay)
// When a doc update needs to be sent to the server, this holds the
// timeout handle for it. When null, the server has the up-to-date
// document.
private sendScheduledInput: number | null = null
constructor(readonly view: EditorView, client: LanguageServerClient) { constructor(readonly view: EditorView, client: LanguageServerClient) {
this.client = client this.client = client
} }
@ -245,7 +246,34 @@ export class CompletionRequester implements PluginValue {
} }
this.lastPos = this.view.state.selection.main.head this.lastPos = this.view.state.selection.main.head
if (viewUpdate.docChanged) this._deffererCodeUpdate(true) if (viewUpdate.docChanged) this.scheduleUpdateDoc()
}
scheduleUpdateDoc() {
if (this.sendScheduledInput != null)
window.clearTimeout(this.sendScheduledInput)
this.sendScheduledInput = window.setTimeout(
() => this.updateDoc(),
changesDelay
)
}
updateDoc() {
if (this.sendScheduledInput != null) {
window.clearTimeout(this.sendScheduledInput)
this.sendScheduledInput = null
}
if (!this.client.ready) return
try {
this.requestCompletions()
} catch (e) {
console.error(e)
}
}
ensureDocUpdated() {
if (this.sendScheduledInput != null) this.updateDoc()
} }
ghostText(): GhostText | null { ghostText(): GhostText | null {

View File

@ -27,13 +27,10 @@ export class KclPlugin implements PluginValue {
this.client = client this.client = client
} }
private _deffererCodeUpdate = deferExecution(() => { // When a doc update needs to be sent to the server, this holds the
if (this.viewUpdate === null) { // timeout handle for it. When null, the server has the up-to-date
return // document.
} private sendScheduledInput: number | null = null
kclManager.executeCode()
}, changesDelay)
private _deffererUserSelect = deferExecution(() => { private _deffererUserSelect = deferExecution(() => {
if (this.viewUpdate === null) { if (this.viewUpdate === null) {
@ -101,7 +98,34 @@ export class KclPlugin implements PluginValue {
codeManager.code = newCode codeManager.code = newCode
codeManager.writeToFile() codeManager.writeToFile()
this._deffererCodeUpdate(true) this.scheduleUpdateDoc()
}
scheduleUpdateDoc() {
if (this.sendScheduledInput != null)
window.clearTimeout(this.sendScheduledInput)
this.sendScheduledInput = window.setTimeout(
() => this.updateDoc(),
changesDelay
)
}
updateDoc() {
if (this.sendScheduledInput != null) {
window.clearTimeout(this.sendScheduledInput)
this.sendScheduledInput = null
}
if (!this.client.ready) return
try {
kclManager.executeCode()
} catch (e) {
console.error(e)
}
}
ensureDocUpdated() {
if (this.sendScheduledInput != null) this.updateDoc()
} }
async updateUnits( async updateUnits(

View File

@ -1,11 +1,16 @@
import { createContext, useContext } from 'react' import { createContext, useContext } from 'react'
import { import {
ConnectingTypeGroup, ConnectingTypeGroup,
EngineConnectionStateType,
EngineConnectionState,
initialConnectingTypeGroupState, initialConnectingTypeGroupState,
} from '../lang/std/engineConnection' } from '../lang/std/engineConnection'
import { NetworkStatus, NetworkHealthState } from './useNetworkStatus' import { NetworkStatus, NetworkHealthState } from './useNetworkStatus'
export const NetworkContext = createContext<NetworkStatus>({ export const NetworkContext = createContext<NetworkStatus>({
immediateState: {
type: EngineConnectionStateType.Disconnected,
} as EngineConnectionState,
hasIssues: undefined, hasIssues: undefined,
overallState: NetworkHealthState.Disconnected, overallState: NetworkHealthState.Disconnected,
internetConnected: true, internetConnected: true,

View File

@ -6,6 +6,7 @@ import {
EngineCommandManagerEvents, EngineCommandManagerEvents,
EngineConnectionEvents, EngineConnectionEvents,
EngineConnectionStateType, EngineConnectionStateType,
EngineConnectionState,
ErrorType, ErrorType,
initialConnectingTypeGroupState, initialConnectingTypeGroupState,
} from '../lang/std/engineConnection' } from '../lang/std/engineConnection'
@ -19,6 +20,7 @@ export enum NetworkHealthState {
} }
export interface NetworkStatus { export interface NetworkStatus {
immediateState: EngineConnectionState
hasIssues: boolean | undefined hasIssues: boolean | undefined
overallState: NetworkHealthState overallState: NetworkHealthState
internetConnected: boolean internetConnected: boolean
@ -33,6 +35,9 @@ export interface NetworkStatus {
// Must be called from one place in the application. // Must be called from one place in the application.
// We've chosen the <Router /> component for this. // We've chosen the <Router /> component for this.
export function useNetworkStatus() { export function useNetworkStatus() {
const [immediateState, setImmediateState] = useState<EngineConnectionState>({
type: EngineConnectionStateType.Disconnected,
})
const [steps, setSteps] = useState( const [steps, setSteps] = useState(
structuredClone(initialConnectingTypeGroupState) structuredClone(initialConnectingTypeGroupState)
) )
@ -126,6 +131,7 @@ export function useNetworkStatus() {
const onConnectionStateChange = ({ const onConnectionStateChange = ({
detail: engineConnectionState, detail: engineConnectionState,
}: CustomEvent) => { }: CustomEvent) => {
setImmediateState(engineConnectionState)
setSteps((steps) => { setSteps((steps) => {
let nextSteps = structuredClone(steps) let nextSteps = structuredClone(steps)
@ -215,6 +221,7 @@ export function useNetworkStatus() {
}, []) }, [])
return { return {
immediateState,
hasIssues, hasIssues,
overallState, overallState,
internetConnected, internetConnected,

View File

@ -4,7 +4,7 @@ import { deferExecution } from 'lib/utils'
import { Themes } from 'lib/theme' import { Themes } from 'lib/theme'
import { makeDefaultPlanes, modifyGrid } from 'lang/wasm' import { makeDefaultPlanes, modifyGrid } from 'lang/wasm'
import { useModelingContext } from './useModelingContext' import { useModelingContext } from './useModelingContext'
import { useAppState } from 'AppState' import { useAppState, useAppStream } from 'AppState'
export function useSetupEngineManager( export function useSetupEngineManager(
streamRef: React.RefObject<HTMLDivElement>, streamRef: React.RefObject<HTMLDivElement>,
@ -28,9 +28,7 @@ export function useSetupEngineManager(
} }
) { ) {
const { setAppState } = useAppState() const { setAppState } = useAppState()
const { setMediaStream } = useAppStream()
const streamWidth = streamRef?.current?.offsetWidth
const streamHeight = streamRef?.current?.offsetHeight
const hasSetNonZeroDimensions = useRef<boolean>(false) const hasSetNonZeroDimensions = useRef<boolean>(false)
@ -40,59 +38,60 @@ export function useSetupEngineManager(
engineCommandManager.pool = settings.pool engineCommandManager.pool = settings.pool
} }
const startEngineInstance = () => { const startEngineInstance = (restart: boolean = false) => {
// Load the engine command manager once with the initial width and height, // Load the engine command manager once with the initial width and height,
// then we do not want to reload it. // then we do not want to reload it.
const { width: quadWidth, height: quadHeight } = getDimensions( const { width: quadWidth, height: quadHeight } = getDimensions(
streamWidth, streamRef?.current?.offsetWidth ?? 0,
streamHeight streamRef?.current?.offsetHeight ?? 0
) )
if ( if (restart) {
!hasSetNonZeroDimensions.current && kclManager.isFirstRender = false
quadHeight &&
quadWidth &&
settings.modelingSend
) {
engineCommandManager.start({
setMediaStream: (mediaStream) =>
settings.modelingSend({
type: 'Set context',
data: { mediaStream },
}),
setIsStreamReady: (isStreamReady) => setAppState({ isStreamReady }),
width: quadWidth,
height: quadHeight,
executeCode: () => {
// We only want to execute the code here that we already have set.
// Nothing else.
kclManager.isFirstRender = true
return kclManager.executeCode(true, true).then(() => {
kclManager.isFirstRender = false
})
},
token,
settings,
makeDefaultPlanes: () => {
return makeDefaultPlanes(kclManager.engineCommandManager)
},
modifyGrid: (hidden: boolean) => {
return modifyGrid(kclManager.engineCommandManager, hidden)
},
})
settings.modelingSend({
type: 'Set context',
data: {
streamDimensions: {
streamWidth: quadWidth,
streamHeight: quadHeight,
},
},
})
hasSetNonZeroDimensions.current = true
} }
engineCommandManager.start({
restart,
setMediaStream: (mediaStream) => setMediaStream(mediaStream),
setIsStreamReady: (isStreamReady) => setAppState({ isStreamReady }),
width: quadWidth,
height: quadHeight,
executeCode: () => {
// We only want to execute the code here that we already have set.
// Nothing else.
kclManager.isFirstRender = true
return kclManager.executeCode(true, true).then(() => {
kclManager.isFirstRender = false
})
},
token,
settings,
makeDefaultPlanes: () => {
return makeDefaultPlanes(kclManager.engineCommandManager)
},
modifyGrid: (hidden: boolean) => {
return modifyGrid(kclManager.engineCommandManager, hidden)
},
})
settings.modelingSend({
type: 'Set context',
data: {
streamDimensions: {
streamWidth: quadWidth,
streamHeight: quadHeight,
},
},
})
hasSetNonZeroDimensions.current = true
} }
useLayoutEffect(startEngineInstance, [ useLayoutEffect(() => {
const { width: quadWidth, height: quadHeight } = getDimensions(
streamRef?.current?.offsetWidth ?? 0,
streamRef?.current?.offsetHeight ?? 0
)
if (!hasSetNonZeroDimensions.current && quadHeight && quadWidth) {
startEngineInstance()
}
}, [
streamRef?.current?.offsetWidth, streamRef?.current?.offsetWidth,
streamRef?.current?.offsetHeight, streamRef?.current?.offsetHeight,
settings.modelingSend, settings.modelingSend,
@ -101,8 +100,8 @@ export function useSetupEngineManager(
useEffect(() => { useEffect(() => {
const handleResize = deferExecution(() => { const handleResize = deferExecution(() => {
const { width, height } = getDimensions( const { width, height } = getDimensions(
streamRef?.current?.offsetWidth, streamRef?.current?.offsetWidth ?? 0,
streamRef?.current?.offsetHeight streamRef?.current?.offsetHeight ?? 0
) )
if ( if (
settings.modelingContext.store.streamDimensions.streamWidth !== width || settings.modelingContext.store.streamDimensions.streamWidth !== width ||
@ -125,10 +124,37 @@ export function useSetupEngineManager(
}, 500) }, 500)
const onOnline = () => { const onOnline = () => {
startEngineInstance() startEngineInstance(true)
} }
const onVisibilityChange = () => {
if (window.document.visibilityState === 'visible') {
if (
!engineCommandManager.engineConnection?.isReady() &&
!engineCommandManager.engineConnection?.isConnecting()
) {
startEngineInstance()
}
}
}
window.document.addEventListener('visibilitychange', onVisibilityChange)
const onAnyInput = () => {
if (
!engineCommandManager.engineConnection?.isReady() &&
!engineCommandManager.engineConnection?.isConnecting()
) {
startEngineInstance()
}
}
window.document.addEventListener('keydown', onAnyInput)
window.document.addEventListener('mousemove', onAnyInput)
window.document.addEventListener('mousedown', onAnyInput)
window.document.addEventListener('scroll', onAnyInput)
window.document.addEventListener('touchstart', onAnyInput)
const onOffline = () => { const onOffline = () => {
kclManager.isFirstRender = true
engineCommandManager.tearDown() engineCommandManager.tearDown()
} }
@ -136,11 +162,30 @@ export function useSetupEngineManager(
window.addEventListener('offline', onOffline) window.addEventListener('offline', onOffline)
window.addEventListener('resize', handleResize) window.addEventListener('resize', handleResize)
return () => { return () => {
window.document.removeEventListener(
'visibilitychange',
onVisibilityChange
)
window.document.removeEventListener('keydown', onAnyInput)
window.document.removeEventListener('mousemove', onAnyInput)
window.document.removeEventListener('mousedown', onAnyInput)
window.document.removeEventListener('scroll', onAnyInput)
window.document.removeEventListener('touchstart', onAnyInput)
window.removeEventListener('online', onOnline) window.removeEventListener('online', onOnline)
window.removeEventListener('offline', onOffline) window.removeEventListener('offline', onOffline)
window.removeEventListener('resize', handleResize) window.removeEventListener('resize', handleResize)
} }
}, [])
// Engine relies on many settings so we should rebind events when it changes
// We have to list out the ones we care about because the settings object holds
// non-settings too...
}, [
settings.enableSSAO,
settings.highlightEdges,
settings.showScaleGrid,
settings.theme,
settings.pool,
])
} }
function getDimensions(streamWidth?: number, streamHeight?: number) { function getDimensions(streamWidth?: number, streamHeight?: number) {

View File

@ -6,7 +6,11 @@ import { modelingMachine } from 'machines/modelingMachine'
import { authMachine } from 'machines/authMachine' import { authMachine } from 'machines/authMachine'
import { settingsMachine } from 'machines/settingsMachine' import { settingsMachine } from 'machines/settingsMachine'
import { homeMachine } from 'machines/homeMachine' import { homeMachine } from 'machines/homeMachine'
import { Command, CommandSetConfig, CommandSetSchema } from 'lib/commandTypes' import {
Command,
StateMachineCommandSetConfig,
StateMachineCommandSetSchema,
} from 'lib/commandTypes'
import { useKclContext } from 'lang/KclProvider' import { useKclContext } from 'lang/KclProvider'
import { useNetworkContext } from 'hooks/useNetworkContext' import { useNetworkContext } from 'hooks/useNetworkContext'
import { NetworkHealthState } from 'hooks/useNetworkStatus' import { NetworkHealthState } from 'hooks/useNetworkStatus'
@ -21,20 +25,20 @@ export type AllMachines =
interface UseStateMachineCommandsArgs< interface UseStateMachineCommandsArgs<
T extends AllMachines, T extends AllMachines,
S extends CommandSetSchema<T> S extends StateMachineCommandSetSchema<T>
> { > {
machineId: T['id'] machineId: T['id']
state: StateFrom<T> state: StateFrom<T>
send: Function send: Function
actor: InterpreterFrom<T> actor: InterpreterFrom<T>
commandBarConfig?: CommandSetConfig<T, S> commandBarConfig?: StateMachineCommandSetConfig<T, S>
allCommandsRequireNetwork?: boolean allCommandsRequireNetwork?: boolean
onCancel?: () => void onCancel?: () => void
} }
export default function useStateMachineCommands< export default function useStateMachineCommands<
T extends AnyStateMachine, T extends AnyStateMachine,
S extends CommandSetSchema<T> S extends StateMachineCommandSetSchema<T>
>({ >({
machineId, machineId,
state, state,
@ -58,9 +62,10 @@ export default function useStateMachineCommands<
const newCommands = state.nextEvents const newCommands = state.nextEvents
.filter((_) => !allCommandsRequireNetwork || !disableAllButtons) .filter((_) => !allCommandsRequireNetwork || !disableAllButtons)
.filter((e) => !['done.', 'error.'].some((n) => e.includes(n))) .filter((e) => !['done.', 'error.'].some((n) => e.includes(n)))
.map((type) => .flatMap((type) =>
createMachineCommand<T, S>({ createMachineCommand<T, S>({
ownerMachine: machineId, // The group is the owner machine's ID.
groupId: machineId,
type, type,
state, state,
send, send,

View File

@ -13,6 +13,7 @@ import {
UpdaterRestartModal, UpdaterRestartModal,
createUpdaterRestartModal, createUpdaterRestartModal,
} from 'components/UpdaterRestartModal' } from 'components/UpdaterRestartModal'
import { AppStreamProvider } from 'AppState'
// uncomment for xstate inspector // uncomment for xstate inspector
// import { DEV } from 'env' // import { DEV } from 'env'
@ -26,28 +27,30 @@ const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
root.render( root.render(
<HotkeysProvider> <HotkeysProvider>
<Router /> <AppStreamProvider>
<Toaster <Router />
position="bottom-center" <Toaster
toastOptions={{ position="bottom-center"
style: { toastOptions={{
borderRadius: '3px', style: {
}, borderRadius: '3px',
className:
'bg-chalkboard-10 dark:bg-chalkboard-90 text-chalkboard-110 dark:text-chalkboard-10 rounded-sm border-chalkboard-20/50 dark:border-chalkboard-80/50',
success: {
iconTheme: {
primary: 'oklch(89% 0.16 143.4deg)',
secondary: 'oklch(48.62% 0.1654 142.5deg)',
}, },
duration: className:
window?.localStorage.getItem('playwright') === 'true' 'bg-chalkboard-10 dark:bg-chalkboard-90 text-chalkboard-110 dark:text-chalkboard-10 rounded-sm border-chalkboard-20/50 dark:border-chalkboard-80/50',
? 10 // speed up e2e tests success: {
: 1500, iconTheme: {
}, primary: 'oklch(89% 0.16 143.4deg)',
}} secondary: 'oklch(48.62% 0.1654 142.5deg)',
/> },
<ModalContainer /> duration:
window?.localStorage.getItem('playwright') === 'true'
? 10 // speed up e2e tests
: 1500,
},
}}
/>
<ModalContainer />
</AppStreamProvider>
</HotkeysProvider> </HotkeysProvider>
) )

View File

@ -3,6 +3,8 @@ import { createContext, useContext, useEffect, useState } from 'react'
import { type IndexLoaderData } from 'lib/types' import { type IndexLoaderData } from 'lib/types'
import { useLoaderData } from 'react-router-dom' import { useLoaderData } from 'react-router-dom'
import { codeManager, kclManager } from 'lib/singletons' import { codeManager, kclManager } from 'lib/singletons'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { Command } from 'lib/commandTypes'
const KclContext = createContext({ const KclContext = createContext({
code: codeManager?.code || '', code: codeManager?.code || '',
@ -35,6 +37,7 @@ export function KclContextProvider({
const [errors, setErrors] = useState<KCLError[]>([]) const [errors, setErrors] = useState<KCLError[]>([])
const [logs, setLogs] = useState<string[]>([]) const [logs, setLogs] = useState<string[]>([])
const [wasmInitFailed, setWasmInitFailed] = useState(false) const [wasmInitFailed, setWasmInitFailed] = useState(false)
const { commandBarSend } = useCommandsContext()
useEffect(() => { useEffect(() => {
codeManager.registerCallBacks({ codeManager.registerCallBacks({
@ -50,6 +53,28 @@ export function KclContextProvider({
}) })
}, []) }, [])
// Add format code to command palette.
useEffect(() => {
const commands: Command[] = [
{
name: 'format-code',
displayName: 'Format Code',
description: 'Nicely formats the KCL code in the editor.',
needsReview: false,
groupId: 'code',
icon: 'code',
onSubmit: (data) => {
kclManager.format()
},
},
]
commandBarSend({ type: 'Add commands', data: { commands } })
return () => {
commandBarSend({ type: 'Remove commands', data: { commands } })
}
}, [kclManager, commandBarSend])
return ( return (
<KclContext.Provider <KclContext.Provider
value={{ value={{

View File

@ -304,7 +304,6 @@ describe('testing sketchOnExtrudedFace', () => {
const ast = parse(code) const ast = parse(code)
if (err(ast)) throw ast if (err(ast)) throw ast
const programMemory = await enginelessExecutor(ast)
const segmentSnippet = `line([9.7, 9.19], %)` const segmentSnippet = `line([9.7, 9.19], %)`
const segmentRange: [number, number] = [ const segmentRange: [number, number] = [
code.indexOf(segmentSnippet), code.indexOf(segmentSnippet),
@ -321,8 +320,7 @@ describe('testing sketchOnExtrudedFace', () => {
const extruded = sketchOnExtrudedFace( const extruded = sketchOnExtrudedFace(
ast, ast,
segmentPathToNode, segmentPathToNode,
extrudePathToNode, extrudePathToNode
programMemory
) )
if (err(extruded)) throw extruded if (err(extruded)) throw extruded
const { modifiedAst } = extruded const { modifiedAst } = extruded
@ -345,7 +343,6 @@ const sketch001 = startSketchOn(part001, seg01)`)
|> extrude(5 + 7, %)` |> extrude(5 + 7, %)`
const ast = parse(code) const ast = parse(code)
if (err(ast)) throw ast if (err(ast)) throw ast
const programMemory = await enginelessExecutor(ast)
const segmentSnippet = `close(%)` const segmentSnippet = `close(%)`
const segmentRange: [number, number] = [ const segmentRange: [number, number] = [
code.indexOf(segmentSnippet), code.indexOf(segmentSnippet),
@ -362,8 +359,7 @@ const sketch001 = startSketchOn(part001, seg01)`)
const extruded = sketchOnExtrudedFace( const extruded = sketchOnExtrudedFace(
ast, ast,
segmentPathToNode, segmentPathToNode,
extrudePathToNode, extrudePathToNode
programMemory
) )
if (err(extruded)) throw extruded if (err(extruded)) throw extruded
const { modifiedAst } = extruded const { modifiedAst } = extruded
@ -386,7 +382,6 @@ const sketch001 = startSketchOn(part001, seg01)`)
|> extrude(5 + 7, %)` |> extrude(5 + 7, %)`
const ast = parse(code) const ast = parse(code)
if (err(ast)) throw ast if (err(ast)) throw ast
const programMemory = await enginelessExecutor(ast)
const sketchSnippet = `startProfileAt([3.58, 2.06], %)` const sketchSnippet = `startProfileAt([3.58, 2.06], %)`
const sketchRange: [number, number] = [ const sketchRange: [number, number] = [
code.indexOf(sketchSnippet), code.indexOf(sketchSnippet),
@ -404,7 +399,6 @@ const sketch001 = startSketchOn(part001, seg01)`)
ast, ast,
sketchPathToNode, sketchPathToNode,
extrudePathToNode, extrudePathToNode,
programMemory,
'end' 'end'
) )
if (err(extruded)) throw extruded if (err(extruded)) throw extruded
@ -436,7 +430,6 @@ const sketch001 = startSketchOn(part001, 'END')`)
const part001 = extrude(5 + 7, sketch001)` const part001 = extrude(5 + 7, sketch001)`
const ast = parse(code) const ast = parse(code)
if (err(ast)) throw ast if (err(ast)) throw ast
const programMemory = await enginelessExecutor(ast)
const segmentSnippet = `line([4.99, -0.46], %)` const segmentSnippet = `line([4.99, -0.46], %)`
const segmentRange: [number, number] = [ const segmentRange: [number, number] = [
code.indexOf(segmentSnippet), code.indexOf(segmentSnippet),
@ -453,8 +446,7 @@ const sketch001 = startSketchOn(part001, 'END')`)
const updatedAst = sketchOnExtrudedFace( const updatedAst = sketchOnExtrudedFace(
ast, ast,
segmentPathToNode, segmentPathToNode,
extrudePathToNode, extrudePathToNode
programMemory
) )
if (err(updatedAst)) throw updatedAst if (err(updatedAst)) throw updatedAst
const newCode = recast(updatedAst.modifiedAst) const newCode = recast(updatedAst.modifiedAst)

View File

@ -349,7 +349,6 @@ export function sketchOnExtrudedFace(
node: Program, node: Program,
sketchPathToNode: PathToNode, sketchPathToNode: PathToNode,
extrudePathToNode: PathToNode, extrudePathToNode: PathToNode,
programMemory: ProgramMemory,
cap: 'none' | 'start' | 'end' = 'none' cap: 'none' | 'start' | 'end' = 'none'
): { modifiedAst: Program; pathToNode: PathToNode } | Error { ): { modifiedAst: Program; pathToNode: PathToNode } | Error {
let _node = { ...node } let _node = { ...node }
@ -388,7 +387,6 @@ export function sketchOnExtrudedFace(
if (cap === 'none') { if (cap === 'none') {
const __tag = addTagForSketchOnFace( const __tag = addTagForSketchOnFace(
{ {
previousProgramMemory: programMemory,
pathToNode: sketchPathToNode, pathToNode: sketchPathToNode,
node: _node, node: _node,
}, },

View File

@ -0,0 +1,315 @@
import {
parse,
recast,
initPromise,
PathToNode,
Value,
Program,
CallExpression,
} from '../wasm'
import { addFillet, isTagUsedInFillet } from './addFillet'
import { getNodeFromPath, getNodePathFromSourceRange } from '../queryAst'
import { createLiteral } from 'lang/modifyAst'
import { err } from 'lib/trap'
beforeAll(async () => {
await initPromise // Initialize the WASM environment before running tests
})
const runFilletTest = async (
code: string,
segmentSnippet: string,
extrudeSnippet: string,
radius = createLiteral(5) as Value,
expectedCode: string
) => {
const astOrError = parse(code)
if (astOrError instanceof Error) {
return new Error('AST not found')
}
const ast = astOrError as Program
const segmentRange: [number, number] = [
code.indexOf(segmentSnippet),
code.indexOf(segmentSnippet) + segmentSnippet.length,
]
const pathToSegmentNode: PathToNode = getNodePathFromSourceRange(
ast,
segmentRange
)
const extrudeRange: [number, number] = [
code.indexOf(extrudeSnippet),
code.indexOf(extrudeSnippet) + extrudeSnippet.length,
]
const pathToExtrudeNode: PathToNode = getNodePathFromSourceRange(
ast,
extrudeRange
)
if (pathToExtrudeNode instanceof Error) {
return new Error('Path to extrude node not found')
}
// const radius = createLiteral(5) as Value
const result = addFillet(ast, pathToSegmentNode, pathToExtrudeNode, radius)
if (result instanceof Error) {
return result
}
const { modifiedAst } = result
const newCode = recast(modifiedAst)
expect(newCode).toContain(expectedCode)
}
describe('Testing addFillet', () => {
/**
* 1. Ideal Case
*/
it('should add a fillet to a specific segment after extrusion, clean', async () => {
const code = `
const sketch001 = startSketchOn('XZ')
|> startProfileAt([2.16, 49.67], %)
|> line([101.49, 139.93], %)
|> line([60.04, -55.72], %)
|> line([1.29, -115.74], %)
|> line([-87.24, -47.08], %)
|> tangentialArcTo([56.15, -94.58], %)
|> tangentialArcTo([14.68, -104.52], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const extrude001 = extrude(50, sketch001)
`
const segmentSnippet = `line([60.04, -55.72], %)`
const extrudeSnippet = `const extrude001 = extrude(50, sketch001)`
const radius = createLiteral(5) as Value
const expectedCode = `const sketch001 = startSketchOn('XZ')
|> startProfileAt([2.16, 49.67], %)
|> line([101.49, 139.93], %)
|> line([60.04, -55.72], %, $seg01)
|> line([1.29, -115.74], %)
|> line([-87.24, -47.08], %)
|> tangentialArcTo([56.15, -94.58], %)
|> tangentialArcTo([14.68, -104.52], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const extrude001 = extrude(50, sketch001)
|> fillet({ radius: 5, tags: [seg01] }, %)`
await runFilletTest(
code,
segmentSnippet,
extrudeSnippet,
radius,
expectedCode
)
})
/**
* 2. Case of existing tag in the other line
*/
it('should add a fillet to a specific segment after extrusion with existing tag in any other line', async () => {
const code = `
const sketch001 = startSketchOn('XZ')
|> startProfileAt([2.16, 49.67], %)
|> line([101.49, 139.93], %)
|> line([60.04, -55.72], %)
|> line([1.29, -115.74], %)
|> line([-87.24, -47.08], %, $seg01)
|> tangentialArcTo([56.15, -94.58], %)
|> tangentialArcTo([14.68, -104.52], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const extrude001 = extrude(50, sketch001)
`
const segmentSnippet = `line([60.04, -55.72], %)`
const extrudeSnippet = `const extrude001 = extrude(50, sketch001)`
const radius = createLiteral(5) as Value
const expectedCode = `const sketch001 = startSketchOn('XZ')
|> startProfileAt([2.16, 49.67], %)
|> line([101.49, 139.93], %)
|> line([60.04, -55.72], %, $seg02)
|> line([1.29, -115.74], %)
|> line([-87.24, -47.08], %, $seg01)
|> tangentialArcTo([56.15, -94.58], %)
|> tangentialArcTo([14.68, -104.52], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const extrude001 = extrude(50, sketch001)
|> fillet({ radius: 5, tags: [seg02] }, %)`
await runFilletTest(
code,
segmentSnippet,
extrudeSnippet,
radius,
expectedCode
)
})
/**
* 3. Case of existing tag in the fillet line
*/
it('should add a fillet to a specific segment after extrusion with existing tag in that exact line', async () => {
const code = `
const sketch001 = startSketchOn('XZ')
|> startProfileAt([2.16, 49.67], %)
|> line([101.49, 139.93], %)
|> line([60.04, -55.72], %)
|> line([1.29, -115.74], %)
|> line([-87.24, -47.08], %, $seg03)
|> tangentialArcTo([56.15, -94.58], %)
|> tangentialArcTo([14.68, -104.52], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const extrude001 = extrude(50, sketch001)
`
const segmentSnippet = `line([-87.24, -47.08], %, $seg03)`
const extrudeSnippet = `const extrude001 = extrude(50, sketch001)`
const radius = createLiteral(5) as Value
const expectedCode = `const sketch001 = startSketchOn('XZ')
|> startProfileAt([2.16, 49.67], %)
|> line([101.49, 139.93], %)
|> line([60.04, -55.72], %)
|> line([1.29, -115.74], %)
|> line([-87.24, -47.08], %, $seg03)
|> tangentialArcTo([56.15, -94.58], %)
|> tangentialArcTo([14.68, -104.52], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const extrude001 = extrude(50, sketch001)
|> fillet({ radius: 5, tags: [seg03] }, %)`
await runFilletTest(
code,
segmentSnippet,
extrudeSnippet,
radius,
expectedCode
)
})
/**
* 4. Case of existing fillet on some other segment
*/
it('should add another fillet after the existing fillet', async () => {
const code = `const sketch001 = startSketchOn('XZ')
|> startProfileAt([2.16, 49.67], %)
|> line([101.49, 139.93], %)
|> line([60.04, -55.72], %)
|> line([1.29, -115.74], %)
|> line([-87.24, -47.08], %, $seg03)
|> tangentialArcTo([56.15, -94.58], %)
|> tangentialArcTo([14.68, -104.52], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const extrude001 = extrude(50, sketch001)
|> fillet({ radius: 10, tags: [seg03] }, %)`
const segmentSnippet = `line([60.04, -55.72], %)`
const extrudeSnippet = `const extrude001 = extrude(50, sketch001)`
const radius = createLiteral(5) as Value
const expectedCode = `const sketch001 = startSketchOn('XZ')
|> startProfileAt([2.16, 49.67], %)
|> line([101.49, 139.93], %)
|> line([60.04, -55.72], %, $seg01)
|> line([1.29, -115.74], %)
|> line([-87.24, -47.08], %, $seg03)
|> tangentialArcTo([56.15, -94.58], %)
|> tangentialArcTo([14.68, -104.52], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const extrude001 = extrude(50, sketch001)
|> fillet({ radius: 10, tags: [seg03] }, %)
|> fillet({ radius: 5, tags: [seg01] }, %)`
await runFilletTest(
code,
segmentSnippet,
extrudeSnippet,
radius,
expectedCode
)
})
})
describe('Testing isTagUsedInFillet', () => {
const code = `const sketch001 = startSketchOn('XZ')
|> startProfileAt([7.72, 4.13], %)
|> line([7.11, 3.48], %, $seg01)
|> line([-3.29, -13.85], %)
|> line([-6.37, 3.88], %, $seg02)
|> close(%)
const extrude001 = extrude(-5, sketch001)
|> fillet({
radius: 1.11,
tags: [
getOppositeEdge(seg01, %),
seg01,
getPreviousAdjacentEdge(seg02, %)
]
}, %)
`
it('should correctly identify getOppositeEdge and baseEdge edges', () => {
const ast = parse(code)
if (err(ast)) return
const lineOfInterest = `line([7.11, 3.48], %, $seg01)`
const range: [number, number] = [
code.indexOf(lineOfInterest),
code.indexOf(lineOfInterest) + lineOfInterest.length,
]
const pathToNode = getNodePathFromSourceRange(ast, range)
if (err(pathToNode)) return
const callExp = getNodeFromPath<CallExpression>(
ast,
pathToNode,
'CallExpression'
)
if (err(callExp)) return
const edges = isTagUsedInFillet({ ast, callExp: callExp.node })
expect(edges).toEqual(['getOppositeEdge', 'baseEdge'])
})
it('should correctly identify getPreviousAdjacentEdge edges', () => {
const ast = parse(code)
if (err(ast)) return
const lineOfInterest = `line([-6.37, 3.88], %, $seg02)`
const range: [number, number] = [
code.indexOf(lineOfInterest),
code.indexOf(lineOfInterest) + lineOfInterest.length,
]
const pathToNode = getNodePathFromSourceRange(ast, range)
if (err(pathToNode)) return
const callExp = getNodeFromPath<CallExpression>(
ast,
pathToNode,
'CallExpression'
)
if (err(callExp)) return
const edges = isTagUsedInFillet({ ast, callExp: callExp.node })
expect(edges).toEqual(['getPreviousAdjacentEdge'])
})
it('should correctly identify no edges', () => {
const ast = parse(code)
if (err(ast)) return
const lineOfInterest = `line([-3.29, -13.85], %)`
const range: [number, number] = [
code.indexOf(lineOfInterest),
code.indexOf(lineOfInterest) + lineOfInterest.length,
]
const pathToNode = getNodePathFromSourceRange(ast, range)
if (err(pathToNode)) return
const callExp = getNodeFromPath<CallExpression>(
ast,
pathToNode,
'CallExpression'
)
if (err(callExp)) return
const edges = isTagUsedInFillet({ ast, callExp: callExp.node })
expect(edges).toEqual([])
})
})

View File

@ -0,0 +1,404 @@
import {
ArrayExpression,
CallExpression,
ObjectExpression,
PathToNode,
Program,
Value,
VariableDeclaration,
VariableDeclarator,
} from '../wasm'
import {
createCallExpressionStdLib,
createLiteral,
createPipeSubstitution,
createObjectExpression,
createArrayExpression,
createIdentifier,
createPipeExpression,
} from '../modifyAst'
import {
getNodeFromPath,
getNodePathFromSourceRange,
hasSketchPipeBeenExtruded,
traverse,
} from '../queryAst'
import {
addTagForSketchOnFace,
getTagFromCallExpression,
sketchLineHelperMap,
} from '../std/sketch'
import { err } from 'lib/trap'
import { Selections, canFilletSelection } from 'lib/selections'
export function addFillet(
node: Program,
pathToSegmentNode: PathToNode,
pathToExtrudeNode: PathToNode,
radius = createLiteral(5) as Value
// shouldPipe = false, // TODO: Implement this feature
): { modifiedAst: Program; pathToFilletNode: PathToNode } | Error {
// close ast to make mutations safe
let _node: Program = JSON.parse(JSON.stringify(node))
/**
* Add Tag to the Segment Expression
*/
// Find the specific sketch segment to tag with the new tag
const sketchSegmentChunk = getNodeFromPath(
_node,
pathToSegmentNode,
'CallExpression'
)
if (err(sketchSegmentChunk)) return sketchSegmentChunk
const { node: sketchSegmentNode } = sketchSegmentChunk as {
node: CallExpression
}
// Check whether selection is a valid segment from sketchLineHelpersMap
if (!(sketchSegmentNode.callee.name in sketchLineHelperMap)) {
return new Error('Selection is not a sketch segment')
}
// Add tag to the sketch segment or use existing tag
const taggedSegment = addTagForSketchOnFace(
{
// previousProgramMemory: programMemory,
pathToNode: pathToSegmentNode,
node: _node,
},
sketchSegmentNode.callee.name
)
if (err(taggedSegment)) return taggedSegment
const { tag } = taggedSegment
/**
* Find Extrude Expression automatically
*/
// 1. Get the sketch name
/**
* Add Fillet to the Extrude expression
*/
// Create the fillet call expression in one line
const filletCall = createCallExpressionStdLib('fillet', [
createObjectExpression({
radius: radius,
tags: createArrayExpression([createIdentifier(tag)]),
}),
createPipeSubstitution(),
])
// Locate the extrude call
const extrudeChunk = getNodeFromPath<VariableDeclaration>(
_node,
pathToExtrudeNode,
'VariableDeclaration'
)
if (err(extrudeChunk)) return extrudeChunk
const { node: extrudeVarDecl } = extrudeChunk
const extrudeDeclarator = extrudeVarDecl.declarations[0]
const extrudeInit = extrudeDeclarator.init
if (
!extrudeDeclarator ||
(extrudeInit.type !== 'CallExpression' &&
extrudeInit.type !== 'PipeExpression')
) {
return new Error('Extrude PipeExpression / CallExpression not found.')
}
// determine if extrude is in a PipeExpression or CallExpression
// CallExpression - no fillet
// PipeExpression - fillet exists
const getPathToNodeOfFilletLiteral = (
pathToExtrudeNode: PathToNode,
extrudeDeclarator: VariableDeclarator,
tag: string
): PathToNode => {
let pathToFilletObj: any
let inFillet = false
traverse(extrudeDeclarator.init, {
enter(node, path) {
if (node.type === 'CallExpression' && node.callee.name === 'fillet') {
inFillet = true
}
if (inFillet && node.type === 'ObjectExpression') {
const hasTag = node.properties.some((prop) => {
const isTagProp = prop.key.name === 'tags'
if (isTagProp && prop.value.type === 'ArrayExpression') {
return prop.value.elements.some(
(element) =>
element.type === 'Identifier' && element.name === tag
)
}
return false
})
if (!hasTag) return false
pathToFilletObj = path
node.properties.forEach((prop, index) => {
if (prop.key.name === 'radius') {
pathToFilletObj.push(
['properties', 'ObjectExpression'],
[index, 'index'],
['value', 'Property']
)
}
})
}
},
leave(node) {
if (node.type === 'CallExpression' && node.callee.name === 'fillet') {
inFillet = false
}
},
})
let indexOfPipeExpression = pathToExtrudeNode.findIndex(
(path) => path[1] === 'PipeExpression'
)
indexOfPipeExpression =
indexOfPipeExpression === -1
? pathToExtrudeNode.length
: indexOfPipeExpression
return [
...pathToExtrudeNode.slice(0, indexOfPipeExpression),
...pathToFilletObj,
]
}
if (extrudeInit.type === 'CallExpression') {
// 1. no fillet case
extrudeDeclarator.init = createPipeExpression([extrudeInit, filletCall])
return {
modifiedAst: _node,
pathToFilletNode: getPathToNodeOfFilletLiteral(
pathToExtrudeNode,
extrudeDeclarator,
tag
),
}
} else if (extrudeInit.type === 'PipeExpression') {
// 2. fillet case
// there are 2 options here:
const existingFilletCall = extrudeInit.body.find((node) => {
return node.type === 'CallExpression' && node.callee.name === 'fillet'
})
if (!existingFilletCall || existingFilletCall.type !== 'CallExpression') {
return new Error('Fillet CallExpression not found.')
}
// check if the existing fillet has the same tag as the new fillet
let filletTag = null
if (existingFilletCall.arguments[0].type === 'ObjectExpression') {
const properties = (existingFilletCall.arguments[0] as ObjectExpression)
.properties
const tagsProperty = properties.find((prop) => prop.key.name === 'tags')
if (tagsProperty && tagsProperty.value.type === 'ArrayExpression') {
const elements = (tagsProperty.value as ArrayExpression).elements
if (elements.length > 0 && elements[0].type === 'Identifier') {
filletTag = elements[0].name
}
}
} else {
return new Error('Expected an ObjectExpression node')
}
if (filletTag !== tag) {
extrudeInit.body.push(filletCall)
return {
modifiedAst: _node,
pathToFilletNode: getPathToNodeOfFilletLiteral(
pathToExtrudeNode,
extrudeDeclarator,
tag
),
}
}
} else {
return new Error('Unsupported extrude type.')
}
return new Error('Unsupported extrude type.')
}
export const hasValidFilletSelection = ({
selectionRanges,
ast,
code,
}: {
selectionRanges: Selections
ast: Program
code: string
}) => {
// case 0: check if there is anything filletable in the scene
let extrudeExists = false
traverse(ast, {
enter(node) {
if (node.type === 'CallExpression' && node.callee.name === 'extrude') {
extrudeExists = true
}
},
})
if (!extrudeExists) return false
// case 1: nothing selected, test whether the extrusion exists
if (selectionRanges) {
if (selectionRanges.codeBasedSelections.length === 0) {
return true
}
const range0 = selectionRanges.codeBasedSelections[0].range[0]
const codeLength = code.length
if (range0 === codeLength) {
return true
}
}
// case 2: sketch segment selected, test whether it is extruded
// TODO: add loft / sweep check
if (selectionRanges.codeBasedSelections.length > 0) {
const isExtruded = hasSketchPipeBeenExtruded(
selectionRanges.codeBasedSelections[0],
ast
)
if (isExtruded) {
const pathToSelectedNode = getNodePathFromSourceRange(
ast,
selectionRanges.codeBasedSelections[0].range
)
const segmentNode = getNodeFromPath<CallExpression>(
ast,
pathToSelectedNode,
'CallExpression'
)
if (err(segmentNode)) return false
if (segmentNode.node.type === 'CallExpression') {
const segmentName = segmentNode.node.callee.name
if (segmentName in sketchLineHelperMap) {
const edges = isTagUsedInFillet({
ast,
callExp: segmentNode.node,
})
// edge has already been filleted
if (
['edge', 'default'].includes(
selectionRanges.codeBasedSelections[0].type
) &&
edges.includes('baseEdge')
)
return false
return true
} else {
return false
}
}
} else {
return false
}
}
return canFilletSelection(selectionRanges)
}
type EdgeTypes =
| 'baseEdge'
| 'getNextAdjacentEdge'
| 'getPreviousAdjacentEdge'
| 'getOppositeEdge'
export const isTagUsedInFillet = ({
ast,
callExp,
}: {
ast: Program
callExp: CallExpression
}): Array<EdgeTypes> => {
const tag = getTagFromCallExpression(callExp)
if (err(tag)) return []
let inFillet = false
let inObj = false
let inTagHelper: EdgeTypes | '' = ''
const edges: Array<EdgeTypes> = []
traverse(ast, {
enter: (node) => {
if (node.type === 'CallExpression' && node.callee.name === 'fillet') {
inFillet = true
}
if (inFillet && node.type === 'ObjectExpression') {
node.properties.forEach((prop) => {
if (
prop.key.name === 'tags' &&
prop.value.type === 'ArrayExpression'
) {
inObj = true
}
})
}
if (
inObj &&
inFillet &&
node.type === 'CallExpression' &&
(node.callee.name === 'getOppositeEdge' ||
node.callee.name === 'getNextAdjacentEdge' ||
node.callee.name === 'getPreviousAdjacentEdge')
) {
inTagHelper = node.callee.name
}
if (
inObj &&
inFillet &&
!inTagHelper &&
node.type === 'Identifier' &&
node.name === tag
) {
edges.push('baseEdge')
}
if (
inObj &&
inFillet &&
inTagHelper &&
node.type === 'Identifier' &&
node.name === tag
) {
edges.push(inTagHelper)
}
},
leave: (node) => {
if (node.type === 'CallExpression' && node.callee.name === 'fillet') {
inFillet = false
}
if (inFillet && node.type === 'ObjectExpression') {
node.properties.forEach((prop) => {
if (
prop.key.name === 'tags' &&
prop.value.type === 'ArrayExpression'
) {
inObj = true
}
})
}
if (
inObj &&
inFillet &&
node.type === 'CallExpression' &&
(node.callee.name === 'getOppositeEdge' ||
node.callee.name === 'getNextAdjacentEdge' ||
node.callee.name === 'getPreviousAdjacentEdge')
) {
inTagHelper = ''
}
},
})
return edges
}

View File

@ -143,6 +143,7 @@ export enum DisconnectingType {
Error = 'error', Error = 'error',
Timeout = 'timeout', Timeout = 'timeout',
Quit = 'quit', Quit = 'quit',
Pause = 'pause',
} }
// Sorted by severity // Sorted by severity
@ -200,6 +201,7 @@ export type DisconnectingValue =
| State<DisconnectingType.Error, ErrorType> | State<DisconnectingType.Error, ErrorType>
| State<DisconnectingType.Timeout, void> | State<DisconnectingType.Timeout, void>
| State<DisconnectingType.Quit, void> | State<DisconnectingType.Quit, void>
| State<DisconnectingType.Pause, void>
// These are ordered by the expected sequence. // These are ordered by the expected sequence.
export enum ConnectingType { export enum ConnectingType {
@ -300,7 +302,31 @@ class EngineConnection extends EventTarget {
pc?: RTCPeerConnection pc?: RTCPeerConnection
unreliableDataChannel?: RTCDataChannel unreliableDataChannel?: RTCDataChannel
mediaStream?: MediaStream mediaStream?: MediaStream
freezeFrame: boolean = false idleMode: boolean = false
onIceCandidate = function (
this: RTCPeerConnection,
event: RTCPeerConnectionIceEvent
) {}
onIceCandidateError = function (
this: RTCPeerConnection,
event: RTCPeerConnectionIceErrorEvent
) {}
onConnectionStateChange = function (this: RTCPeerConnection, event: Event) {}
onDataChannelOpen = function (this: RTCDataChannel, event: Event) {}
onDataChannelClose = function (this: RTCDataChannel, event: Event) {}
onDataChannelError = function (this: RTCDataChannel, event: Event) {}
onDataChannelMessage = function (this: RTCDataChannel, event: MessageEvent) {}
onDataChannel = function (
this: RTCPeerConnection,
event: RTCDataChannelEvent
) {}
onTrack = function (this: RTCPeerConnection, event: RTCTrackEvent) {}
onWebSocketOpen = function (event: Event) {}
onWebSocketClose = function (event: Event) {}
onWebSocketError = function (event: Event) {}
onWebSocketMessage = function (event: MessageEvent) {}
onNetworkStatusReady = () => {}
private _state: EngineConnectionState = { private _state: EngineConnectionState = {
type: EngineConnectionStateType.Fresh, type: EngineConnectionStateType.Fresh,
@ -346,6 +372,7 @@ class EngineConnection extends EventTarget {
private engineCommandManager: EngineCommandManager private engineCommandManager: EngineCommandManager
private pingPongSpan: { ping?: Date; pong?: Date } private pingPongSpan: { ping?: Date; pong?: Date }
private pingIntervalId: ReturnType<typeof setInterval>
constructor({ constructor({
engineCommandManager, engineCommandManager,
@ -366,10 +393,10 @@ class EngineConnection extends EventTarget {
this.pingPongSpan = { ping: undefined, pong: undefined } this.pingPongSpan = { ping: undefined, pong: undefined }
// Without an interval ping, our connection will timeout. // Without an interval ping, our connection will timeout.
// If this.freezeFrame is true we skip this logic so only reconnect // If this.idleMode is true we skip this logic so only reconnect
// happens on mouse move // happens on mouse move
setInterval(() => { this.pingIntervalId = setInterval(() => {
if (this.freezeFrame) return if (this.idleMode) return
switch (this.state.type as EngineConnectionStateType) { switch (this.state.type as EngineConnectionStateType) {
case EngineConnectionStateType.ConnectionEstablished: case EngineConnectionStateType.ConnectionEstablished:
@ -431,13 +458,60 @@ class EngineConnection extends EventTarget {
return this.state.type === EngineConnectionStateType.ConnectionEstablished return this.state.type === EngineConnectionStateType.ConnectionEstablished
} }
tearDown(opts?: { freeze: boolean }) { tearDown(opts?: { idleMode: boolean }) {
this.freezeFrame = opts?.freeze ?? false this.idleMode = opts?.idleMode ?? false
this.disconnectAll() this.disconnectAll()
this.state = { clearInterval(this.pingIntervalId)
type: EngineConnectionStateType.Disconnecting,
value: { type: DisconnectingType.Quit }, this.pc?.removeEventListener('icecandidate', this.onIceCandidate)
} this.pc?.removeEventListener('icecandidateerror', this.onIceCandidateError)
this.pc?.removeEventListener(
'connectionstatechange',
this.onConnectionStateChange
)
this.pc?.removeEventListener('track', this.onTrack)
this.unreliableDataChannel?.removeEventListener(
'open',
this.onDataChannelOpen
)
this.unreliableDataChannel?.removeEventListener(
'close',
this.onDataChannelClose
)
this.unreliableDataChannel?.removeEventListener(
'error',
this.onDataChannelError
)
this.unreliableDataChannel?.removeEventListener(
'message',
this.onDataChannelMessage
)
this.pc?.removeEventListener('datachannel', this.onDataChannel)
this.websocket?.removeEventListener('open', this.onWebSocketOpen)
this.websocket?.removeEventListener('close', this.onWebSocketClose)
this.websocket?.removeEventListener('error', this.onWebSocketError)
this.websocket?.removeEventListener('message', this.onWebSocketMessage)
window.removeEventListener(
'use-network-status-ready',
this.onNetworkStatusReady
)
this.state = opts?.idleMode
? {
type: EngineConnectionStateType.Disconnecting,
value: {
type: DisconnectingType.Pause,
},
}
: {
type: EngineConnectionStateType.Disconnecting,
value: {
type: DisconnectingType.Quit,
},
}
} }
/** /**
@ -477,7 +551,7 @@ class EngineConnection extends EventTarget {
}, },
} }
this.pc.addEventListener('icecandidate', (event) => { this.onIceCandidate = (event: RTCPeerConnectionIceEvent) => {
if (event.candidate === null) { if (event.candidate === null) {
return return
} }
@ -499,18 +573,20 @@ class EngineConnection extends EventTarget {
usernameFragment: event.candidate.usernameFragment || undefined, usernameFragment: event.candidate.usernameFragment || undefined,
}, },
}) })
}) }
this.pc.addEventListener('icecandidate', this.onIceCandidate)
this.pc.addEventListener('icecandidateerror', (_event: Event) => { this.onIceCandidateError = (_event: Event) => {
const event = _event as RTCPeerConnectionIceErrorEvent const event = _event as RTCPeerConnectionIceErrorEvent
console.warn( console.warn(
`ICE candidate returned an error: ${event.errorCode}: ${event.errorText} for ${event.url}` `ICE candidate returned an error: ${event.errorCode}: ${event.errorText} for ${event.url}`
) )
}) }
this.pc.addEventListener('icecandidateerror', this.onIceCandidateError)
// https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/connectionstatechange_event // https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/connectionstatechange_event
// Event type: generic Event type... // Event type: generic Event type...
this.pc.addEventListener('connectionstatechange', (event: any) => { this.onConnectionStateChange = (event: any) => {
console.log('connectionstatechange: ' + event.target?.connectionState) console.log('connectionstatechange: ' + event.target?.connectionState)
switch (event.target?.connectionState) { switch (event.target?.connectionState) {
// From what I understand, only after have we done the ICE song and // From what I understand, only after have we done the ICE song and
@ -539,9 +615,13 @@ class EngineConnection extends EventTarget {
default: default:
break break
} }
}) }
this.pc.addEventListener(
'connectionstatechange',
this.onConnectionStateChange
)
this.pc.addEventListener('track', (event) => { this.onTrack = (event) => {
const mediaStream = event.streams[0] const mediaStream = event.streams[0]
this.state = { this.state = {
@ -625,9 +705,10 @@ class EngineConnection extends EventTarget {
// to pass it to the rest of the application. // to pass it to the rest of the application.
this.mediaStream = mediaStream this.mediaStream = mediaStream
}) }
this.pc.addEventListener('track', this.onTrack)
this.pc.addEventListener('datachannel', (event) => { this.onDataChannel = (event) => {
this.unreliableDataChannel = event.channel this.unreliableDataChannel = event.channel
this.state = { this.state = {
@ -638,7 +719,7 @@ class EngineConnection extends EventTarget {
}, },
} }
this.unreliableDataChannel.addEventListener('open', (event) => { this.onDataChannelOpen = (event) => {
this.state = { this.state = {
type: EngineConnectionStateType.Connecting, type: EngineConnectionStateType.Connecting,
value: { value: {
@ -654,14 +735,22 @@ class EngineConnection extends EventTarget {
this.dispatchEvent( this.dispatchEvent(
new CustomEvent(EngineConnectionEvents.Opened, { detail: this }) new CustomEvent(EngineConnectionEvents.Opened, { detail: this })
) )
}) }
this.unreliableDataChannel?.addEventListener(
'open',
this.onDataChannelOpen
)
this.unreliableDataChannel.addEventListener('close', (event) => { this.onDataChannelClose = (event) => {
this.disconnectAll() this.disconnectAll()
this.finalizeIfAllConnectionsClosed() this.finalizeIfAllConnectionsClosed()
}) }
this.unreliableDataChannel?.addEventListener(
'close',
this.onDataChannelClose
)
this.unreliableDataChannel.addEventListener('error', (event) => { this.onDataChannelError = (event) => {
this.disconnectAll() this.disconnectAll()
this.state = { this.state = {
@ -674,8 +763,13 @@ class EngineConnection extends EventTarget {
}, },
}, },
} }
}) }
this.unreliableDataChannel.addEventListener('message', (event) => { this.unreliableDataChannel?.addEventListener(
'error',
this.onDataChannelError
)
this.onDataChannelMessage = (event) => {
const result: UnreliableResponses = JSON.parse(event.data) const result: UnreliableResponses = JSON.parse(event.data)
Object.values( Object.values(
this.engineCommandManager.unreliableSubscriptions[result.type] || {} this.engineCommandManager.unreliableSubscriptions[result.type] || {}
@ -697,8 +791,13 @@ class EngineConnection extends EventTarget {
} }
} }
) )
}) }
}) this.unreliableDataChannel.addEventListener(
'message',
this.onDataChannelMessage
)
}
this.pc.addEventListener('datachannel', this.onDataChannel)
} }
const createWebSocketConnection = () => { const createWebSocketConnection = () => {
@ -712,7 +811,7 @@ class EngineConnection extends EventTarget {
this.websocket = new WebSocket(this.url, []) this.websocket = new WebSocket(this.url, [])
this.websocket.binaryType = 'arraybuffer' this.websocket.binaryType = 'arraybuffer'
this.websocket.addEventListener('open', (event) => { this.onWebSocketOpen = (event) => {
this.state = { this.state = {
type: EngineConnectionStateType.Connecting, type: EngineConnectionStateType.Connecting,
value: { value: {
@ -733,14 +832,16 @@ class EngineConnection extends EventTarget {
// Send an initial ping // Send an initial ping
this.send({ type: 'ping' }) this.send({ type: 'ping' })
this.pingPongSpan.ping = new Date() this.pingPongSpan.ping = new Date()
}) }
this.websocket.addEventListener('open', this.onWebSocketOpen)
this.websocket.addEventListener('close', (event) => { this.onWebSocketClose = (event) => {
this.disconnectAll() this.disconnectAll()
this.finalizeIfAllConnectionsClosed() this.finalizeIfAllConnectionsClosed()
}) }
this.websocket.addEventListener('close', this.onWebSocketClose)
this.websocket.addEventListener('error', (event) => { this.onWebSocketError = (event) => {
this.disconnectAll() this.disconnectAll()
this.state = { this.state = {
@ -753,9 +854,10 @@ class EngineConnection extends EventTarget {
}, },
}, },
} }
}) }
this.websocket.addEventListener('error', this.onWebSocketError)
this.websocket.addEventListener('message', (event) => { this.onWebSocketMessage = (event) => {
// In the EngineConnection, we're looking for messages to/from // In the EngineConnection, we're looking for messages to/from
// the server that relate to the ICE handshake, or WebRTC // the server that relate to the ICE handshake, or WebRTC
// negotiation. There may be other messages (including ArrayBuffer // negotiation. There may be other messages (including ArrayBuffer
@ -960,15 +1062,20 @@ class EngineConnection extends EventTarget {
}) })
break break
} }
}) }
this.websocket.addEventListener('message', this.onWebSocketMessage)
} }
if (reconnecting) { if (reconnecting) {
createWebSocketConnection() createWebSocketConnection()
} else { } else {
window.addEventListener('use-network-status-ready', () => { this.onNetworkStatusReady = () => {
createWebSocketConnection() createWebSocketConnection()
}) }
window.addEventListener(
'use-network-status-ready',
this.onNetworkStatusReady
)
} }
} }
// Do not change this back to an object or any, we should only be sending the // Do not change this back to an object or any, we should only be sending the
@ -1003,8 +1110,6 @@ class EngineConnection extends EventTarget {
this.unreliableDataChannel?.readyState === 'closed' this.unreliableDataChannel?.readyState === 'closed'
if (allClosed) { if (allClosed) {
// Do not notify the rest of the program that we have cut off anything. // Do not notify the rest of the program that we have cut off anything.
if (this.freezeFrame) return
this.state = { type: EngineConnectionStateType.Disconnected } this.state = { type: EngineConnectionStateType.Disconnected }
} }
} }
@ -1154,7 +1259,15 @@ export class EngineCommandManager extends EventTarget {
private makeDefaultPlanes: () => Promise<DefaultPlanes> | null = () => null private makeDefaultPlanes: () => Promise<DefaultPlanes> | null = () => null
private modifyGrid: (hidden: boolean) => Promise<void> | null = () => null private modifyGrid: (hidden: boolean) => Promise<void> | null = () => null
private onEngineConnectionOpened = () => {}
private onEngineConnectionClosed = () => {}
private onEngineConnectionStarted = ({ detail: engineConnection }: any) => {}
private onEngineConnectionNewTrack = ({
detail,
}: CustomEvent<NewTrackArgs>) => {}
start({ start({
restart,
setMediaStream, setMediaStream,
setIsStreamReady, setIsStreamReady,
width, width,
@ -1170,6 +1283,7 @@ export class EngineCommandManager extends EventTarget {
showScaleGrid: false, showScaleGrid: false,
}, },
}: { }: {
restart?: boolean
setMediaStream: (stream: MediaStream) => void setMediaStream: (stream: MediaStream) => void
setIsStreamReady: (isStreamReady: boolean) => void setIsStreamReady: (isStreamReady: boolean) => void
width: number width: number
@ -1215,162 +1329,168 @@ export class EngineCommandManager extends EventTarget {
}) })
) )
this.onEngineConnectionOpened = () => {
// Set the stream background color
// This takes RGBA values from 0-1
// So we convert from the conventional 0-255 found in Figma
void this.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'set_background_color',
color: getThemeColorForEngine(settings.theme),
},
})
// Sets the default line colors
const opposingTheme = getOppositeTheme(settings.theme)
this.sendSceneCommand({
cmd_id: uuidv4(),
type: 'modeling_cmd_req',
cmd: {
type: 'set_default_system_properties',
color: getThemeColorForEngine(opposingTheme),
},
})
// Set the edge lines visibility
this.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'edge_lines_visible' as any, // TODO: update kittycad.ts to use the correct type
hidden: !settings.highlightEdges,
},
})
this._camControlsCameraChange()
this.sendSceneCommand({
// CameraControls subscribes to default_camera_get_settings response events
// firing this at connection ensure the camera's are synced initially
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
})
// We want modify the grid first because we don't want it to flash.
// Ideally these would already be default hidden in engine (TODO do
// that) https://github.com/KittyCAD/engine/issues/2282
this.modifyGrid(!settings.showScaleGrid)?.then(async () => {
await this.initPlanes()
this.resolveReady()
setIsStreamReady(true)
await executeCode()
})
}
this.engineConnection.addEventListener( this.engineConnection.addEventListener(
EngineConnectionEvents.Opened, EngineConnectionEvents.Opened,
() => { this.onEngineConnectionOpened
// Set the stream background color
// This takes RGBA values from 0-1
// So we convert from the conventional 0-255 found in Figma
void this.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'set_background_color',
color: getThemeColorForEngine(settings.theme),
},
})
// Sets the default line colors
const opposingTheme = getOppositeTheme(settings.theme)
this.sendSceneCommand({
cmd_id: uuidv4(),
type: 'modeling_cmd_req',
cmd: {
type: 'set_default_system_properties',
color: getThemeColorForEngine(opposingTheme),
},
})
// Set the edge lines visibility
this.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'edge_lines_visible' as any, // TODO: update kittycad.ts to use the correct type
hidden: !settings.highlightEdges,
},
})
this._camControlsCameraChange()
this.sendSceneCommand({
// CameraControls subscribes to default_camera_get_settings response events
// firing this at connection ensure the camera's are synced initially
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
})
// We want modify the grid first because we don't want it to flash.
// Ideally these would already be default hidden in engine (TODO do
// that) https://github.com/KittyCAD/engine/issues/2282
this.modifyGrid(!settings.showScaleGrid)?.then(async () => {
await this.initPlanes()
this.resolveReady()
setIsStreamReady(true)
await executeCode()
})
}
) )
this.onEngineConnectionClosed = () => {
setIsStreamReady(false)
}
this.engineConnection.addEventListener( this.engineConnection.addEventListener(
EngineConnectionEvents.Closed, EngineConnectionEvents.Closed,
() => { this.onEngineConnectionClosed
setIsStreamReady(false)
}
) )
this.engineConnection.addEventListener( this.onEngineConnectionStarted = ({ detail: engineConnection }: any) => {
EngineConnectionEvents.ConnectionStarted, engineConnection?.pc?.addEventListener(
({ detail: engineConnection }: any) => { 'datachannel',
engineConnection?.pc?.addEventListener( (event: RTCDataChannelEvent) => {
'datachannel', let unreliableDataChannel = event.channel
(event: RTCDataChannelEvent) => {
let unreliableDataChannel = event.channel
unreliableDataChannel.addEventListener( unreliableDataChannel.addEventListener(
'message', 'message',
(event: MessageEvent) => { (event: MessageEvent) => {
const result: UnreliableResponses = JSON.parse(event.data) const result: UnreliableResponses = JSON.parse(event.data)
Object.values( Object.values(
this.unreliableSubscriptions[result.type] || {} this.unreliableSubscriptions[result.type] || {}
).forEach( ).forEach(
// TODO: There is only one response that uses the unreliable channel atm, // TODO: There is only one response that uses the unreliable channel atm,
// highlight_set_entity, if there are more it's likely they will all have the same // highlight_set_entity, if there are more it's likely they will all have the same
// sequence logic, but I'm not sure if we use a single global sequence or a sequence // sequence logic, but I'm not sure if we use a single global sequence or a sequence
// per unreliable subscription. // per unreliable subscription.
(callback) => { (callback) => {
let data = result?.data let data = result?.data
if (isHighlightSetEntity_type(data)) { if (isHighlightSetEntity_type(data)) {
if ( if (
data.sequence !== undefined && data.sequence !== undefined &&
data.sequence > this.inSequence data.sequence > this.inSequence
) { ) {
this.inSequence = data.sequence this.inSequence = data.sequence
callback(result) callback(result)
}
} }
} }
) }
}
)
}
)
// When the EngineConnection starts a connection, we want to register
// callbacks into the WebSocket/PeerConnection.
engineConnection.websocket?.addEventListener('message', ((
event: MessageEvent
) => {
if (event.data instanceof ArrayBuffer) {
// If the data is an ArrayBuffer, it's the result of an export command,
// because in all other cases we send JSON strings. But in the case of
// export we send a binary blob.
// Pass this to our export function.
exportSave(event.data).then(() => {
this.pendingExport?.resolve()
}, this.pendingExport?.reject)
} else {
const message: Models['WebSocketResponse_type'] = JSON.parse(
event.data
)
if (
message.success &&
(message.resp.type === 'modeling' ||
message.resp.type === 'modeling_batch') &&
message.request_id
) {
this.handleModelingCommand(
message.resp,
message.request_id,
message
) )
} else if (
!message.success &&
message.request_id &&
this.artifactMap[message.request_id]
) {
this.handleFailedModelingCommand(message.request_id, message)
} }
)
}
)
// When the EngineConnection starts a connection, we want to register
// callbacks into the WebSocket/PeerConnection.
engineConnection.websocket?.addEventListener('message', ((
event: MessageEvent
) => {
if (event.data instanceof ArrayBuffer) {
// If the data is an ArrayBuffer, it's the result of an export command,
// because in all other cases we send JSON strings. But in the case of
// export we send a binary blob.
// Pass this to our export function.
exportSave(event.data).then(() => {
this.pendingExport?.resolve()
}, this.pendingExport?.reject)
} else {
const message: Models['WebSocketResponse_type'] = JSON.parse(
event.data
)
if (
message.success &&
(message.resp.type === 'modeling' ||
message.resp.type === 'modeling_batch') &&
message.request_id
) {
this.handleModelingCommand(
message.resp,
message.request_id,
message
)
} else if (
!message.success &&
message.request_id &&
this.artifactMap[message.request_id]
) {
this.handleFailedModelingCommand(message.request_id, message)
} }
}) as EventListener) }
}) as EventListener)
this.engineConnection?.addEventListener( this.onEngineConnectionNewTrack = ({
EngineConnectionEvents.NewTrack, detail: { mediaStream },
(({ detail: { mediaStream } }: CustomEvent<NewTrackArgs>) => { }: CustomEvent<NewTrackArgs>) => {
mediaStream.getVideoTracks()[0].addEventListener('mute', () => { mediaStream.getVideoTracks()[0].addEventListener('mute', () => {
console.error( console.error(
'video track mute: check webrtc internals -> inbound rtp' 'video track mute: check webrtc internals -> inbound rtp'
) )
}) })
setMediaStream(mediaStream) setMediaStream(mediaStream)
}) as EventListener
)
this.engineConnection?.connect()
} }
this.engineConnection?.addEventListener(
EngineConnectionEvents.NewTrack,
this.onEngineConnectionNewTrack as EventListener
)
this.engineConnection?.connect()
}
this.engineConnection.addEventListener(
EngineConnectionEvents.ConnectionStarted,
this.onEngineConnectionStarted
) )
} }
@ -1627,15 +1747,34 @@ export class EngineCommandManager extends EventTarget {
} }
} }
} }
tearDown() { tearDown(opts?: { idleMode: boolean }) {
if (this.engineConnection) { if (this.engineConnection) {
this.engineConnection?.tearDown() this.engineConnection.removeEventListener(
EngineConnectionEvents.Opened,
this.onEngineConnectionOpened
)
this.engineConnection.removeEventListener(
EngineConnectionEvents.Closed,
this.onEngineConnectionClosed
)
this.engineConnection.removeEventListener(
EngineConnectionEvents.ConnectionStarted,
this.onEngineConnectionStarted
)
this.engineConnection.removeEventListener(
EngineConnectionEvents.NewTrack,
this.onEngineConnectionNewTrack as EventListener
)
this.engineConnection?.tearDown(opts)
this.engineConnection = undefined
// Our window.tearDown assignment causes this case to happen which is // Our window.tearDown assignment causes this case to happen which is
// only really for tests. // only really for tests.
// @ts-ignore // @ts-ignore
} else if (this.engineCommandManager?.engineConnection) { } else if (this.engineCommandManager?.engineConnection) {
// @ts-ignore // @ts-ignore
this.engineCommandManager?.engineConnection?.tearDown() this.engineCommandManager?.engineConnection?.tearDown(opts)
} }
} }
async startNewSession() { async startNewSession() {

View File

@ -221,7 +221,7 @@ describe('testing addTagForSketchOnFace', () => {
const pathToNode = getNodePathFromSourceRange(ast, sourceRange) const pathToNode = getNodePathFromSourceRange(ast, sourceRange)
const sketchOnFaceRetVal = addTagForSketchOnFace( const sketchOnFaceRetVal = addTagForSketchOnFace(
{ {
previousProgramMemory: programMemory, // previousProgramMemory: programMemory, // redundant?
pathToNode, pathToNode,
node: ast, node: ast,
}, },

View File

@ -28,7 +28,6 @@ import { createPipeExpression, splitPathAtPipeExpression } from '../modifyAst'
import { import {
SketchLineHelper, SketchLineHelper,
ModifyAstBase,
TransformCallback, TransformCallback,
ConstrainInfo, ConstrainInfo,
RawValues, RawValues,
@ -37,6 +36,7 @@ import {
SingleValueInput, SingleValueInput,
VarValueKeys, VarValueKeys,
ArrayOrObjItemInput, ArrayOrObjItemInput,
AddTagInfo,
} from 'lang/std/stdTypes' } from 'lang/std/stdTypes'
import { import {
@ -308,6 +308,18 @@ function singleRawValueHelper(
] ]
} }
function getTag(index = 2): SketchLineHelper['getTag'] {
return (callExp: CallExpression) => {
if (callExp.type !== 'CallExpression')
return new Error('Not a CallExpression')
const arg = callExp.arguments?.[index]
if (!arg) return new Error('No argument')
if (arg.type !== 'TagDeclarator')
return new Error('Tag not a TagDeclarator')
return arg.value
}
}
export const lineTo: SketchLineHelper = { export const lineTo: SketchLineHelper = {
add: ({ add: ({
node, node,
@ -377,6 +389,7 @@ export const lineTo: SketchLineHelper = {
pathToNode, pathToNode,
} }
}, },
getTag: getTag(),
addTag: addTag(), addTag: addTag(),
getConstraintInfo: (callExp, ...args) => getConstraintInfo: (callExp, ...args) =>
commonConstraintInfoHelper( commonConstraintInfoHelper(
@ -503,6 +516,7 @@ export const line: SketchLineHelper = {
pathToNode, pathToNode,
} }
}, },
getTag: getTag(),
addTag: addTag(), addTag: addTag(),
getConstraintInfo: (callExp, ...args) => getConstraintInfo: (callExp, ...args) =>
commonConstraintInfoHelper( commonConstraintInfoHelper(
@ -563,6 +577,7 @@ export const xLineTo: SketchLineHelper = {
pathToNode, pathToNode,
} }
}, },
getTag: getTag(),
addTag: addTag(), addTag: addTag(),
getConstraintInfo: (callExp, ...args) => getConstraintInfo: (callExp, ...args) =>
horzVertConstraintInfoHelper( horzVertConstraintInfoHelper(
@ -623,6 +638,7 @@ export const yLineTo: SketchLineHelper = {
pathToNode, pathToNode,
} }
}, },
getTag: getTag(),
addTag: addTag(), addTag: addTag(),
getConstraintInfo: (callExp, ...args) => getConstraintInfo: (callExp, ...args) =>
horzVertConstraintInfoHelper( horzVertConstraintInfoHelper(
@ -682,6 +698,7 @@ export const xLine: SketchLineHelper = {
pathToNode, pathToNode,
} }
}, },
getTag: getTag(),
addTag: addTag(), addTag: addTag(),
getConstraintInfo: (callExp, ...args) => getConstraintInfo: (callExp, ...args) =>
horzVertConstraintInfoHelper( horzVertConstraintInfoHelper(
@ -738,6 +755,7 @@ export const yLine: SketchLineHelper = {
pathToNode, pathToNode,
} }
}, },
getTag: getTag(),
addTag: addTag(), addTag: addTag(),
getConstraintInfo: (callExp, ...args) => getConstraintInfo: (callExp, ...args) =>
horzVertConstraintInfoHelper( horzVertConstraintInfoHelper(
@ -830,6 +848,7 @@ export const tangentialArcTo: SketchLineHelper = {
pathToNode, pathToNode,
} }
}, },
getTag: getTag(),
addTag: addTag(), addTag: addTag(),
getConstraintInfo: (callExp: CallExpression, code, pathToNode) => { getConstraintInfo: (callExp: CallExpression, code, pathToNode) => {
if (callExp.type !== 'CallExpression') return [] if (callExp.type !== 'CallExpression') return []
@ -948,6 +967,7 @@ export const angledLine: SketchLineHelper = {
pathToNode, pathToNode,
} }
}, },
getTag: getTag(),
addTag: addTag(), addTag: addTag(),
getConstraintInfo: (callExp, ...args) => getConstraintInfo: (callExp, ...args) =>
commonConstraintInfoHelper( commonConstraintInfoHelper(
@ -1044,6 +1064,7 @@ export const angledLineOfXLength: SketchLineHelper = {
pathToNode, pathToNode,
} }
}, },
getTag: getTag(),
addTag: addTag(), addTag: addTag(),
getConstraintInfo: (callExp, ...args) => getConstraintInfo: (callExp, ...args) =>
commonConstraintInfoHelper( commonConstraintInfoHelper(
@ -1140,6 +1161,7 @@ export const angledLineOfYLength: SketchLineHelper = {
pathToNode, pathToNode,
} }
}, },
getTag: getTag(),
addTag: addTag(), addTag: addTag(),
getConstraintInfo: (callExp, ...args) => getConstraintInfo: (callExp, ...args) =>
commonConstraintInfoHelper( commonConstraintInfoHelper(
@ -1227,6 +1249,7 @@ export const angledLineToX: SketchLineHelper = {
pathToNode, pathToNode,
} }
}, },
getTag: getTag(),
addTag: addTag(), addTag: addTag(),
getConstraintInfo: (callExp, ...args) => getConstraintInfo: (callExp, ...args) =>
commonConstraintInfoHelper( commonConstraintInfoHelper(
@ -1316,6 +1339,7 @@ export const angledLineToY: SketchLineHelper = {
pathToNode, pathToNode,
} }
}, },
getTag: getTag(),
addTag: addTag(), addTag: addTag(),
getConstraintInfo: (callExp, ...args) => getConstraintInfo: (callExp, ...args) =>
commonConstraintInfoHelper( commonConstraintInfoHelper(
@ -1440,6 +1464,7 @@ export const angledLineThatIntersects: SketchLineHelper = {
pathToNode, pathToNode,
} }
}, },
getTag: getTag(),
addTag: addTag(), addTag: addTag(),
getConstraintInfo: (callExp: CallExpression, code, pathToNode) => { getConstraintInfo: (callExp: CallExpression, code, pathToNode) => {
if (callExp.type !== 'CallExpression') return [] if (callExp.type !== 'CallExpression') return []
@ -1792,10 +1817,7 @@ export function replaceSketchLine({
return { modifiedAst, valueUsedInTransform, pathToNode } return { modifiedAst, valueUsedInTransform, pathToNode }
} }
export function addTagForSketchOnFace( export function addTagForSketchOnFace(a: AddTagInfo, expressionName: string) {
a: ModifyAstBase,
expressionName: string
) {
if (expressionName === 'close') { if (expressionName === 'close') {
return addTag(1)(a) return addTag(1)(a)
} }
@ -1806,6 +1828,17 @@ export function addTagForSketchOnFace(
return new Error(`"${expressionName}" is not a sketch line helper`) return new Error(`"${expressionName}" is not a sketch line helper`)
} }
export function getTagFromCallExpression(
callExp: CallExpression
): string | Error {
if (callExp.callee.name === 'close') return getTag(1)(callExp)
if (callExp.callee.name in sketchLineHelperMap) {
const { getTag } = sketchLineHelperMap[callExp.callee.name]
return getTag(callExp)
}
return new Error(`"${callExp.callee.name}" is not a sketch line helper`)
}
function isAngleLiteral(lineArugement: Value): boolean { function isAngleLiteral(lineArugement: Value): boolean {
return lineArugement?.type === 'ArrayExpression' return lineArugement?.type === 'ArrayExpression'
? isLiteralArrayOrStatic(lineArugement.elements[0]) ? isLiteralArrayOrStatic(lineArugement.elements[0])
@ -1816,9 +1849,7 @@ function isAngleLiteral(lineArugement: Value): boolean {
: false : false
} }
type addTagFn = ( type addTagFn = (a: AddTagInfo) => { modifiedAst: Program; tag: string } | Error
a: ModifyAstBase
) => { modifiedAst: Program; tag: string } | Error
function addTag(tagIndex = 2): addTagFn { function addTag(tagIndex = 2): addTagFn {
return ({ node, pathToNode }) => { return ({ node, pathToNode }) => {

View File

@ -32,6 +32,11 @@ export interface ModifyAstBase {
pathToNode: PathToNode pathToNode: PathToNode
} }
export interface AddTagInfo {
node: Program
pathToNode: PathToNode
}
interface addCall extends ModifyAstBase { interface addCall extends ModifyAstBase {
to: [number, number] to: [number, number]
from: [number, number] from: [number, number]
@ -127,7 +132,8 @@ export interface SketchLineHelper {
pathToNode: PathToNode pathToNode: PathToNode
} }
| Error | Error
addTag: (a: ModifyAstBase) => getTag: (a: CallExpression) => string | Error
addTag: (a: AddTagInfo) =>
| { | {
modifiedAst: Program modifiedAst: Program
tag: string tag: string

View File

@ -64,7 +64,7 @@ export interface MouseGuard {
rotate: MouseGuardHandler rotate: MouseGuardHandler
} }
export const butName = (e: React.MouseEvent) => ({ export const btnName = (e: React.MouseEvent) => ({
middle: !!(e.buttons & 4) || e.button === 1, middle: !!(e.buttons & 4) || e.button === 1,
right: !!(e.buttons & 2) || e.button === 2, right: !!(e.buttons & 2) || e.button === 2,
left: !!(e.buttons & 1) || e.button === 0, left: !!(e.buttons & 1) || e.button === 0,
@ -75,8 +75,8 @@ export const cameraMouseDragGuards: Record<CameraSystem, MouseGuard> = {
pan: { pan: {
description: 'Right click + Shift + drag or middle click + drag', description: 'Right click + Shift + drag or middle click + drag',
callback: (e) => callback: (e) =>
(butName(e).middle && noModifiersPressed(e)) || (btnName(e).middle && noModifiersPressed(e)) ||
(butName(e).right && e.shiftKey), (btnName(e).right && e.shiftKey),
}, },
zoom: { zoom: {
description: 'Scroll wheel or Right click + Ctrl + drag', description: 'Scroll wheel or Right click + Ctrl + drag',
@ -85,15 +85,15 @@ export const cameraMouseDragGuards: Record<CameraSystem, MouseGuard> = {
}, },
rotate: { rotate: {
description: 'Right click + drag', description: 'Right click + drag',
callback: (e) => butName(e).right && noModifiersPressed(e), callback: (e) => btnName(e).right && noModifiersPressed(e),
}, },
}, },
OnShape: { OnShape: {
pan: { pan: {
description: 'Right click + Ctrl + drag or middle click + drag', description: 'Right click + Ctrl + drag or middle click + drag',
callback: (e) => callback: (e) =>
(butName(e).right && e.ctrlKey) || (btnName(e).right && e.ctrlKey) ||
(butName(e).middle && noModifiersPressed(e)), (btnName(e).middle && noModifiersPressed(e)),
}, },
zoom: { zoom: {
description: 'Scroll wheel', description: 'Scroll wheel',
@ -102,72 +102,72 @@ export const cameraMouseDragGuards: Record<CameraSystem, MouseGuard> = {
}, },
rotate: { rotate: {
description: 'Right click + drag', description: 'Right click + drag',
callback: (e) => butName(e).right && noModifiersPressed(e), callback: (e) => btnName(e).right && noModifiersPressed(e),
}, },
}, },
'Trackpad Friendly': { 'Trackpad Friendly': {
pan: { pan: {
description: 'Left click + Alt + Shift + drag or middle click + drag', description: 'Left click + Alt + Shift + drag or middle click + drag',
callback: (e) => callback: (e) =>
(butName(e).left && e.altKey && e.shiftKey && !e.metaKey) || (btnName(e).left && e.altKey && e.shiftKey && !e.metaKey) ||
(butName(e).middle && noModifiersPressed(e)), (btnName(e).middle && noModifiersPressed(e)),
}, },
zoom: { zoom: {
description: 'Scroll wheel or Left click + Alt + OS + drag', description: 'Scroll wheel or Left click + Alt + OS + drag',
dragCallback: (e) => butName(e).left && e.altKey && e.metaKey, dragCallback: (e) => btnName(e).left && e.altKey && e.metaKey,
scrollCallback: () => true, scrollCallback: () => true,
}, },
rotate: { rotate: {
description: 'Left click + Alt + drag', description: 'Left click + Alt + drag',
callback: (e) => butName(e).left && e.altKey && !e.shiftKey && !e.metaKey, callback: (e) => btnName(e).left && e.altKey && !e.shiftKey && !e.metaKey,
lenientDragStartButton: 0, lenientDragStartButton: 0,
}, },
}, },
Solidworks: { Solidworks: {
pan: { pan: {
description: 'Right click + Ctrl + drag', description: 'Right click + Ctrl + drag',
callback: (e) => butName(e).right && e.ctrlKey, callback: (e) => btnName(e).right && e.ctrlKey,
lenientDragStartButton: 2, lenientDragStartButton: 2,
}, },
zoom: { zoom: {
description: 'Scroll wheel or Middle click + Shift + drag', description: 'Scroll wheel or Middle click + Shift + drag',
dragCallback: (e) => butName(e).middle && e.shiftKey, dragCallback: (e) => btnName(e).middle && e.shiftKey,
scrollCallback: () => true, scrollCallback: () => true,
}, },
rotate: { rotate: {
description: 'Middle click + drag', description: 'Middle click + drag',
callback: (e) => butName(e).middle && noModifiersPressed(e), callback: (e) => btnName(e).middle && noModifiersPressed(e),
}, },
}, },
NX: { NX: {
pan: { pan: {
description: 'Middle click + Shift + drag', description: 'Middle click + Shift + drag',
callback: (e) => butName(e).middle && e.shiftKey, callback: (e) => btnName(e).middle && e.shiftKey,
}, },
zoom: { zoom: {
description: 'Scroll wheel or Middle click + Ctrl + drag', description: 'Scroll wheel or Middle click + Ctrl + drag',
dragCallback: (e) => butName(e).middle && e.ctrlKey, dragCallback: (e) => btnName(e).middle && e.ctrlKey,
scrollCallback: () => true, scrollCallback: () => true,
}, },
rotate: { rotate: {
description: 'Middle click + drag', description: 'Middle click + drag',
callback: (e) => butName(e).middle && noModifiersPressed(e), callback: (e) => btnName(e).middle && noModifiersPressed(e),
}, },
}, },
Creo: { Creo: {
pan: { pan: {
description: 'Left click + Ctrl + drag', description: 'Left click + Ctrl + drag',
callback: (e) => butName(e).left && !butName(e).right && e.ctrlKey, callback: (e) => btnName(e).left && !btnName(e).right && e.ctrlKey,
}, },
zoom: { zoom: {
description: 'Scroll wheel or Right click + Ctrl + drag', description: 'Scroll wheel or Right click + Ctrl + drag',
dragCallback: (e) => butName(e).right && !butName(e).left && e.ctrlKey, dragCallback: (e) => btnName(e).right && !btnName(e).left && e.ctrlKey,
scrollCallback: () => true, scrollCallback: () => true,
}, },
rotate: { rotate: {
description: 'Middle (or Left + Right) click + Ctrl + drag', description: 'Middle (or Left + Right) click + Ctrl + drag',
callback: (e) => { callback: (e) => {
const b = butName(e) const b = btnName(e)
return (b.middle || (b.left && b.right)) && e.ctrlKey return (b.middle || (b.left && b.right)) && e.ctrlKey
}, },
}, },
@ -175,7 +175,7 @@ export const cameraMouseDragGuards: Record<CameraSystem, MouseGuard> = {
AutoCAD: { AutoCAD: {
pan: { pan: {
description: 'Middle click + drag', description: 'Middle click + drag',
callback: (e) => butName(e).middle && noModifiersPressed(e), callback: (e) => btnName(e).middle && noModifiersPressed(e),
}, },
zoom: { zoom: {
description: 'Scroll wheel', description: 'Scroll wheel',
@ -184,7 +184,7 @@ export const cameraMouseDragGuards: Record<CameraSystem, MouseGuard> = {
}, },
rotate: { rotate: {
description: 'Middle click + Shift + drag', description: 'Middle click + Shift + drag',
callback: (e) => butName(e).middle && e.shiftKey, callback: (e) => btnName(e).middle && e.shiftKey,
}, },
}, },
} }

View File

@ -1,9 +1,9 @@
import { CommandSetConfig } from 'lib/commandTypes' import { StateMachineCommandSetConfig } from 'lib/commandTypes'
import { authMachine } from 'machines/authMachine' import { authMachine } from 'machines/authMachine'
type AuthCommandSchema = {} type AuthCommandSchema = {}
export const authCommandBarConfig: CommandSetConfig< export const authCommandBarConfig: StateMachineCommandSetConfig<
typeof authMachine, typeof authMachine,
AuthCommandSchema AuthCommandSchema
> = { > = {

View File

@ -1,4 +1,4 @@
import { CommandSetConfig } from 'lib/commandTypes' import { StateMachineCommandSetConfig } from 'lib/commandTypes'
import { homeMachine } from 'machines/homeMachine' import { homeMachine } from 'machines/homeMachine'
export type HomeCommandSchema = { export type HomeCommandSchema = {
@ -17,7 +17,7 @@ export type HomeCommandSchema = {
} }
} }
export const homeCommandBarConfig: CommandSetConfig< export const homeCommandBarConfig: StateMachineCommandSetConfig<
typeof homeMachine, typeof homeMachine,
HomeCommandSchema HomeCommandSchema
> = { > = {

View File

@ -1,8 +1,8 @@
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import { CommandSetConfig, KclCommandValue } from 'lib/commandTypes' import { StateMachineCommandSetConfig, KclCommandValue } from 'lib/commandTypes'
import { KCL_DEFAULT_LENGTH } from 'lib/constants' import { KCL_DEFAULT_LENGTH } from 'lib/constants'
import { Selections } from 'lib/selections' import { Selections } from 'lib/selections'
import { modelingMachine } from 'machines/modelingMachine' import { modelingMachine, SketchTool } from 'machines/modelingMachine'
type OutputFormat = Models['OutputFormat_type'] type OutputFormat = Models['OutputFormat_type']
type OutputTypeKey = OutputFormat['type'] type OutputTypeKey = OutputFormat['type']
@ -27,9 +27,17 @@ export type ModelingCommandSchema = {
// result: (typeof EXTRUSION_RESULTS)[number] // result: (typeof EXTRUSION_RESULTS)[number]
distance: KclCommandValue distance: KclCommandValue
} }
Fillet: {
// todo
selection: Selections
radius: KclCommandValue
}
'change tool': {
tool: SketchTool
}
} }
export const modelingMachineConfig: CommandSetConfig< export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
typeof modelingMachine, typeof modelingMachine,
ModelingCommandSchema ModelingCommandSchema
> = { > = {
@ -37,22 +45,47 @@ export const modelingMachineConfig: CommandSetConfig<
description: 'Enter sketch mode.', description: 'Enter sketch mode.',
icon: 'sketch', icon: 'sketch',
}, },
// TODO the event is no 'change tool' with data: 'line', 'rectangle' etc 'change tool': [
// 'Equip Line tool': { {
// description: 'Start drawing straight lines.', description: 'Start drawing straight lines.',
// icon: 'line', icon: 'line',
// displayName: 'Line', displayName: 'Line',
// }, args: {
// 'Equip tangential arc to': { tool: {
// description: 'Start drawing an arc tangent to the current segment.', defaultValue: 'line',
// icon: 'arc', required: true,
// displayName: 'Tangential Arc', skip: true,
// }, inputType: 'string',
// 'Equip rectangle tool': { },
// description: 'Start drawing a rectangle.', },
// icon: 'rectangle', },
// displayName: 'Rectangle', {
// }, description: 'Start drawing an arc tangent to the current segment.',
icon: 'arc',
displayName: 'Tangential Arc',
args: {
tool: {
defaultValue: 'tangentialArc',
required: true,
skip: true,
inputType: 'string',
},
},
},
{
description: 'Start drawing a rectangle.',
icon: 'rectangle',
displayName: 'Rectangle',
args: {
tool: {
defaultValue: 'rectangle',
required: true,
skip: true,
inputType: 'string',
},
},
},
],
Export: { Export: {
description: 'Export the current model.', description: 'Export the current model.',
icon: 'exportFile', icon: 'exportFile',
@ -157,4 +190,36 @@ export const modelingMachineConfig: CommandSetConfig<
}, },
}, },
}, },
Fillet: {
// todo
description: 'Fillet edge',
icon: 'fillet',
needsReview: true,
args: {
selection: {
inputType: 'selection',
selectionTypes: [
'default',
'line-end',
'line-mid',
'extrude-wall', // to fix: accespts only this selection type
'start-cap',
'end-cap',
'point',
'edge',
'line',
'arc',
'all',
],
multiple: true, // TODO: multiple selection like in extrude command
required: true,
skip: true,
},
radius: {
inputType: 'kcl',
defaultValue: KCL_DEFAULT_LENGTH,
required: true,
},
},
},
} }

View File

@ -124,7 +124,8 @@ export function createSettingsCommand({
displayName: `Settings · ${decamelize(type.replaceAll('.', ' · '), { displayName: `Settings · ${decamelize(type.replaceAll('.', ' · '), {
separator: ' ', separator: ' ',
})}`, })}`,
ownerMachine: 'settings', description: settingConfig.description,
groupId: 'settings',
icon: 'settings', icon: 'settings',
needsReview: false, needsReview: false,
onSubmit: (data) => { onSubmit: (data) => {

View File

@ -33,13 +33,13 @@ export interface KclExpressionWithVariable extends KclExpression {
export type KclCommandValue = KclExpression | KclExpressionWithVariable export type KclCommandValue = KclExpression | KclExpressionWithVariable
export type CommandInputType = (typeof INPUT_TYPES)[number] export type CommandInputType = (typeof INPUT_TYPES)[number]
export type CommandSetSchema<T extends AnyStateMachine> = Partial<{ export type StateMachineCommandSetSchema<T extends AnyStateMachine> = Partial<{
[EventType in EventFrom<T>['type']]: Record<string, any> [EventType in EventFrom<T>['type']]: Record<string, any>
}> }>
export type CommandSet< export type StateMachineCommandSet<
T extends AllMachines, T extends AllMachines,
Schema extends CommandSetSchema<T> Schema extends StateMachineCommandSetSchema<T>
> = Partial<{ > = Partial<{
[EventType in EventFrom<T>['type']]: Command< [EventType in EventFrom<T>['type']]: Command<
T, T,
@ -48,24 +48,28 @@ export type CommandSet<
> >
}> }>
export type CommandSetConfig< /**
* A configuration object for a set of commands tied to a state machine.
* Each event type can have one or more commands associated with it.
* @param T The state machine type.
* @param Schema The schema for the command set, defined by the developer.
*/
export type StateMachineCommandSetConfig<
T extends AllMachines, T extends AllMachines,
Schema extends CommandSetSchema<T> Schema extends StateMachineCommandSetSchema<T>
> = Partial<{ > = Partial<{
[EventType in EventFrom<T>['type']]: CommandConfig< [EventType in EventFrom<T>['type']]:
T, | CommandConfig<T, EventFrom<T>['type'], Schema[EventType]>
EventFrom<T>['type'], | CommandConfig<T, EventFrom<T>['type'], Schema[EventType]>[]
Schema[EventType]
>
}> }>
export type Command< export type Command<
T extends AnyStateMachine = AnyStateMachine, T extends AnyStateMachine = AnyStateMachine,
CommandName extends EventFrom<T>['type'] = EventFrom<T>['type'], CommandName extends EventFrom<T>['type'] = EventFrom<T>['type'],
CommandSchema extends CommandSetSchema<T>[CommandName] = CommandSetSchema<T>[CommandName] CommandSchema extends StateMachineCommandSetSchema<T>[CommandName] = StateMachineCommandSetSchema<T>[CommandName]
> = { > = {
name: CommandName name: CommandName
ownerMachine: T['id'] groupId: T['id']
needsReview: boolean needsReview: boolean
onSubmit: (data?: CommandSchema) => void onSubmit: (data?: CommandSchema) => void
onCancel?: () => void onCancel?: () => void
@ -81,10 +85,10 @@ export type Command<
export type CommandConfig< export type CommandConfig<
T extends AnyStateMachine = AnyStateMachine, T extends AnyStateMachine = AnyStateMachine,
CommandName extends EventFrom<T>['type'] = EventFrom<T>['type'], CommandName extends EventFrom<T>['type'] = EventFrom<T>['type'],
CommandSchema extends CommandSetSchema<T>[CommandName] = CommandSetSchema<T>[CommandName] CommandSchema extends StateMachineCommandSetSchema<T>[CommandName] = StateMachineCommandSetSchema<T>[CommandName]
> = Omit< > = Omit<
Command<T, CommandName, CommandSchema>, Command<T, CommandName, CommandSchema>,
'name' | 'ownerMachine' | 'onSubmit' | 'onCancel' | 'args' | 'needsReview' 'name' | 'groupId' | 'onSubmit' | 'onCancel' | 'args' | 'needsReview'
> & { > & {
needsReview?: true needsReview?: true
args?: { args?: {

View File

@ -51,6 +51,7 @@ export const ONBOARDING_PROJECT_NAME = 'Tutorial Project $nn'
export const KCL_DEFAULT_CONSTANT_PREFIXES = { export const KCL_DEFAULT_CONSTANT_PREFIXES = {
SKETCH: 'sketch', SKETCH: 'sketch',
EXTRUDE: 'extrude', EXTRUDE: 'extrude',
SEGMENT: 'seg',
} as const } as const
/** The default KCL length expression */ /** The default KCL length expression */
export const KCL_DEFAULT_LENGTH = `5` export const KCL_DEFAULT_LENGTH = `5`

View File

@ -11,20 +11,20 @@ import {
CommandArgument, CommandArgument,
CommandArgumentConfig, CommandArgumentConfig,
CommandConfig, CommandConfig,
CommandSetConfig, StateMachineCommandSetConfig,
CommandSetSchema, StateMachineCommandSetSchema,
} from './commandTypes' } from './commandTypes'
interface CreateMachineCommandProps< interface CreateMachineCommandProps<
T extends AnyStateMachine, T extends AnyStateMachine,
S extends CommandSetSchema<T> S extends StateMachineCommandSetSchema<T>
> { > {
type: EventFrom<T>['type'] type: EventFrom<T>['type']
ownerMachine: T['id'] groupId: T['id']
state: StateFrom<T> state: StateFrom<T>
send: Function send: Function
actor: InterpreterFrom<T> actor: InterpreterFrom<T>
commandBarConfig?: CommandSetConfig<T, S> commandBarConfig?: StateMachineCommandSetConfig<T, S>
onCancel?: () => void onCancel?: () => void
} }
@ -32,22 +32,39 @@ interface CreateMachineCommandProps<
// from a more terse Command Bar Meta definition. // from a more terse Command Bar Meta definition.
export function createMachineCommand< export function createMachineCommand<
T extends AnyStateMachine, T extends AnyStateMachine,
S extends CommandSetSchema<T> S extends StateMachineCommandSetSchema<T>
>({ >({
ownerMachine, groupId,
type, type,
state, state,
send, send,
actor, actor,
commandBarConfig, commandBarConfig,
onCancel, onCancel,
}: CreateMachineCommandProps<T, S>): Command< }: CreateMachineCommandProps<T, S>):
T, | Command<T, typeof type, S[typeof type]>
typeof type, | Command<T, typeof type, S[typeof type]>[]
S[typeof type] | null {
> | null {
const commandConfig = commandBarConfig && commandBarConfig[type] const commandConfig = commandBarConfig && commandBarConfig[type]
if (!commandConfig) return null // There may be no command config for this event type,
// or there may be multiple commands to create.
if (!commandConfig) {
return null
} else if (commandConfig instanceof Array) {
return commandConfig
.map((config) =>
createMachineCommand({
groupId,
type,
state,
send,
actor,
commandBarConfig: { [type]: config },
onCancel,
})
)
.filter((c) => c !== null) as Command<T, typeof type, S[typeof type]>[]
}
// Hide commands based on platform by returning `null` // Hide commands based on platform by returning `null`
// so the consumer can filter them out // so the consumer can filter them out
@ -62,8 +79,9 @@ export function createMachineCommand<
const command: Command<T, typeof type, S[typeof type]> = { const command: Command<T, typeof type, S[typeof type]> = {
name: type, name: type,
ownerMachine: ownerMachine, groupId,
icon, icon,
description: commandConfig.description,
needsReview: commandConfig.needsReview || false, needsReview: commandConfig.needsReview || false,
onSubmit: (data?: S[typeof type]) => { onSubmit: (data?: S[typeof type]) => {
if (data !== undefined && data !== null) { if (data !== undefined && data !== null) {
@ -84,6 +102,10 @@ export function createMachineCommand<
command.onCancel = onCancel command.onCancel = onCancel
} }
if ('displayName' in commandConfig) {
command.displayName = commandConfig.displayName
}
return command return command
} }
@ -92,7 +114,7 @@ export function createMachineCommand<
// bundled together into the args for a Command. // bundled together into the args for a Command.
function buildCommandArguments< function buildCommandArguments<
T extends AnyStateMachine, T extends AnyStateMachine,
S extends CommandSetSchema<T>, S extends StateMachineCommandSetSchema<T>,
CommandName extends EventFrom<T>['type'] = EventFrom<T>['type'] CommandName extends EventFrom<T>['type'] = EventFrom<T>['type']
>( >(
state: StateFrom<T>, state: StateFrom<T>,
@ -112,7 +134,7 @@ function buildCommandArguments<
export function buildCommandArgument< export function buildCommandArgument<
T extends AnyStateMachine, T extends AnyStateMachine,
O extends CommandSetSchema<T> = CommandSetSchema<T> O extends StateMachineCommandSetSchema<T> = StateMachineCommandSetSchema<T>
>( >(
arg: CommandArgumentConfig<O, T>, arg: CommandArgumentConfig<O, T>,
context: ContextFrom<T>, context: ContextFrom<T>,

View File

@ -406,6 +406,17 @@ export function canExtrudeSelection(selection: Selections) {
) )
} }
export function canFilletSelection(selection: Selections) {
const commonNodes = selection.codeBasedSelections.map((_, i) =>
buildCommonNodeFromSelection(selection, i)
) // TODO FILLET DUMMY PLACEHOLDER
return (
!!isSketchPipe(selection) &&
commonNodes.every((n) => nodeHasClose(n)) &&
commonNodes.every((n) => !nodeHasExtrude(n))
)
}
function canExtrudeSelectionItem(selection: Selections, i: number) { function canExtrudeSelectionItem(selection: Selections, i: number) {
const commonNode = buildCommonNodeFromSelection(selection, i) const commonNode = buildCommonNodeFromSelection(selection, i)

View File

@ -163,6 +163,17 @@ export function createSettings() {
validate: (v) => typeof v === 'boolean', validate: (v) => typeof v === 'boolean',
hideOnPlatform: 'both', //for now hideOnPlatform: 'both', //for now
}), }),
/**
* Stream resource saving behavior toggle
*/
streamIdleMode: new Setting<boolean>({
defaultValue: false,
description: 'Toggle stream idling, saving bandwidth and battery',
validate: (v) => typeof v === 'boolean',
commandConfig: {
inputType: 'boolean',
},
}),
onboardingStatus: new Setting<string>({ onboardingStatus: new Setting<string>({
defaultValue: '', defaultValue: '',
validate: (v) => typeof v === 'string', validate: (v) => typeof v === 'string',

View File

@ -38,6 +38,7 @@ function configurationToSettingsPayload(
: undefined, : undefined,
onboardingStatus: configuration?.settings?.app?.onboarding_status, onboardingStatus: configuration?.settings?.app?.onboarding_status,
dismissWebBanner: configuration?.settings?.app?.dismiss_web_banner, dismissWebBanner: configuration?.settings?.app?.dismiss_web_banner,
streamIdleMode: configuration?.settings?.app?.stream_idle_mode,
projectDirectory: configuration?.settings?.project?.directory, projectDirectory: configuration?.settings?.project?.directory,
enableSSAO: configuration?.settings?.modeling?.enable_ssao, enableSSAO: configuration?.settings?.modeling?.enable_ssao,
}, },
@ -75,6 +76,7 @@ function projectConfigurationToSettingsPayload(
: undefined, : undefined,
onboardingStatus: configuration?.settings?.app?.onboarding_status, onboardingStatus: configuration?.settings?.app?.onboarding_status,
dismissWebBanner: configuration?.settings?.app?.dismiss_web_banner, dismissWebBanner: configuration?.settings?.app?.dismiss_web_banner,
streamIdleMode: configuration?.settings?.app?.stream_idle_mode,
enableSSAO: configuration?.settings?.modeling?.enable_ssao, enableSSAO: configuration?.settings?.modeling?.enable_ssao,
}, },
modeling: { modeling: {

View File

@ -37,4 +37,7 @@ if (typeof window !== 'undefined') {
document.addEventListener('mousemove', (e) => document.addEventListener('mousemove', (e) =>
console.log(`await page.mouse.click(${e.clientX}, ${e.clientY})`) console.log(`await page.mouse.click(${e.clientX}, ${e.clientY})`)
) )
;(window as any).enableFillet = () => {
;(window as any)._enableFillet = true
}
} }

View File

@ -6,7 +6,7 @@ import { PrevVariable, findAllPreviousVariables } from 'lang/queryAst'
import { Value, parse } from 'lang/wasm' import { Value, parse } from 'lang/wasm'
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { executeAst } from 'lang/langHelpers' import { executeAst } from 'lang/langHelpers'
import { trap } from 'lib/trap' import { err, trap } from 'lib/trap'
const isValidVariableName = (name: string) => const isValidVariableName = (name: string) =>
/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name) /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)
@ -86,6 +86,7 @@ export function useCalculateKclExpression({
const execAstAndSetResult = async () => { const execAstAndSetResult = async () => {
const _code = `const __result__ = ${value}` const _code = `const __result__ = ${value}`
const ast = parse(_code) const ast = parse(_code)
if (err(ast)) return
if (trap(ast, { suppress: true })) return if (trap(ast, { suppress: true })) return
const _programMem: any = { root: {}, return: null } const _programMem: any = { root: {}, return: null }

View File

@ -57,7 +57,7 @@ export type CommandBarMachineEvent =
} }
| { | {
type: 'Find and select command' type: 'Find and select command'
data: { name: string; ownerMachine: string } data: { name: string; groupId: string }
} }
| { | {
type: 'Change current argument' type: 'Change current argument'
@ -66,7 +66,7 @@ export type CommandBarMachineEvent =
export const commandBarMachine = createMachine( export const commandBarMachine = createMachine(
{ {
/** @xstate-layout N4IgpgJg5mDOIC5QGED2BbdBDAdhABAEJYBOAxMgDaqxgDaADALqKgAONAlgC6eo6sQAD0QBaAJwA6AGwAmAKwBmBoukAWafIAcDcSoA0IAJ6JZDaZIDs8hgzV6AjA61a1DWQF8PhtJlwFiciowUkYWJBAOWB4+AQiRBFF5CwdpcVkHS1lpVyU5QxNEh1lFGTUsrUUtOQd5SwZLLx8MbDwiUkkqGkgyAHk2MBwwwSiY-kEEswZJbPltM0U3eXLZAsRFeQcrerUHRbTFvfkmkF9WgI6u2ggyADFONv98WkowAGNufDeW-2GI0d443iiHESkktUUilkskqdiUWjWCAcDC0kjUqnElkc6lkoK0JzOT0CnWo1zIAEEIARvn48LA-uwuIC4qAEqIHJjJPJcYtxFoYdJFFjVsZEG5pqCquJxJoGvJxOUCT82sSrj0AEpgdCoABuYC+yog9OYIyZsQmYmhWzMmTqLnU0kyikRGVRbj5lg2SmUam5StpFxIkgAymBXh8HlADQGyKHw58aecGZEzUDWYgchY7CisWoSlpQQ5EVDLJJxKkdIpyypUop-ed2kHCW0Xu9uD1kwDzcCEJCtlUNgWhZZ1OlnaKEBpZJItHVzDZ5xlpPWiZdDc8w22Ow5wozosyLUiVNMSg5ytVKmfrIipzO564z2otPVpI1vKd18SAOJYbgACzAEhI3wUgoAAV3QQZuFgCg-1wGAvjAkgSCgkCSHAyCcG4TtUxZYRED2TQZGSOw80qaQ7ARCc9mmZYSksextGkNJBRXFUOh-f9AOA0CIKgmCABE4E3D5oyTE1-lww8clKBjZF2Bh5SFZwXUUyRVDzJwNE2LQzzYwNJE4gCgJwKNeMw6DJHJAB3LAYlM-AHjYMDuFjMCACN0B4NCMKgnD927dMkWSUs8yY8RISfWQshvcsrDlapshULJPHfZsDKM7iHPM-jJAANSwShOAgX9IzICB+DASQHh1VAAGsqp1Qrit-MByXQvisP8sY8ISZwbCsDllBLSwz1qRFBQsRZ5WRIVkui-TG0M39jJ4jqLNgfLmpK3hTLIQCSFQIM2EoX8ADMjvQSQmqKna2vWvyJL3HrD1SEoyzSdJUkUm9dnUtQn10aK6hkxbiU1HVODAay3M87zE1+J6UwCtN8IQUauR0OZ0ksFwUUqRFPWmUdouPXR3TBjoIahmHKQIHKuqRrtUYSaVShhLF+TkeTzBFQosVKDRM2RVInEdSmg2p6GyE1bU9R8zrsKZqSexFqQHWlHNXHzCbFhnX1+UydFkVxiXJClmGAFEIG8hmld3ZGXp7TR5C5RSCxdjlQV1tR9bqaVdhsSFxDN5AALeOrgPa3ysJgiqcCqmr6sa7bWujxXjQd5nesQZYthsblIQYJx6hUic9AsdEFT2HQzByVLmgDJaw-eSOHPTjbysq6qcFqhrrtT9sO-4ugd1NFGc4QXEPo5eVLGsFxlnKREXGnZI9HcT1mOikO0s-S5w7bqNh9j-bkKOyQTvOy6B9utOHtj7qD1V8pSyrHJZ3STY4QmjQ0R2Ww0oSjuF3u+HAqAIBwEEOlRs48nZBVENkKQNprC42qBoJ0LothaXMJ9aElRXDLj3k3VUpJIBwOfgg4oMx8y41SPUOY8p5DFk2GiKoxsoRVmhGoM2cY2zAQRngChgU0a7HZtFH0ilRp1D5usVh6JXCKD2CUdI8lQ7rlbB8chkkJ6HkxFsBeulbQZAUCwrYCiqzIhyE+fqZtMomTMg-aCwiWYEV2NOBUCghQ6EFGzYsvpwQUT5MxawFECx2JWllRxMdLI2TsrtKMTkXIuMnmeCiZYwrePMFWCKv1XZKVGroWwmxNARK4g4hWG0tp3wSSk16lQpC41ULjV8uxUjSBvFCNEDTdCOkWCY44xCGzgzAJDaGdTnaZHBADYcqg5DpCovzeRno5izA0BeUOh8o5OPgDo+BaNvqV0xNFaE7gdYTnnvkmUChdKEMyF4LwQA */ /** @xstate-layout N4IgpgJg5mDOIC5QGED2BbdBDAdhABAEJYBOAxMgDaqxgDaADALqKgAONAlgC6eo6sQAD0QBaAJwA6AGwAmAKwBmBoukAWafIAcDcSoA0IAJ6JZDaZIDs8hgzV6AjA61a1DWQF8PhtJlwFiciowUkYWJBAOWB4+AQiRBFF5CwdpcVkHS1lpVyU5QxNEh1lFGTUsrUUtOQd5SwZLLx8MbDwiUkkqGkgyAHk2MBwwwSiY-kEEswZJbPltM0U3eXLZAsRFeQcrerUHRbTFvfkmkF9WgI6u2ggyADFONv98WkowAGNufDeW-2GI0d443iiHESkktUUilkskqdiUWjWCAcDC0kjUqnElkc6lkoK0JzOT0CnWo1zIAEEIARvn48LA-uwuIC4qAEqIHJjJPJcYtxFoYdJFFjVsZEG5pqCquJxJoGvJxOUCT82sSrj0AEpgdCoABuYC+yog9OYIyZsQmYmhWzMmTqLnU0kyikRGVRbj5lg2SmUam5StpFxIkgAymBXh8HlADQGyKHw58aecGZEzUDWYgchY7CisWoSlpQQ5EVDLJJxKkdIpyypUop-ed2kHCW0Xu9uD1kwDzcCEJCtlUNgWhZZ1OlnaKEBpZJItHVzDZ5xlpPWiZdDc8w22O7JwozosyLUj3KiZZY85YtMUNgx5Ii81JuS5xBtPVCCyuVR0AOJYbgACzAEhI3wUgoAAV3QQZuFgCg-1wGAvjAkgSCgkCSHAyCcG4TtUxZYRED2TQZGSOw80qaQ7ARCc9mmZYSjPPFpDSQUP0DSQf3-QDgNAiCoJggAROBNw+aMkxNf5cMPHJSjPWRdhvZ9LGcF0b0kVQ8ycDRNkvNRWMbdjfwAoCcCjHjMOgyRyQAdywGITPwB42DA7hYzAgAjdAeDQjCoJw-du3TJE6mnWwoT0NIUTUAs71sMtdGsRYCyUtI9OJDijO49DeKw2BJAANSwShOAgX9IzICB+DASQHh1VAAGsqp1Qrit-MBySy8y-LGPCElSeoy2lZJcwcNRfXHQpBWmWQsRsPYdDPZJUu-QyuPssy+Py5qSt4EyyEAkhUCDNhKF-AAzQ70EkJqiu2tqOt88S926w9UhhLlykWXFHXcTJixsd60hHGxNj2Jag01HVODAKzXI8rzE1+R6U38tN8IQJSuR0OZ0gvHQXHGxBPWmUdppUWwFV0MHJAhqGYcpAh1qwrqDx7ZFajUmVpulEbqlqREsfBTQ0jcPM5P5KmaehshNW1PVvOy7Cka7VHeoYDkyjSPQtAvPMqkRQU1BnX1+UydFkQvCWwEhqWAFEIC8xnFd3ZHntZuTDZG4ob1nGxPTUfXrBnS8nDmEpT2ObxTnXVUALeOrgPanycvKyrqpwWqGqurbWsThXjWd5WesQZYtmBkplCceplIncK0SrXYqnccxxcj5s2OQWP4-s3PzJgiqcCqmr6sa7P2x7vi6AcAvJJ7XEy0dUFMWsFxlnKfn+S5CL3E9Jiuapjv3i7qNx+T-bDskY6zourObpz+6cuZgK0Y5Ua0TqEvXDkJR-YnR0s1xjkIMnDln3uuVsHwOxT1NCjIuR5nCSBUExJi1huRqyooUTYpYnzeyUFkKsy4Tg4FQBAOAgg26Nmga7QKohshSBtNYXGDonQumtPYaQfsSjLBHDefepJICUJZtQ4oMx8wXj6jebGt4JwbC2OiVwig9hh2hLpVu0cOhxjbMBBGeABFPwSA3ERRMlhKWCn9WRVQzZQirMo0BAYNzxn4RJGBh5MRbGXsHawGQFBmLrpUasOQorOAjs0OxaUVrGVMvfaCuiVYEV2KWTYg1yxyA0i6ZYaIPSL3lOkS8wSo6hOWpxCJ8te6WRsnZKMjlnIxNgSNIiiTF6vVSdI9JM1taXlxLzTwqiClBnSqtSJScLIFVvjtKANSXquENqoJwtgyZzRFIUQ4MhqgNACdNTQCpLbWyshMnsjpSwjXRJYHecgcmImsLIz0odNAaGqPiHpDYY6HwTlE+ATiqHPwohYewWQshmGhHrCcJz5Bck5toZwGhMheC8EAA */
predictableActionArguments: true, predictableActionArguments: true,
tsTypes: {} as import('./commandBarMachine.typegen').Typegen0, tsTypes: {} as import('./commandBarMachine.typegen').Typegen0,
context: { context: {
@ -120,9 +120,7 @@ export const commandBarMachine = createMachine(
context.commands.filter( context.commands.filter(
(c) => (c) =>
!event.data.commands.some( !event.data.commands.some(
(c2) => (c2) => c2.name === c.name && c2.groupId === c.groupId
c2.name === c.name &&
c2.ownerMachine === c.ownerMachine
) )
), ),
}), }),
@ -149,6 +147,10 @@ export const commandBarMachine = createMachine(
cond: 'Command has no arguments', cond: 'Command has no arguments',
actions: ['Execute command'], actions: ['Execute command'],
}, },
{
target: 'Checking Arguments',
cond: 'All arguments are skippable',
},
{ {
target: 'Gathering arguments', target: 'Gathering arguments',
actions: ['Set current argument to first non-skippable'], actions: ['Set current argument to first non-skippable'],
@ -201,7 +203,7 @@ export const commandBarMachine = createMachine(
'Change current argument': { 'Change current argument': {
target: 'Gathering arguments', target: 'Gathering arguments',
internal: true, internal: true,
actions: ['Remove current argument and set a new one'], actions: ['Set current argument'],
}, },
'Deselect command': { 'Deselect command': {
@ -357,29 +359,13 @@ export const commandBarMachine = createMachine(
switch (event.type) { switch (event.type) {
case 'Edit argument': case 'Edit argument':
return event.data.arg return event.data.arg
case 'Change current argument':
return Object.values(event.data)[0]
default: default:
return context.currentArgument return context.currentArgument
} }
}, },
}), }),
'Remove current argument and set a new one': assign({
argumentsToSubmit: (context, event) => {
if (
event.type !== 'Change current argument' ||
!context.currentArgument
)
return context.argumentsToSubmit
const { name } = context.currentArgument
const { [name]: _, ...rest } = context.argumentsToSubmit
return rest
},
currentArgument: (context, event) => {
if (event.type !== 'Change current argument')
return context.currentArgument
return Object.values(event.data)[0]
},
}),
'Clear argument data': assign({ 'Clear argument data': assign({
selectedCommand: undefined, selectedCommand: undefined,
currentArgument: undefined, currentArgument: undefined,
@ -393,9 +379,7 @@ export const commandBarMachine = createMachine(
selectedCommand: (c, e) => { selectedCommand: (c, e) => {
if (e.type !== 'Find and select command') return c.selectedCommand if (e.type !== 'Find and select command') return c.selectedCommand
const found = c.commands.find( const found = c.commands.find(
(cmd) => (cmd) => cmd.name === e.data.name && cmd.groupId === e.data.groupId
cmd.name === e.data.name &&
cmd.ownerMachine === e.data.ownerMachine
) )
return !!found ? found : c.selectedCommand return !!found ? found : c.selectedCommand
@ -514,7 +498,9 @@ export const commandBarMachine = createMachine(
) )
function sortCommands(a: Command, b: Command) { function sortCommands(a: Command, b: Command) {
if (b.ownerMachine === 'auth') return -1 if (b.groupId === 'auth' && !(a.groupId === 'auth')) return -2
if (a.ownerMachine === 'auth') return 1 if (a.groupId === 'auth' && !(b.groupId === 'auth')) return 2
if (b.groupId === 'settings' && !(a.groupId === 'settings')) return -1
if (a.groupId === 'settings' && !(b.groupId === 'settings')) return 1
return a.name.localeCompare(b.name) return a.name.localeCompare(b.name)
} }

File diff suppressed because one or more lines are too long

View File

@ -63,6 +63,12 @@ export const settingsMachine = createMachine(
], ],
}, },
'set.app.streamIdleMode': {
target: 'persisting settings',
actions: ['setSettingAtLevel', 'toastSuccess'],
},
'set.modeling.highlightEdges': { 'set.modeling.highlightEdges': {
target: 'persisting settings', target: 'persisting settings',

View File

@ -57,6 +57,9 @@ const Home = () => {
kclManager.cancelAllExecutions() kclManager.cancelAllExecutions()
}, []) }, [])
useHotkeys('backspace', (e) => {
e.preventDefault()
})
useHotkeys( useHotkeys(
isTauri() ? 'mod+,' : 'shift+mod+,', isTauri() ? 'mod+,' : 'shift+mod+,',
() => navigate(paths.HOME + paths.SETTINGS), () => navigate(paths.HOME + paths.SETTINGS),

View File

@ -169,7 +169,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -180,7 +180,7 @@ checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -191,7 +191,7 @@ checksum = "3c87f3f15e7794432337fc718554eaa4dc8f04c9677a950ffe366f20a162ae42"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -431,7 +431,7 @@ dependencies = [
"heck 0.5.0", "heck 0.5.0",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -631,7 +631,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"strsim 0.10.0", "strsim 0.10.0",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -642,7 +642,7 @@ checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f"
dependencies = [ dependencies = [
"darling_core", "darling_core",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -697,7 +697,7 @@ checksum = "4078275de501a61ceb9e759d37bdd3d7210e654dbc167ac1a3678ef4435ed57b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
"synstructure", "synstructure",
] ]
@ -726,7 +726,7 @@ dependencies = [
"rustfmt-wrapper", "rustfmt-wrapper",
"serde", "serde",
"serde_tokenstream", "serde_tokenstream",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -737,7 +737,7 @@ checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -764,7 +764,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -936,7 +936,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -1026,7 +1026,7 @@ dependencies = [
"inflections", "inflections",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -1448,7 +1448,7 @@ dependencies = [
"pretty_assertions", "pretty_assertions",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -1824,7 +1824,7 @@ dependencies = [
"regex", "regex",
"regex-syntax 0.8.3", "regex-syntax 0.8.3",
"structmeta", "structmeta",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -1842,7 +1842,7 @@ dependencies = [
"bincode", "bincode",
"either", "either",
"fnv", "fnv",
"itertools 0.10.5", "itertools 0.12.1",
"lazy_static", "lazy_static",
"nom", "nom",
"quick-xml", "quick-xml",
@ -1877,7 +1877,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -2041,7 +2041,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"pyo3-macros-backend", "pyo3-macros-backend",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -2054,7 +2054,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"pyo3-build-config", "pyo3-build-config",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -2516,7 +2516,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"serde_derive_internals", "serde_derive_internals",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -2590,7 +2590,7 @@ checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -2601,7 +2601,7 @@ checksum = "330f01ce65a3a5fe59a60c82f3c9a024b573b8a6e875bd233fe5f934e71d54e3"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -2624,7 +2624,7 @@ checksum = "0b2e6b945e9d3df726b65d6ee24060aff8e3533d431f677a9695db04eff9dfdb"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -2645,7 +2645,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"serde", "serde",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -2782,7 +2782,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"structmeta-derive", "structmeta-derive",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -2793,7 +2793,7 @@ checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -2837,9 +2837,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.70" version = "2.0.71"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f0209b68b3613b093e0ec905354eccaedcfe83b8cb37cbdeae64026c3064c16" checksum = "b146dcf730474b4bcd16c311627b31ede9ab149045db4d6088b3becaea046462"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -2860,7 +2860,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -2928,22 +2928,22 @@ dependencies = [
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.61" version = "1.0.62"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" checksum = "f2675633b1499176c2dff06b0856a27976a8f9d436737b4cf4f312d4d91d8bbb"
dependencies = [ dependencies = [
"thiserror-impl", "thiserror-impl",
] ]
[[package]] [[package]]
name = "thiserror-impl" name = "thiserror-impl"
version = "1.0.61" version = "1.0.62"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" checksum = "d20468752b09f49e909e55a5d338caa8bedf615594e9d80bc4c565d30faf798c"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -3039,7 +3039,7 @@ checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -3192,7 +3192,7 @@ checksum = "84fd902d4e0b9a4b27f2f440108dc034e1758628a9b702f8ec61ad66355422fa"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -3220,7 +3220,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -3297,7 +3297,7 @@ checksum = "c88cc88fd23b5a04528f3a8436024f20010a16ec18eb23c164b1242f65860130"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
"termcolor", "termcolor",
] ]
@ -3455,7 +3455,7 @@ dependencies = [
"proc-macro-error", "proc-macro-error",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -3516,7 +3516,7 @@ dependencies = [
"once_cell", "once_cell",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -3551,7 +3551,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
"wasm-bindgen-backend", "wasm-bindgen-backend",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -3876,7 +3876,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]

View File

@ -20,7 +20,7 @@ quote = "1"
regex = "1.10" regex = "1.10"
serde = { version = "1.0.204", features = ["derive"] } serde = { version = "1.0.204", features = ["derive"] }
serde_tokenstream = "0.2" serde_tokenstream = "0.2"
syn = { version = "2.0.70", features = ["full"] } syn = { version = "2.0.71", features = ["full"] }
[dev-dependencies] [dev-dependencies]
anyhow = "1.0.86" anyhow = "1.0.86"

View File

@ -837,7 +837,7 @@ fn generate_code_block_test(fn_name: &str, code_block: &str, index: usize) -> pr
// Read the output file. // Read the output file.
let actual = image::io::Reader::open(output_file).unwrap().decode().unwrap(); let actual = image::io::Reader::open(output_file).unwrap().decode().unwrap();
twenty_twenty::assert_image(&format!("tests/outputs/{}.png", #test_name_str), &actual, 1.0); twenty_twenty::assert_image(&format!("tests/outputs/{}.png", #test_name_str), &actual, 0.99);
} }
} }

View File

@ -86,7 +86,7 @@ mod test_examples_someFn {
twenty_twenty::assert_image( twenty_twenty::assert_image(
&format!("tests/outputs/{}.png", "serial_test_example_someFn0"), &format!("tests/outputs/{}.png", "serial_test_example_someFn0"),
&actual, &actual,
1.0, 0.99,
); );
} }
} }

View File

@ -86,7 +86,7 @@ mod test_examples_someFn {
twenty_twenty::assert_image( twenty_twenty::assert_image(
&format!("tests/outputs/{}.png", "serial_test_example_someFn0"), &format!("tests/outputs/{}.png", "serial_test_example_someFn0"),
&actual, &actual,
1.0, 0.99,
); );
} }
} }

View File

@ -86,7 +86,7 @@ mod test_examples_show {
twenty_twenty::assert_image( twenty_twenty::assert_image(
&format!("tests/outputs/{}.png", "serial_test_example_show0"), &format!("tests/outputs/{}.png", "serial_test_example_show0"),
&actual, &actual,
1.0, 0.99,
); );
} }
@ -176,7 +176,7 @@ mod test_examples_show {
twenty_twenty::assert_image( twenty_twenty::assert_image(
&format!("tests/outputs/{}.png", "serial_test_example_show1"), &format!("tests/outputs/{}.png", "serial_test_example_show1"),
&actual, &actual,
1.0, 0.99,
); );
} }
} }

View File

@ -86,7 +86,7 @@ mod test_examples_show {
twenty_twenty::assert_image( twenty_twenty::assert_image(
&format!("tests/outputs/{}.png", "serial_test_example_show0"), &format!("tests/outputs/{}.png", "serial_test_example_show0"),
&actual, &actual,
1.0, 0.99,
); );
} }
} }

View File

@ -88,7 +88,7 @@ mod test_examples_my_func {
twenty_twenty::assert_image( twenty_twenty::assert_image(
&format!("tests/outputs/{}.png", "serial_test_example_my_func0"), &format!("tests/outputs/{}.png", "serial_test_example_my_func0"),
&actual, &actual,
1.0, 0.99,
); );
} }
@ -178,7 +178,7 @@ mod test_examples_my_func {
twenty_twenty::assert_image( twenty_twenty::assert_image(
&format!("tests/outputs/{}.png", "serial_test_example_my_func1"), &format!("tests/outputs/{}.png", "serial_test_example_my_func1"),
&actual, &actual,
1.0, 0.99,
); );
} }
} }

View File

@ -88,7 +88,7 @@ mod test_examples_line_to {
twenty_twenty::assert_image( twenty_twenty::assert_image(
&format!("tests/outputs/{}.png", "serial_test_example_line_to0"), &format!("tests/outputs/{}.png", "serial_test_example_line_to0"),
&actual, &actual,
1.0, 0.99,
); );
} }
@ -178,7 +178,7 @@ mod test_examples_line_to {
twenty_twenty::assert_image( twenty_twenty::assert_image(
&format!("tests/outputs/{}.png", "serial_test_example_line_to1"), &format!("tests/outputs/{}.png", "serial_test_example_line_to1"),
&actual, &actual,
1.0, 0.99,
); );
} }
} }

View File

@ -86,7 +86,7 @@ mod test_examples_min {
twenty_twenty::assert_image( twenty_twenty::assert_image(
&format!("tests/outputs/{}.png", "serial_test_example_min0"), &format!("tests/outputs/{}.png", "serial_test_example_min0"),
&actual, &actual,
1.0, 0.99,
); );
} }
@ -176,7 +176,7 @@ mod test_examples_min {
twenty_twenty::assert_image( twenty_twenty::assert_image(
&format!("tests/outputs/{}.png", "serial_test_example_min1"), &format!("tests/outputs/{}.png", "serial_test_example_min1"),
&actual, &actual,
1.0, 0.99,
); );
} }
} }

View File

@ -86,7 +86,7 @@ mod test_examples_show {
twenty_twenty::assert_image( twenty_twenty::assert_image(
&format!("tests/outputs/{}.png", "serial_test_example_show0"), &format!("tests/outputs/{}.png", "serial_test_example_show0"),
&actual, &actual,
1.0, 0.99,
); );
} }
} }

View File

@ -86,7 +86,7 @@ mod test_examples_import {
twenty_twenty::assert_image( twenty_twenty::assert_image(
&format!("tests/outputs/{}.png", "serial_test_example_import0"), &format!("tests/outputs/{}.png", "serial_test_example_import0"),
&actual, &actual,
1.0, 0.99,
); );
} }
} }

View File

@ -86,7 +86,7 @@ mod test_examples_import {
twenty_twenty::assert_image( twenty_twenty::assert_image(
&format!("tests/outputs/{}.png", "serial_test_example_import0"), &format!("tests/outputs/{}.png", "serial_test_example_import0"),
&actual, &actual,
1.0, 0.99,
); );
} }
} }

View File

@ -86,7 +86,7 @@ mod test_examples_import {
twenty_twenty::assert_image( twenty_twenty::assert_image(
&format!("tests/outputs/{}.png", "serial_test_example_import0"), &format!("tests/outputs/{}.png", "serial_test_example_import0"),
&actual, &actual,
1.0, 0.99,
); );
} }
} }

View File

@ -86,7 +86,7 @@ mod test_examples_show {
twenty_twenty::assert_image( twenty_twenty::assert_image(
&format!("tests/outputs/{}.png", "serial_test_example_show0"), &format!("tests/outputs/{}.png", "serial_test_example_show0"),
&actual, &actual,
1.0, 0.99,
); );
} }
} }

View File

@ -15,7 +15,7 @@ databake = "0.1.8"
kcl-lib = { path = "../kcl" } kcl-lib = { path = "../kcl" }
proc-macro2 = "1" proc-macro2 = "1"
quote = "1" quote = "1"
syn = { version = "2.0.70", features = ["full"] } syn = { version = "2.0.71", features = ["full"] }
[dev-dependencies] [dev-dependencies]
pretty_assertions = "1.4.0" pretty_assertions = "1.4.0"

View File

@ -35,7 +35,7 @@ schemars = { version = "0.8.17", features = ["impl_json_schema", "url", "uuid1"]
serde = { version = "1.0.204", features = ["derive"] } serde = { version = "1.0.204", features = ["derive"] }
serde_json = "1.0.120" serde_json = "1.0.120"
sha2 = "0.10.8" sha2 = "0.10.8"
thiserror = "1.0.61" thiserror = "1.0.62"
toml = "0.8.14" toml = "0.8.14"
ts-rs = { version = "9.0.1", features = ["uuid-impl", "url-impl", "chrono-impl", "no-serde-warnings", "serde-json-impl"] } ts-rs = { version = "9.0.1", features = ["uuid-impl", "url-impl", "chrono-impl", "no-serde-warnings", "serde-json-impl"] }
url = { version = "2.5.2", features = ["serde"] } url = { version = "2.5.2", features = ["serde"] }
@ -98,6 +98,10 @@ harness = false
name = "compiler_benchmark_iai" name = "compiler_benchmark_iai"
harness = false harness = false
[[bench]]
name = "digest_benchmark"
harness = false
[[bench]] [[bench]]
name = "lsp_semantic_tokens_benchmark_criterion" name = "lsp_semantic_tokens_benchmark_criterion"
harness = false harness = false

View File

@ -0,0 +1,31 @@
use criterion::{criterion_group, criterion_main, Criterion};
pub fn bench_digest(c: &mut Criterion) {
for (name, file) in [
("pipes_on_pipes", PIPES_PROGRAM),
("big_kitt", KITT_PROGRAM),
("cube", CUBE_PROGRAM),
("math", MATH_PROGRAM),
("mike_stress_test", MIKE_STRESS_TEST_PROGRAM),
] {
let tokens = kcl_lib::token::lexer(file).unwrap();
let prog = kcl_lib::parser::Parser::new(tokens).ast().unwrap();
c.bench_function(&format!("digest_{name}"), move |b| {
let prog = prog.clone();
b.iter(move || {
let mut prog = prog.clone();
prog.compute_digest();
});
});
}
}
criterion_group!(benches, bench_digest);
criterion_main!(benches);
const KITT_PROGRAM: &str = include_str!("../../tests/executor/inputs/kittycad_svg.kcl");
const PIPES_PROGRAM: &str = include_str!("../../tests/executor/inputs/pipes_on_pipes.kcl");
const CUBE_PROGRAM: &str = include_str!("../../tests/executor/inputs/cube.kcl");
const MATH_PROGRAM: &str = include_str!("../../tests/executor/inputs/math.kcl");
const MIKE_STRESS_TEST_PROGRAM: &str = include_str!("../../tests/executor/inputs/mike_stress_test.kcl");

View File

@ -100,18 +100,6 @@ impl ProgramMemory {
}) })
.collect() .collect()
} }
/// Get all TagDeclarators and TagIdentifiers in the memory.
pub fn get_tags(&self) -> HashMap<String, MemoryItem> {
self.root
.values()
.filter_map(|item| match item {
MemoryItem::TagDeclarator(t) => Some((t.name.to_string(), item.clone())),
MemoryItem::TagIdentifier(t) => Some((t.value.to_string(), item.clone())),
_ => None,
})
.collect::<HashMap<String, MemoryItem>>()
}
} }
impl Default for ProgramMemory { impl Default for ProgramMemory {

View File

@ -232,7 +232,7 @@ impl crate::lsp::backend::Backend for Backend {
// Lets update the ast. // Lets update the ast.
let parser = crate::parser::Parser::new(tokens.clone()); let parser = crate::parser::Parser::new(tokens.clone());
let result = parser.ast(); let result = parser.ast();
let ast = match result { let mut ast = match result {
Ok(ast) => ast, Ok(ast) => ast,
Err(err) => { Err(err) => {
self.add_to_diagnostics(&params, &[err], true).await; self.add_to_diagnostics(&params, &[err], true).await;
@ -243,6 +243,11 @@ impl crate::lsp::backend::Backend for Backend {
} }
}; };
// Here we will want to store the digest and compare, but for now
// we're doing this in a non-load-bearing capacity so we can remove
// this if it backfires and only hork the LSP.
ast.compute_digest();
// Check if the ast changed. // Check if the ast changed.
let ast_changed = match self.ast_map.get(&filename) { let ast_changed = match self.ast_map.get(&filename) {
Some(old_ast) => { Some(old_ast) => {

View File

@ -2371,9 +2371,12 @@ async fn serial_test_kcl_lsp_full_to_empty_file_updates_ast_and_memory() {
}) })
.await; .await;
let mut default_hashed = crate::ast::types::Program::default();
default_hashed.compute_digest();
// Get the ast. // Get the ast.
let ast = server.ast_map.get("file:///test.kcl").unwrap().clone(); let ast = server.ast_map.get("file:///test.kcl").unwrap().clone();
assert_eq!(ast, crate::ast::types::Program::default()); assert_eq!(ast, default_hashed);
// Get the memory. // Get the memory.
let memory = server.memory_map.get("file:///test.kcl").unwrap().clone(); let memory = server.memory_map.get("file:///test.kcl").unwrap().clone();
assert_eq!(memory, ProgramMemory::default()); assert_eq!(memory, ProgramMemory::default());
@ -2835,9 +2838,12 @@ async fn serial_test_kcl_lsp_cant_execute_set() {
let units = server.executor_ctx().await.clone().unwrap().settings.units; let units = server.executor_ctx().await.clone().unwrap().settings.units;
assert_eq!(units, crate::settings::types::UnitLength::Mm); assert_eq!(units, crate::settings::types::UnitLength::Mm);
let mut default_hashed = crate::ast::types::Program::default();
default_hashed.compute_digest();
// Get the ast. // Get the ast.
let ast = server.ast_map.get("file:///test.kcl").unwrap().clone(); let ast = server.ast_map.get("file:///test.kcl").unwrap().clone();
assert!(ast != crate::ast::types::Program::default()); assert!(ast != default_hashed);
// Get the memory. // Get the memory.
let memory = server.memory_map.get("file:///test.kcl").unwrap().clone(); let memory = server.memory_map.get("file:///test.kcl").unwrap().clone();
// Now it should be the default memory. // Now it should be the default memory.

View File

@ -234,6 +234,9 @@ pub struct AppSettings {
/// This setting only applies to the web app. And is temporary until we have Linux support. /// This setting only applies to the web app. And is temporary until we have Linux support.
#[serde(default, alias = "dismissWebBanner", skip_serializing_if = "is_default")] #[serde(default, alias = "dismissWebBanner", skip_serializing_if = "is_default")]
pub dismiss_web_banner: bool, pub dismiss_web_banner: bool,
/// When the user is idle, and this is true, the stream will be torn down.
#[serde(default, alias = "streamIdleMode", skip_serializing_if = "is_default")]
stream_idle_mode: bool,
} }
// TODO: When we remove backwards compatibility with the old settings file, we can remove this. // TODO: When we remove backwards compatibility with the old settings file, we can remove this.
@ -651,6 +654,7 @@ textWrapping = true
theme_color: None, theme_color: None,
dismiss_web_banner: false, dismiss_web_banner: false,
enable_ssao: None, enable_ssao: None,
stream_idle_mode: false,
}, },
modeling: ModelingSettings { modeling: ModelingSettings {
base_unit: UnitLength::In, base_unit: UnitLength::In,
@ -710,6 +714,7 @@ includeSettings = false
theme_color: None, theme_color: None,
dismiss_web_banner: false, dismiss_web_banner: false,
enable_ssao: None, enable_ssao: None,
stream_idle_mode: false,
}, },
modeling: ModelingSettings { modeling: ModelingSettings {
base_unit: UnitLength::Yd, base_unit: UnitLength::Yd,
@ -774,6 +779,7 @@ defaultProjectName = "projects-$nnn"
theme_color: None, theme_color: None,
dismiss_web_banner: false, dismiss_web_banner: false,
enable_ssao: None, enable_ssao: None,
stream_idle_mode: false,
}, },
modeling: ModelingSettings { modeling: ModelingSettings {
base_unit: UnitLength::Yd, base_unit: UnitLength::Yd,
@ -850,6 +856,7 @@ projectDirectory = "/Users/macinatormax/Documents/kittycad-modeling-projects""#;
theme_color: None, theme_color: None,
dismiss_web_banner: false, dismiss_web_banner: false,
enable_ssao: None, enable_ssao: None,
stream_idle_mode: false,
}, },
modeling: ModelingSettings { modeling: ModelingSettings {
base_unit: UnitLength::Mm, base_unit: UnitLength::Mm,

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