Compare commits

...

40 Commits

Author SHA1 Message Date
33822b5a19 Bump to v0.6.1 (#465) 2023-09-13 06:33:11 -04:00
a2a4daebe3 fix move cmd order (#462) 2023-09-13 07:42:42 +00:00
a17ede50bd Build endpoint for download page on website (#451) 2023-09-13 03:03:13 +00:00
2d452f80d1 ts-rs changes (#450)
* initial changes

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

* updates

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

* fix tests

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

* fixes

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

* updates

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

* bust cache

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

* add dumb shit

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

* Revert "add dumb shit"

This reverts commit 638e9cf72f75e1ad08fb6b22d2a7b143ab7e06e5.

* Revert "bust cache"

This reverts commit fd6f53ba0757d635190aa82d4b055a83755f3027.

* fix

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2023-09-13 11:10:27 +10:00
cf39c08428 Bump to v0.6.0 (#442) 2023-09-12 18:48:12 -04:00
2f25564fcc Fix PathError on Linux builds (#441)
* Path Error on Linux builds
Fixes #438

* Add fallback to homeDir
2023-09-12 18:46:35 -04:00
fd2ed8acbd Sketch on plane WIP (#309)
create default planes wip
2023-09-12 22:36:47 +00:00
5f3e1cfb6c ability to test stuff against a real executor (#448)
* ability to test stuff against a real executor

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

* add another regression test

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

* fix clippy

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

* updates

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

* figure out why hanging

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

* ficxes

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>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2023-09-12 14:59:40 -07:00
ee767afc3f Franknoirot/online indicator (#443)
* Add network health indicator to AppHeader

* Add tests for network detection responsiveness

* Format test file
2023-09-12 14:58:59 -04:00
8071eb6f8a Fix: allow the error from getUser to really be thrown (#437)
We have been catching the error thrown by the getUser fetch,
but since our state machine needs to fire onError we should
actually *not* wrap it in a try/catch statement.

Signed-off-by: Frank Noirot <frank@kittycad.io>
2023-09-12 00:37:54 +00:00
11f789e980 fixes angledLine (#436)
* fixes

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

* tests

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

* fix other tests

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2023-09-11 17:14:41 -07:00
3f82522fe9 dont set globally that we are not in a pipe (#432)
* dont set globally that we are not in a pipe

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

* updates

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

* fix 0-l case as well

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

* updates

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

* cleanup

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

* fix unary expression as well, add test

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

* fix array expressions in functions

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

* more options

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

* updates

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

* u[dates

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>

* add a test

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

* updates

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

* Revert "updates"

This reverts commit 3cf06388db.

* fix tets

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2023-09-11 15:15:37 -07:00
c5cb0e2fd4 Add "Trackpad Friendly" camera control setting inspired by Blender (#431)
* Refactor: rename CADProgram to CameraSystem

* Fix buttonDownInStream always set to 0
This is problematic because the left mouse
button ID is actually 0. If no button is
pressed we should set back to undefined.

* Fix: middle mouse button ID is 1, not 3

* Add "Trackpad Friendly" camera system setting

Signed off by Frank Noirot <frank@kittycad.io>

* Allow camera configs to be lenient on first click
2023-09-11 16:21:23 -04:00
9e2a94fcd9 Bump to v0.5.0 (#430) 2023-09-11 05:16:53 -04:00
8a3e8d331d Change WebRTC metrics to be request/response from the Engine (#410)
* Add in a Metrics request/response handler

Signed-off-by: Paul Tagliamonte <paul@kittycad.io>

* Update @kittycad/lib to 0.0.37

Signed-off-by: Paul Tagliamonte <paul@kittycad.io>

* Fix up type issues

Signed-off-by: Paul Tagliamonte <paul@kittycad.io>

* yarn fmt

* Remove VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS

Signed-off-by: Paul Tagliamonte <paul@kittycad.io>

---------

Signed-off-by: Paul Tagliamonte <paul@kittycad.io>
Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>
2023-09-11 09:04:46 +10:00
1be9b2612c Add menu to code editor, put "Format code" and "Convert to variable" buttons in it (#426)
* Move format code button to menu item
by extending CollapsiblePanel to take an optional
menu React element.

Signed-off-by: Frank Noirot <frank@kittycad.io>

* Style tweaks

* Add shortcuts for format and cmd bar to codemirror

* Move convert to variable into code menu

Signed off by Frank Noirot <frank@kittycad.io>

* Add keyboard shortcut to convert to variable

* Remove convert to variable from toolbar

* Refactor: move TextEditor to its own component

* Set a better convertToVar shortcut

* Style and ergonomic polish for convertToVar modal

* Use named constants for shortcuts 😇

* Try yet another keyboard shortcut

* Fix formatting

* remove isShiftDown from app.tsx

---------

Signed-off-by: Frank Noirot <frank@kittycad.io>
Co-authored-by: Kurt Hutten Irev-Dev <k.hutten@protonmail.ch>
2023-09-09 05:38:36 +00:00
7c9aaeafa2 Guard Promise resolution with a shouldTrace() (#424)
The Promises are created behind a shouldTrace, so they'll be undefined
if you shouldn't be tracing. As a result, we need to guard the resoluton
of the promises.

Thanks @mlfarrell for spotting this in local dev!

Signed-off-by: Paul Tagliamonte <paul@kittycad.io>
2023-09-08 16:40:08 -04:00
46c0078885 try window.location.origin (#423)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2023-09-08 13:02:02 -07:00
87ebf3b1d6 bump kittycad lib (#421)
updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2023-09-08 11:20:59 -07:00
45238f8196 Bump KCL lib to 0.1.25 (#420) 2023-09-08 12:24:25 -05:00
44f3a12fbe Remove .vscode dir (#419) 2023-09-08 11:47:34 -05:00
61acada2a0 Bump kittycad from 0.2.23 to 0.2.25 in /src/wasm-lib (#418)
* Bump kittycad from 0.2.23 to 0.2.25 in /src/wasm-lib

Bumps [kittycad](https://github.com/KittyCAD/kittycad.rs) from 0.2.23 to 0.2.25.
- [Release notes](https://github.com/KittyCAD/kittycad.rs/releases)
- [Commits](https://github.com/KittyCAD/kittycad.rs/compare/v0.2.23...v0.2.25)

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

Signed-off-by: dependabot[bot] <support@github.com>

* Handle metricsrequest

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Adam Chalmers <adam.s.chalmers@gmail.com>
2023-09-08 11:45:50 -05:00
c68fbbd89d Make camera mouse controls configurable (#411)
* Add camera handler config object
Using definitions of camera controls of various
CAD incumbents from Onshape's onboarding.

Signed-off-by: Frank Noirot <frank@kittycad.io>

* Refactor: alphabetize settingsMachine

* Refactor: add descriptions to MouseGuards

* Refactor: don't destructure mousemove event

* Refactor: button down in stream as int, not bool

* Honor current camera control settings

* Add cameraControls to settings

* Refactor: alphabetize settings imports

* Refactor: break out cameraControls to own file

* Fix camera control setting in command bar

* Fix formatting on generated type file

* dont use "as" in App.tsx guards

Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>

* Don't use "as" in Stream.tsx

Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>

* Don't use "as" in settingsMachine.ts

Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>

* Add type to cadPrograms

Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>

* Kurt review

---------

Signed-off-by: Frank Noirot <frank@kittycad.io>
Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>
2023-09-08 10:13:35 -04:00
97a0b6a543 fix persist (#416) 2023-09-08 17:52:50 +10:00
3bccae492d clear old engine ids (#415)
* clear old engine ids

* animate re-execute and deffer execution for user typing
2023-09-08 17:50:37 +10:00
0120a89d9c Make empty defaultProjectName value impossible (#409)
* Set named const as default project name

* Refactor: move base units into settings machine

Signed off by Frank Noirot <frank@kittycad.io>

* Reset default when creating with blank name

Signed off by Frank Noirot <frank@kittycad.io>

* Make it impossible to set empty defaultProjectName

Signed off by Frank Noirot <frank@kittycad.io>

* Make it impossible to assign empty strings
to defaultProjectName

Signed off by Frank Noirot <frank@kittycad.io>
2023-09-07 21:48:51 -04:00
3da6fc3b7e Bump to v0.4.0 (#413) 2023-09-07 20:04:04 -04:00
34dd15ead7 Add macOS universal release builds (#408)
* Add macOS universal release builds
Fixes #397

* Change macos to universal-apple-darwin

* Upload universal-apple-darwin/release

* Clean up

* Clean up
2023-09-07 19:19:58 -04:00
b3d441e9d6 start of fuzzing (#405)
* fuzzing

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

* more tests

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

* unicode

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

* more fixes

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

* fix clippy

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>

* fixes

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

* one more

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

* one more

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

* last one

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2023-09-06 21:56:10 -07:00
4b3dc3756c remove noisy log (#407) 2023-09-07 04:11:46 +00:00
10027b98b5 implement rename (#396)
* updates

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

* rename function

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

* start of rename

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>

* updates

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

* updates

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

* cache rust

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

* fix gnarly bug

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

* updates

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

* fixes

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

* fucking tabs

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>
2023-09-06 19:34:47 -07:00
da17dad63b remove rust tests in ci, already covered in build (#403) 2023-09-07 01:49:11 +00:00
fba6c422a8 Fix LSP tooltip cutoff, style hover/autocomplete tooltips, add text wrapping setting (#404)
* Fix: allow tooltips to overflow code pane
while keeping the same vertical and horizontal
scroll behavior that we've had.

Signed-off-by: Frank Noirot <frank@kittycad.io>

* Style tooltips in light and dark mode

* Fix: properly display autocomplete info as HTML
We were parsing it from md to html, but displaying
the parsed html as a string in the info box.

Signed-off-by: Frank Noirot <frank@kittycad.io>

* Fix z-index of command bar to show over code panel

* Let user set text wrapping in editor

* Style hover tooltips

* Fix failing tests
by not including line wrapping plugin in test mode

---------

Signed-off-by: Frank Noirot <frank@kittycad.io>
2023-09-06 21:27:30 -04:00
0b4b93932d tweak prettierignore (#401) 2023-09-07 00:31:36 +00:00
f42900ec46 break up ci (#400) 2023-09-07 10:15:38 +10:00
eeca624ba6 bump kitty lib (#398) 2023-09-06 22:45:43 +00:00
84d08bad16 Allow people to set format options (#389)
* better naming

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

* fixes

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

* up[dates

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

* bump version

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

* fix tests

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

* updates

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

* whitespace

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2023-09-06 10:36:03 -07:00
1181f33e9d Bump to v0.3.2 (#392) 2023-09-06 09:04:06 -04:00
797e200d08 Make sure extra artifacts don't get uploaded on release (#390)
* Make sure extra artifacts don't get uploaded on release
Fixes #388

* Clean up
2023-09-06 07:52:58 -04:00
d2f231066b Franknoirot/debug rerendering (#387)
* Refactor: let Stream handle control drag status

* Fix: prevent app rerender on mouse move
By not setting the highlight range unless things
actually need to change. Setting the highlight range
still causes an app rerender, though.

Signed-off-by: Frank Noirot <frank@kittycad.io>

---------

Signed-off-by: Frank Noirot <frank@kittycad.io>
2023-09-06 15:32:53 +10:00
78 changed files with 6643 additions and 1449 deletions

View File

@ -1,7 +1,6 @@
VITE_KC_API_WS_MODELING_URL=wss://api.dev.kittycad.io/ws/modeling/commands
VITE_KC_API_BASE_URL=https://api.dev.kittycad.io
VITE_KC_SITE_BASE_URL=https://dev.kittycad.io
VITE_KC_API_WS_MODELING_URL=wss://api.kittycad.io/ws/modeling/commands
VITE_KC_API_BASE_URL=https://api.kittycad.io
VITE_KC_SITE_BASE_URL=https://kittycad.io
VITE_KC_SKIP_AUTH=false
VITE_KC_CONNECTION_TIMEOUT_MS=5000
VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS=0
VITE_KC_CONNECTION_TIMEOUT_MS=15000
VITE_KC_SENTRY_DSN=

View File

@ -3,5 +3,4 @@ VITE_KC_API_BASE_URL=https://api.kittycad.io
VITE_KC_SITE_BASE_URL=https://kittycad.io
VITE_KC_SKIP_AUTH=false
VITE_KC_CONNECTION_TIMEOUT_MS=15000
VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS=30000
VITE_KC_SENTRY_DSN=https://a814f2f66734989a90367f48feee28ca@o1042111.ingest.sentry.io/4505789425844224

View File

@ -40,6 +40,17 @@ jobs:
- name: Rust Cache
uses: Swatinem/rust-cache@v2.6.1
- name: Install ffmpeg
run: |
sudo apt update
sudo apt install \
ffmpeg \
libavformat-dev \
libavutil-dev \
libclang-dev \
libswscale-dev \
--no-install-recommends
- name: Run clippy
run: |
cd "${{ matrix.dir }}"

View File

@ -41,6 +41,16 @@ jobs:
- uses: taiki-e/install-action@nextest
- name: Rust Cache
uses: Swatinem/rust-cache@v2.6.1
- name: Install ffmpeg
run: |
sudo apt update
sudo apt install \
ffmpeg \
libavformat-dev \
libavutil-dev \
libclang-dev \
libswscale-dev \
--no-install-recommends
- name: cargo test
shell: bash
run: |-

View File

@ -1,4 +1,4 @@
name: CI
name: CI
on:
pull_request:
@ -13,17 +13,31 @@ jobs:
check-format:
runs-on: 'ubuntu-20.04'
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- run: yarn install
- run: yarn fmt-check
check-types:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- run: yarn install
- uses: Swatinem/rust-cache@v2
with:
workspaces: "./src/wasm-lib"
- run: yarn build:wasm
- run: yarn tsc
build-test-web:
runs-on: ubuntu-20.04
@ -36,12 +50,15 @@ jobs:
- uses: actions/setup-node@v3
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- run: yarn install
- run: yarn build:wasm
- uses: Swatinem/rust-cache@v2
with:
workspaces: "./src/wasm-lib"
- run: yarn tsc
- run: yarn build:wasm
- run: yarn simpleserver:ci
@ -49,14 +66,12 @@ jobs:
- run: yarn test:cov
- run: yarn test:rust
- id: export_version
run: echo "version=`cat package.json | jq -r '.version'`" >> "$GITHUB_OUTPUT"
build-apps:
needs: [check-format, build-test-web]
needs: [check-format, build-test-web, check-types]
runs-on: ${{ matrix.os }}
strategy:
matrix:
@ -87,6 +102,10 @@ jobs:
with:
workspaces: './src-tauri -> target'
- uses: Swatinem/rust-cache@v2
with:
workspaces: "./src/wasm-lib"
- name: wasm prep
shell: bash
run: |
@ -110,15 +129,22 @@ jobs:
- name: Fix format
run: yarn fmt
- name: install apple silicon target mac
if: matrix.os == 'macos-latest'
run: |
rustup target add aarch64-apple-darwin
- name: Build the app for the current platform (no upload)
uses: tauri-apps/tauri-action@v0
env:
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
with:
args: ${{ matrix.os == 'macos-latest' && '--target universal-apple-darwin' || '' }}
- uses: actions/upload-artifact@v3
with:
path: src-tauri/target/release/bundle/*/*
path: ${{ matrix.os == 'macos-latest' && 'src-tauri/target/universal-apple-darwin/release/bundle/*/*' || 'src-tauri/target/release/bundle/*/*' }}
publish-apps-release:
@ -127,20 +153,23 @@ jobs:
needs: [build-test-web, build-apps]
env:
VERSION_NO_V: ${{ needs.build-test-web.outputs.version }}
PUB_DATE: ${{ github.event.release.created_at }}
NOTES: ${{ github.event.release.body }}
steps:
- uses: actions/download-artifact@v3
- name: Generate the update static endpoint
run: |
ls -l artifact
ls -l artifact/*
ls -l artifact/*/*itty*
DARWIN_SIG=`cat artifact/macos/*.app.tar.gz.sig`
LINUX_SIG=`cat artifact/appimage/*.AppImage.tar.gz.sig`
WINDOWS_SIG=`cat artifact/nsis/*.nsis.zip.sig`
RELEASE_DIR=https://dl.kittycad.io/releases/modeling-app/v${VERSION_NO_V}
jq --null-input \
--arg version "v${VERSION_NO_V}" \
--arg pub_date "${PUB_DATE}" \
--arg notes "${NOTES}" \
--arg darwin_sig "$DARWIN_SIG" \
--arg darwin_url "$RELEASE_DIR/macos/KittyCAD%20Modeling.app.tar.gz" \
--arg linux_sig "$LINUX_SIG" \
@ -149,11 +178,17 @@ jobs:
--arg windows_url "$RELEASE_DIR/nsis/KittyCAD%20Modeling_${VERSION_NO_V}_x64-setup.nsis.zip" \
'{
"version": $version,
"pub_date": $pub_date,
"notes": $notes,
"platforms": {
"darwin-x86_64": {
"signature": $darwin_sig,
"url": $darwin_url
},
"darwin-aarch64": {
"signature": $darwin_sig,
"url": $darwin_url
},
"linux-x86_64": {
"signature": $linux_sig,
"url": $linux_url
@ -166,6 +201,34 @@ jobs:
}' > last_update.json
cat last_update.json
- name: Generate the download static endpoint
run: |
RELEASE_DIR=https://dl.kittycad.io/releases/modeling-app/v${VERSION_NO_V}
jq --null-input \
--arg version "v${VERSION_NO_V}" \
--arg pub_date "${PUB_DATE}" \
--arg notes "${NOTES}" \
--arg darwin_url "$RELEASE_DIR/dmg/KittyCAD%20Modeling_${VERSION_NO_V}_universal.dmg" \
--arg linux_url "$RELEASE_DIR/appimage/kittycad-modeling_${VERSION_NO_V}_amd64.AppImage" \
--arg windows_url "$RELEASE_DIR/msi/KittyCAD%20Modeling_${VERSION_NO_V}_x64_en-US.msi.zip" \
'{
"version": $version,
"pub_date": $pub_date,
"notes": $notes,
"platforms": {
"dmg-universal": {
"url": $darwin_url
},
"appimage-x86_64": {
"url": $linux_url
},
"msi-x86_64": {
"url": $windows_url
}
}
}' > last_download.json
cat last_download.json
- name: Authenticate to Google Cloud
uses: 'google-github-actions/auth@v1.1.1'
with:
@ -175,22 +238,28 @@ jobs:
uses: google-github-actions/setup-gcloud@v1.1.1
with:
project_id: kittycadapi
- name: Upload release files to public bucket
uses: google-github-actions/upload-cloud-storage@v1.0.3
with:
path: artifact
glob: '*/*'
glob: '*/*itty*'
parent: false
destination: dl.kittycad.io/releases/modeling-app/v${{ env.VERSION_NO_V }}
destination: dl.kittycad.io/releases/modeling-app/v${{ env.VERSION_NO_V }}
- name: Upload update endpoint to public bucket
uses: google-github-actions/upload-cloud-storage@v1.0.3
with:
path: last_update.json
destination: dl.kittycad.io/releases/modeling-app
- name: Upload download endpoint to public bucket
uses: google-github-actions/upload-cloud-storage@v1.0.3
with:
path: last_download.json
destination: dl.kittycad.io/releases/modeling-app
- name: Upload release files to Github
uses: softprops/action-gh-release@v1
with:
files: artifact/*/*
files: artifact/*/*itty*

View File

@ -5,3 +5,5 @@ coverage
# Ignore Rust projects:
*.rs
target
src/wasm-lib/pkg
src/wasm-lib/kcl/bindings

View File

@ -86,3 +86,24 @@ The PR may serve as a place to discuss the human-readable changelog and extra QA
3. Create a new release and tag pointing to the bump version commit using semantic versioning `v{x}.{y}.{z}`
4. A new Action kicks in at https://github.com/KittyCAD/modeling-app/actions, uploading artifacts to the release
## Fuzzing the parser
Make sure you install cargo fuzz:
```bash
$ cargo install cargo-fuzz
```
```bash
$ cd src/wasm-lib/kcl
# list the fuzz targets
$ cargo fuzz list
# run the parser fuzzer
$ cargo +nightly fuzz run parser
```
For more information on fuzzing you can check out
[this guide](https://rust-fuzz.github.io/book/cargo-fuzz.html).

View File

@ -1,6 +1,6 @@
{
"name": "untitled-app",
"version": "0.3.1",
"version": "0.6.1",
"private": true,
"dependencies": {
"@codemirror/autocomplete": "^6.9.0",
@ -10,7 +10,7 @@
"@fortawesome/react-fontawesome": "^0.2.0",
"@headlessui/react": "^1.7.13",
"@headlessui/tailwindcss": "^0.2.0",
"@kittycad/lib": "^0.0.35",
"@kittycad/lib": "^0.0.37",
"@lezer/javascript": "^1.4.7",
"@open-rpc/client-js": "^1.8.1",
"@react-hook/resize-observer": "^1.2.6",
@ -70,7 +70,7 @@
"fmt": "prettier --write ./src",
"fmt-check": "prettier --check ./src",
"build:wasm": "yarn wasm-prep && (cd src/wasm-lib && wasm-pack build --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt && yarn remove-importmeta",
"remove-importmeta": "sed -i 's/import.meta.url//g' \"./src/wasm-lib/pkg/wasm_lib.js\"; sed -i '' 's/import.meta.url//g' \"./src/wasm-lib/pkg/wasm_lib.js\" || echo \"sed for both mac and linux\"",
"remove-importmeta": "sed -i 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\"; sed -i '' 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\" || echo \"sed for both mac and linux\"",
"wasm-prep": "rm -rf src/wasm-lib/pkg && mkdir src/wasm-lib/pkg && rm -rf src/wasm-lib/kcl/bindings",
"lint": "eslint --fix src",
"bump-jsons": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json && echo \"$(jq --arg v \"$VERSION\" '.package.version=$v' src-tauri/tauri.conf.json --indent 2)\" > src-tauri/tauri.conf.json"

View File

@ -8,7 +8,7 @@
},
"package": {
"productName": "kittycad-modeling",
"version": "0.3.1"
"version": "0.6.1"
},
"tauri": {
"allowlist": {

View File

@ -2,7 +2,6 @@ import {
useRef,
useEffect,
useLayoutEffect,
useMemo,
useCallback,
MouseEventHandler,
} from 'react'
@ -10,30 +9,20 @@ import { DebugPanel } from './components/DebugPanel'
import { v4 as uuidv4 } from 'uuid'
import { asyncParser } from './lang/abstractSyntaxTree'
import { _executor } from './lang/executor'
import CodeMirror from '@uiw/react-codemirror'
import { linter, lintGutter } from '@codemirror/lint'
import { ViewUpdate, EditorView } from '@codemirror/view'
import {
lineHighlightField,
addLineHighlight,
} from './editor/highlightextension'
import { PaneType, Selections, useStore } from './useStore'
import Server from './editor/lsp/server'
import Client from './editor/lsp/client'
import { PaneType, useStore } from './useStore'
import { Logs, KCLErrors } from './components/Logs'
import { CollapsiblePanel } from './components/CollapsiblePanel'
import { MemoryPanel } from './components/MemoryPanel'
import { useHotKeyListener } from './hooks/useHotKeyListener'
import { Stream } from './components/Stream'
import ModalContainer from 'react-modal-promise'
import { FromServer, IntoServer } from './editor/lsp/codec'
import {
EngineCommand,
EngineCommandManager,
} from './lang/std/engineConnection'
import { isOverlap, throttle } from './lib/utils'
import { throttle } from './lib/utils'
import { AppHeader } from './components/AppHeader'
import { KCLError, kclErrToDiagnostic } from './lang/errors'
import { KCLError } from './lang/errors'
import { Resizable } from 're-resizable'
import {
faCode,
@ -41,96 +30,77 @@ import {
faSquareRootVariable,
} from '@fortawesome/free-solid-svg-icons'
import { useHotkeys } from 'react-hotkeys-hook'
import { TEST } from './env'
import { getNormalisedCoordinates } from './lib/utils'
import { Themes, getSystemTheme } from './lib/theme'
import { isTauri } from './lib/isTauri'
import { useLoaderData, useParams } from 'react-router-dom'
import { writeTextFile } from '@tauri-apps/api/fs'
import { PROJECT_ENTRYPOINT } from './lib/tauriFS'
import { useLoaderData } from 'react-router-dom'
import { IndexLoaderData } from './Router'
import { toast } from 'react-hot-toast'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { onboardingPaths } from 'routes/Onboarding'
import { LanguageServerClient } from 'editor/lsp'
import kclLanguage from 'editor/lsp/language'
import { cameraMouseDragGuards } from 'lib/cameraControls'
import { CameraDragInteractionType_type } from '@kittycad/lib/dist/types/src/models'
import { CodeMenu } from 'components/CodeMenu'
import { TextEditor } from 'components/TextEditor'
import { Themes, getSystemTheme } from 'lib/theme'
export function App() {
const { code: loadedCode, project } = useLoaderData() as IndexLoaderData
const pathParams = useParams()
const streamRef = useRef<HTMLDivElement>(null)
useHotKeyListener()
const {
editorView,
setEditorView,
setSelectionRanges,
selectionRanges,
addLog,
addKCLError,
code,
setCode,
setAst,
setError,
setProgramMemory,
resetLogs,
resetKCLErrors,
selectionRangeTypeMap,
setArtifactMap,
engineCommandManager,
setEngineCommandManager,
highlightRange,
setHighlightRange,
setCursor2,
sourceRangeMap,
setMediaStream,
setIsStreamReady,
isStreamReady,
isLSPServerReady,
setIsLSPServerReady,
isMouseDownInStream,
formatCode,
buttonDownInStream,
openPanes,
setOpenPanes,
didDragInStream,
setDidDragInStream,
setStreamDimensions,
streamDimensions,
setIsExecuting,
defferedCode,
guiMode,
} = useStore((s) => ({
editorView: s.editorView,
setEditorView: s.setEditorView,
setSelectionRanges: s.setSelectionRanges,
selectionRanges: s.selectionRanges,
setGuiMode: s.setGuiMode,
guiMode: s.guiMode,
addLog: s.addLog,
code: s.code,
defferedCode: s.defferedCode,
setCode: s.setCode,
setAst: s.setAst,
setError: s.setError,
setProgramMemory: s.setProgramMemory,
resetLogs: s.resetLogs,
resetKCLErrors: s.resetKCLErrors,
selectionRangeTypeMap: s.selectionRangeTypeMap,
setArtifactMap: s.setArtifactNSourceRangeMaps,
engineCommandManager: s.engineCommandManager,
setEngineCommandManager: s.setEngineCommandManager,
highlightRange: s.highlightRange,
setHighlightRange: s.setHighlightRange,
isShiftDown: s.isShiftDown,
setCursor: s.setCursor,
setCursor2: s.setCursor2,
sourceRangeMap: s.sourceRangeMap,
setMediaStream: s.setMediaStream,
isStreamReady: s.isStreamReady,
setIsStreamReady: s.setIsStreamReady,
isLSPServerReady: s.isLSPServerReady,
setIsLSPServerReady: s.setIsLSPServerReady,
isMouseDownInStream: s.isMouseDownInStream,
formatCode: s.formatCode,
buttonDownInStream: s.buttonDownInStream,
addKCLError: s.addKCLError,
openPanes: s.openPanes,
setOpenPanes: s.setOpenPanes,
didDragInStream: s.didDragInStream,
setDidDragInStream: s.setDidDragInStream,
setStreamDimensions: s.setStreamDimensions,
streamDimensions: s.streamDimensions,
setIsExecuting: s.setIsExecuting,
}))
const {
@ -138,7 +108,7 @@ export function App() {
context: { token },
},
settings: {
context: { showDebugPanel, theme, onboardingStatus },
context: { showDebugPanel, onboardingStatus, cameraControls, theme },
},
} = useGlobalStateContext()
@ -179,80 +149,6 @@ export function App() {
}
}, [loadedCode, setCode])
// const onChange = React.useCallback((value: string, viewUpdate: ViewUpdate) => {
const onChange = (value: string, viewUpdate: ViewUpdate) => {
setCode(value)
if (isTauri() && pathParams.id) {
// Save the file to disk
// Note that PROJECT_ENTRYPOINT is hardcoded until we support multiple files
writeTextFile(pathParams.id + '/' + PROJECT_ENTRYPOINT, value).catch(
(err) => {
// TODO: add Sentry per GH issue #254 (https://github.com/KittyCAD/modeling-app/issues/254)
console.error('error saving file', err)
toast.error('Error saving file, please check file permissions')
}
)
}
if (editorView) {
editorView?.dispatch({ effects: addLineHighlight.of([0, 0]) })
}
} //, []);
const onUpdate = (viewUpdate: ViewUpdate) => {
if (!editorView) {
setEditorView(viewUpdate.view)
}
const ranges = viewUpdate.state.selection.ranges
const isChange =
ranges.length !== selectionRanges.codeBasedSelections.length ||
ranges.some(({ from, to }, i) => {
return (
from !== selectionRanges.codeBasedSelections[i].range[0] ||
to !== selectionRanges.codeBasedSelections[i].range[1]
)
})
if (!isChange) return
const codeBasedSelections: Selections['codeBasedSelections'] = ranges.map(
({ from, to }) => {
if (selectionRangeTypeMap[to]) {
return {
type: selectionRangeTypeMap[to],
range: [from, to],
}
}
return {
type: 'default',
range: [from, to],
}
}
)
const idBasedSelections = codeBasedSelections
.map(({ type, range }) => {
const hasOverlap = Object.entries(sourceRangeMap).filter(
([_, sourceRange]) => {
return isOverlap(sourceRange, range)
}
)
if (hasOverlap.length) {
return {
type,
id: hasOverlap[0][0],
}
}
})
.filter(Boolean) as any
engineCommandManager?.cusorsSelected({
otherSelections: [],
idBasedSelections,
})
setSelectionRanges({
otherSelections: [],
codeBasedSelections,
})
}
const streamWidth = streamRef?.current?.offsetWidth
const streamHeight = streamRef?.current?.offsetHeight
@ -286,57 +182,77 @@ export function App() {
let unsubFn: any[] = []
const asyncWrap = async () => {
try {
if (!code) {
setAst(null)
if (!defferedCode) {
setAst({
start: 0,
end: 0,
body: [],
nonCodeMeta: {
noneCodeNodes: {},
start: null,
},
})
setProgramMemory({ root: {}, return: null })
engineCommandManager.endSession()
engineCommandManager.startNewSession()
return
}
const _ast = await asyncParser(code)
const _ast = await asyncParser(defferedCode)
setAst(_ast)
resetLogs()
resetKCLErrors()
engineCommandManager.endSession()
engineCommandManager.startNewSession()
setIsExecuting(true)
const programMemory = await _executor(
_ast,
{
root: {
_0: {
type: 'userVal',
type: 'UserVal',
value: 0,
__meta: [],
},
_90: {
type: 'userVal',
type: 'UserVal',
value: 90,
__meta: [],
},
_180: {
type: 'userVal',
type: 'UserVal',
value: 180,
__meta: [],
},
_270: {
type: 'userVal',
type: 'UserVal',
value: 270,
__meta: [],
},
},
return: null,
},
engineCommandManager
)
const { artifactMap, sourceRangeMap } =
await engineCommandManager.waitForAllCommands()
setIsExecuting(false)
if (programMemory !== undefined) {
setProgramMemory(programMemory)
}
setArtifactMap({ artifactMap, sourceRangeMap })
const unSubHover = engineCommandManager.subscribeToUnreliable({
event: 'highlight_set_entity',
callback: ({ data }) => {
if (!data?.entity_id) {
setHighlightRange([0, 0])
} else {
if (data?.entity_id) {
const sourceRange = sourceRangeMap[data.entity_id]
setHighlightRange(sourceRange)
} else if (
!highlightRange ||
(highlightRange[0] !== 0 && highlightRange[1] !== 0)
) {
setHighlightRange([0, 0])
}
},
})
@ -352,12 +268,10 @@ export function App() {
},
})
unsubFn.push(unSubHover, unSubClick)
if (programMemory !== undefined) {
setProgramMemory(programMemory)
}
setError()
} catch (e: any) {
setIsExecuting(false)
if (e instanceof KCLError) {
addKCLError(e)
} else {
@ -371,36 +285,73 @@ export function App() {
return () => {
unsubFn.forEach((fn) => fn())
}
}, [code, isStreamReady, engineCommandManager])
}, [defferedCode, isStreamReady, engineCommandManager])
const debounceSocketSend = throttle<EngineCommand>((message) => {
engineCommandManager?.sendSceneCommand(message)
}, 16)
const handleMouseMove: MouseEventHandler<HTMLDivElement> = ({
clientX,
clientY,
ctrlKey,
shiftKey,
currentTarget,
nativeEvent,
}) => {
nativeEvent.preventDefault()
if (isMouseDownInStream) {
setDidDragInStream(true)
}
const handleMouseMove: MouseEventHandler<HTMLDivElement> = (e) => {
e.nativeEvent.preventDefault()
const { x, y } = getNormalisedCoordinates({
clientX,
clientY,
el: currentTarget,
clientX: e.clientX,
clientY: e.clientY,
el: e.currentTarget,
...streamDimensions,
})
const interaction = ctrlKey ? 'zoom' : shiftKey ? 'pan' : 'rotate'
const newCmdId = uuidv4()
if (buttonDownInStream === undefined) {
if (
guiMode.mode === 'sketch' &&
guiMode.sketchMode === ('sketch_line' as any)
) {
debounceSocketSend({
type: 'modeling_cmd_req',
cmd_id: newCmdId,
cmd: {
type: 'mouse_move',
window: { x, y },
},
})
} else {
debounceSocketSend({
type: 'modeling_cmd_req',
cmd: {
type: 'highlight_set_entity',
selected_at_window: { x, y },
},
cmd_id: newCmdId,
})
}
} else {
if (guiMode.mode === 'sketch' && guiMode.sketchMode === ('move' as any)) {
debounceSocketSend({
type: 'modeling_cmd_req',
cmd_id: newCmdId,
cmd: {
type: 'handle_mouse_drag_move',
window: { x, y },
},
})
return
}
const interactionGuards = cameraMouseDragGuards[cameraControls]
let interaction: CameraDragInteractionType_type
const eWithButton = { ...e, button: buttonDownInStream }
if (interactionGuards.pan.callback(eWithButton)) {
interaction = 'pan'
} else if (interactionGuards.rotate.callback(eWithButton)) {
interaction = 'rotate'
} else if (interactionGuards.zoom.dragCallback(eWithButton)) {
interaction = 'zoom'
} else {
console.log('none')
return
}
if (isMouseDownInStream) {
debounceSocketSend({
type: 'modeling_cmd_req',
cmd: {
@ -410,68 +361,9 @@ export function App() {
},
cmd_id: newCmdId,
})
} else {
debounceSocketSend({
type: 'modeling_cmd_req',
cmd: {
type: 'highlight_set_entity',
selected_at_window: { x, y },
},
cmd_id: newCmdId,
})
}
}
const extraExtensions = useMemo(() => {
if (TEST) return []
return [
lintGutter(),
linter((_view) => {
return kclErrToDiagnostic(useStore.getState().kclErrors)
}),
EditorView.lineWrapping,
]
}, [])
// So this is a bit weird, we need to initialize the lsp server and client.
// But the server happens async so we break this into two parts.
// Below is the client and server promise.
const { lspClient } = useMemo(() => {
const intoServer: IntoServer = new IntoServer()
const fromServer: FromServer = FromServer.create()
const client = new Client(fromServer, intoServer)
if (!TEST) {
Server.initialize(intoServer, fromServer).then((lspServer) => {
lspServer.start()
setIsLSPServerReady(true)
})
}
const lspClient = new LanguageServerClient({ client })
return { lspClient }
}, [setIsLSPServerReady])
// Here we initialize the plugin which will start the client.
// When we have multi-file support the name of the file will be a dep of
// this use memo, as well as the directory structure, which I think is
// a good setup becuase it will restart the client but not the server :)
// We do not want to restart the server, its just wasteful.
const kclLSP = useMemo(() => {
let plugin = null
if (isLSPServerReady && !TEST) {
// Set up the lsp plugin.
const lsp = kclLanguage({
// When we have more than one file, we'll need to change this.
documentUri: `file:///we-just-have-one-file-for-now.kcl`,
workspaceFolders: null,
client: lspClient,
})
plugin = lsp
}
return plugin
}, [lspClient, isLSPServerReady])
return (
<div
className="h-screen overflow-hidden relative flex flex-col cursor-pointer select-none"
@ -482,7 +374,7 @@ export function App() {
className={
'transition-opacity transition-duration-75 ' +
paneOpacity +
(isMouseDownInStream ? ' pointer-events-none' : '')
(buttonDownInStream ? ' pointer-events-none' : '')
}
project={project}
enableMenu={true}
@ -491,17 +383,17 @@ export function App() {
<Resizable
className={
'h-full flex flex-col flex-1 z-10 my-5 ml-5 pr-1 transition-opacity transition-duration-75 ' +
(isMouseDownInStream || onboardingStatus === 'camera'
(buttonDownInStream || onboardingStatus === 'camera'
? ' pointer-events-none '
: ' ') +
paneOpacity
}
defaultSize={{
width: '400px',
width: '550px',
height: 'auto',
}}
minWidth={200}
maxWidth={600}
maxWidth={800}
minHeight={'auto'}
maxHeight={'auto'}
handleClasses={{
@ -513,36 +405,11 @@ export function App() {
<CollapsiblePanel
title="Code"
icon={faCode}
className="open:!mb-2 overflow-x-hidden"
className="open:!mb-2"
open={openPanes.includes('code')}
menu={<CodeMenu />}
>
<div className="px-2 py-1">
<button
// disabled={!shouldFormat}
onClick={formatCode}
// className={`${!shouldFormat && 'text-gray-300'}`}
>
format
</button>
</div>
<div
id="code-mirror-override"
className="overflow-x-hidden h-full"
>
<CodeMirror
className="h-full overflow-hidden-x"
value={code}
extensions={
kclLSP
? [kclLSP, lineHighlightField, ...extraExtensions]
: [lineHighlightField, ...extraExtensions]
}
onChange={onChange}
onUpdate={onUpdate}
theme={editorTheme}
onCreateEditor={(_editorView) => setEditorView(_editorView)}
/>
</div>
<TextEditor theme={editorTheme} />
</CollapsiblePanel>
<section className="flex flex-col">
<MemoryPanel
@ -573,7 +440,7 @@ export function App() {
className={
'transition-opacity transition-duration-75 ' +
paneOpacity +
(isMouseDownInStream ? ' pointer-events-none' : '')
(buttonDownInStream ? ' pointer-events-none' : '')
}
open={openPanes.includes('debug')}
/>

View File

@ -1,4 +1,4 @@
import { useStore, toolTips } from './useStore'
import { useStore, toolTips, Selections } from './useStore'
import { extrudeSketch, sketchOnExtrudedFace } from './lang/modifyAst'
import { getNodePathFromSourceRange } from './lang/queryAst'
import { HorzVert } from './components/Toolbar/HorzVert'
@ -8,7 +8,6 @@ import { EqualAngle } from './components/Toolbar/EqualAngle'
import { Intersect } from './components/Toolbar/Intersect'
import { SetHorzVertDistance } from './components/Toolbar/SetHorzVertDistance'
import { SetAngleLength } from './components/Toolbar/setAngleLength'
import { ConvertToVariable } from './components/Toolbar/ConvertVariable'
import { SetAbsDistance } from './components/Toolbar/SetAbsDistance'
import { SetAngleBetween } from './components/Toolbar/SetAngleBetween'
import { Fragment, useEffect } from 'react'
@ -16,6 +15,8 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faSearch, faX } from '@fortawesome/free-solid-svg-icons'
import { Popover, Transition } from '@headlessui/react'
import styles from './Toolbar.module.css'
import { v4 as uuidv4 } from 'uuid'
import { useAppMode } from 'hooks/useAppMode'
export const Toolbar = () => {
const {
@ -25,6 +26,7 @@ export const Toolbar = () => {
ast,
updateAst,
programMemory,
engineCommandManager,
} = useStore((s) => ({
guiMode: s.guiMode,
setGuiMode: s.setGuiMode,
@ -32,7 +34,9 @@ export const Toolbar = () => {
ast: s.ast,
updateAst: s.updateAst,
programMemory: s.programMemory,
engineCommandManager: s.engineCommandManager,
}))
useAppMode()
useEffect(() => {
console.log('guiMode', guiMode)
@ -40,7 +44,7 @@ export const Toolbar = () => {
function ToolbarButtons() {
return (
<>
<span className="overflow-x-auto">
{guiMode.mode === 'default' && (
<button
onClick={() => {
@ -72,9 +76,18 @@ export const Toolbar = () => {
SketchOnFace
</button>
)}
{(guiMode.mode === 'canEditSketch' || false) && (
{guiMode.mode === 'canEditSketch' && (
<button
onClick={() => {
console.log('guiMode.pathId', guiMode.pathId)
engineCommandManager?.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'edit_mode_enter',
target: guiMode.pathId,
},
})
setGuiMode({
mode: 'sketch',
sketchMode: 'sketchEdit',
@ -126,14 +139,23 @@ export const Toolbar = () => {
)}
{guiMode.mode === 'sketch' && (
<button onClick={() => setGuiMode({ mode: 'default' })}>
<button
onClick={() => {
engineCommandManager?.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: { type: 'edit_mode_exit' },
})
setGuiMode({ mode: 'default' })
}}
>
Exit sketch
</button>
)}
{toolTips
.filter(
// (sketchFnName) => !['angledLineThatIntersects'].includes(sketchFnName)
(sketchFnName) => ['line'].includes(sketchFnName)
(sketchFnName) => ['sketch_line', 'move'].includes(sketchFnName)
)
.map((sketchFnName) => {
if (
@ -144,7 +166,18 @@ export const Toolbar = () => {
return (
<button
key={sketchFnName}
onClick={() =>
onClick={() => {
engineCommandManager?.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'set_tool',
tool:
guiMode.sketchMode === sketchFnName
? 'select'
: (sketchFnName as any),
},
})
setGuiMode({
...guiMode,
...(guiMode.sketchMode === sketchFnName
@ -154,17 +187,17 @@ export const Toolbar = () => {
}
: {
sketchMode: sketchFnName,
waitingFirstClick: true,
isTooltip: true,
}),
})
}
}}
>
{sketchFnName}
{guiMode.sketchMode === sketchFnName && '✅'}
</button>
)
})}
<ConvertToVariable />
<HorzVert horOrVert="horizontal" />
<HorzVert horOrVert="vertical" />
<EqualLength />
@ -182,7 +215,7 @@ export const Toolbar = () => {
<Intersect />
<RemoveConstrainingValues />
<SetAngleBetween />
</>
</span>
)
}

View File

@ -4,6 +4,7 @@ import { ProjectWithEntryPointMetadata } from '../Router'
import ProjectSidebarMenu from './ProjectSidebarMenu'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import styles from './AppHeader.module.css'
import { NetworkHealthIndicator } from './NetworkHealthIndicator'
interface AppHeaderProps extends React.PropsWithChildren {
showToolbar?: boolean
@ -43,7 +44,8 @@ export const AppHeader = ({
)}
{/* If there are children, show them, otherwise show User menu */}
{children || (
<div className="ml-auto">
<div className="ml-auto flex items-center gap-1">
<NetworkHealthIndicator />
<UserSidebarMenu user={user} />
</div>
)}

View File

@ -144,7 +144,7 @@ export function useCalc({
try {
const code = `const __result__ = ${value}\nshow(__result__)`
const ast = parser_wasm(code)
const _programMem: any = { root: {} }
const _programMem: any = { root: {}, return: null }
availableVarInfo.variables.forEach(({ key, value }) => {
_programMem.root[key] = { type: 'userVal', value, __meta: [] }
})
@ -198,29 +198,25 @@ export const CreateNewVariable = ({
isNewVariableNameUnique,
setNewVariableName,
shouldCreateVariable,
setShouldCreateVariable,
setShouldCreateVariable = () => {},
showCheckbox = true,
}: {
isNewVariableNameUnique: boolean
newVariableName: string
setNewVariableName: (a: string) => void
shouldCreateVariable: boolean
setShouldCreateVariable: (a: boolean) => void
shouldCreateVariable?: boolean
setShouldCreateVariable?: (a: boolean) => void
showCheckbox?: boolean
}) => {
return (
<>
<label
htmlFor="create-new-variable"
className="block text-sm font-medium text-gray-700 mt-3 font-mono"
>
<label htmlFor="create-new-variable" className="block mt-3 font-mono">
Create new variable
</label>
<div className="mt-1 flex flex-1">
<div className="mt-1 flex gap-2 items-center">
{showCheckbox && (
<input
type="checkbox"
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md font-mono pl-1 flex-shrink"
checked={shouldCreateVariable}
onChange={(e) => {
setShouldCreateVariable(e.target.checked)
@ -232,7 +228,10 @@ export const CreateNewVariable = ({
disabled={!shouldCreateVariable}
name="create-new-variable"
id="create-new-variable"
className={`shadow-sm font-[monospace] focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md font-mono pl-1 flex-shrink-0 ${
autoFocus={true}
autoCapitalize="off"
autoCorrect="off"
className={`font-mono flex-1 sm:text-sm px-2 py-1 rounded-sm bg-chalkboard-10 dark:bg-chalkboard-90 text-chalkboard-90 dark:text-chalkboard-10 ${
!shouldCreateVariable ? 'opacity-50' : ''
}`}
value={newVariableName}

View File

@ -0,0 +1,19 @@
.button {
@apply flex justify-between items-center gap-2 px-2 py-1 text-left border-none rounded-sm;
@apply font-mono text-xs font-bold select-none text-chalkboard-90;
@apply ui-active:bg-liquid-10/50 ui-active:text-liquid-90;
@apply transition-colors ease-out;
}
:global(.dark) .button {
@apply text-chalkboard-30;
@apply ui-active:bg-chalkboard-80 ui-active:text-liquid-10;
}
.button small {
@apply text-chalkboard-60;
}
:global(.dark) .button small {
@apply text-chalkboard-40;
}

View File

@ -0,0 +1,59 @@
import { Menu } from '@headlessui/react'
import { PropsWithChildren } from 'react'
import { faEllipsis } from '@fortawesome/free-solid-svg-icons'
import { ActionIcon } from './ActionIcon'
import { useStore } from 'useStore'
import styles from './CodeMenu.module.css'
import { useConvertToVariable } from 'hooks/useToolbarGuards'
import { editorShortcutMeta } from './TextEditor'
export const CodeMenu = ({ children }: PropsWithChildren) => {
const { formatCode } = useStore((s) => ({
formatCode: s.formatCode,
}))
const { enable: convertToVarEnabled, handleClick: handleConvertToVarClick } =
useConvertToVariable()
return (
<Menu>
<div
className="relative"
onClick={(e) => {
if (e.eventPhase === 3) {
e.stopPropagation()
e.preventDefault()
}
}}
>
<Menu.Button className="p-0 border-none relative">
<ActionIcon
icon={faEllipsis}
bgClassName={
'bg-chalkboard-20 dark:bg-chalkboard-110 hover:bg-liquid-10/50 hover:dark:bg-chalkboard-90 ui-active:bg-chalkboard-80 ui-active:dark:bg-chalkboard-90 rounded'
}
iconClassName={'text-chalkboard-90 dark:text-chalkboard-40'}
/>
</Menu.Button>
<Menu.Items className="absolute right-0 left-auto w-72 flex flex-col gap-1 divide-y divide-chalkboard-20 dark:divide-chalkboard-70 align-stretch px-0 py-1 bg-chalkboard-10 dark:bg-chalkboard-90 rounded-sm shadow-lg border border-solid border-chalkboard-20/50 dark:border-chalkboard-80/50">
<Menu.Item>
<button onClick={() => formatCode()} className={styles.button}>
<span>Format code</span>
<small>{editorShortcutMeta.formatCode.display}</small>
</button>
</Menu.Item>
{convertToVarEnabled && (
<Menu.Item>
<button
onClick={handleConvertToVarClick}
className={styles.button}
>
<span>Convert to Variable</span>
<small>{editorShortcutMeta.convertToVariable.display}</small>
</button>
</Menu.Item>
)}
</Menu.Items>
</div>
</Menu>
)
}

View File

@ -1,5 +1,5 @@
.panel {
@apply relative overflow-auto z-0;
@apply relative z-0;
@apply bg-chalkboard-10/70 backdrop-blur-sm;
}
@ -9,7 +9,7 @@
.header {
@apply sticky top-0 z-10 cursor-pointer;
@apply flex items-center gap-2 w-full p-2;
@apply flex items-center justify-between gap-2 w-full p-2;
@apply font-mono text-xs font-bold select-none text-chalkboard-90;
@apply bg-chalkboard-20;
}

View File

@ -8,6 +8,7 @@ export interface CollapsiblePanelProps
title: string
icon?: IconDefinition
open?: boolean
menu?: React.ReactNode
iconClassNames?: {
bg?: string
icon?: string
@ -18,21 +19,27 @@ export const PanelHeader = ({
title,
icon,
iconClassNames,
menu,
}: CollapsiblePanelProps) => {
return (
<summary className={styles.header}>
<ActionIcon
icon={icon}
bgClassName={
'bg-chalkboard-30 dark:bg-chalkboard-90 group-open:bg-chalkboard-80 rounded ' +
(iconClassNames?.bg || '')
}
iconClassName={
'text-chalkboard-90 dark:text-chalkboard-40 group-open:text-liquid-10 ' +
(iconClassNames?.icon || '')
}
/>
{title}
<div className="flex gap-2 align-center flex-1">
<ActionIcon
icon={icon}
bgClassName={
'bg-chalkboard-30 dark:bg-chalkboard-90 group-open:bg-chalkboard-80 rounded ' +
(iconClassNames?.bg || '')
}
iconClassName={
'text-chalkboard-90 dark:text-chalkboard-40 group-open:text-liquid-10 ' +
(iconClassNames?.icon || '')
}
/>
{title}
</div>
<div className="group-open:opacity-100 opacity-0 group-open:pointer-events-auto pointer-events-none">
{menu}
</div>
</summary>
)
}
@ -43,6 +50,7 @@ export const CollapsiblePanel = ({
children,
className,
iconClassNames,
menu,
...props
}: CollapsiblePanelProps) => {
return (
@ -50,7 +58,12 @@ export const CollapsiblePanel = ({
{...props}
className={styles.panel + ' group ' + (className || '')}
>
<PanelHeader title={title} icon={icon} iconClassNames={iconClassNames} />
<PanelHeader
title={title}
icon={icon}
iconClassNames={iconClassNames}
menu={menu}
/>
{children}
</details>
)

View File

@ -196,7 +196,7 @@ const CommandBar = () => {
setCommandBarOpen(false)
clearState()
}}
className="fixed inset-0 overflow-y-auto p-4 pt-[25vh]"
className="fixed inset-0 z-40 overflow-y-auto p-4 pt-[25vh]"
>
<Transition.Child
enter="duration-100 ease-out"
@ -207,7 +207,7 @@ const CommandBar = () => {
leaveTo="opacity-0"
as={Fragment}
>
<Dialog.Overlay className="fixed z-40 inset-0 bg-chalkboard-10/70 dark:bg-chalkboard-110/50" />
<Dialog.Overlay className="fixed inset-0 bg-chalkboard-10/70 dark:bg-chalkboard-110/50" />
</Transition.Child>
<Transition.Child
enter="duration-100 ease-out"
@ -221,7 +221,7 @@ const CommandBar = () => {
<Combobox
value={selectedCommand}
onChange={handleCommandSelection}
className="rounded relative mx-auto z-40 p-2 bg-chalkboard-10 dark:bg-chalkboard-100 border dark:border-chalkboard-70 max-w-xl w-full shadow-lg"
className="rounded relative mx-auto p-2 bg-chalkboard-10 dark:bg-chalkboard-100 border dark:border-chalkboard-70 max-w-xl w-full shadow-lg"
as="div"
>
<div className="flex gap-2 items-center">

View File

@ -29,6 +29,7 @@ describe('processMemory', () => {
const ast = parser_wasm(code)
const programMemory = await enginelessExecutor(ast, {
root: {},
return: null,
})
const output = processMemory(programMemory)
expect(output.myVar).toEqual(5)

View File

@ -2,7 +2,7 @@ import ReactJson from 'react-json-view'
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
import { useStore } from '../useStore'
import { useMemo } from 'react'
import { ProgramMemory } from '../lang/executor'
import { ProgramMemory, Path, ExtrudeSurface } from '../lang/executor'
import { Themes } from '../lib/theme'
interface MemoryPanelProps extends CollapsiblePanelProps {
@ -49,8 +49,12 @@ export const processMemory = (programMemory: ProgramMemory) => {
Object.keys(programMemory.root).forEach((key) => {
const val = programMemory.root[key]
if (typeof val.value !== 'function') {
if (val.type === 'sketchGroup' || val.type === 'extrudeGroup') {
processedMemory[key] = val.value.map(({ __geoMeta, ...rest }) => {
if (val.type === 'SketchGroup') {
processedMemory[key] = val.value.map(({ __geoMeta, ...rest }: Path) => {
return rest
})
} else if (val.type === 'ExtrudeGroup') {
processedMemory[key] = val.value.map(({ ...rest }: ExtrudeSurface) => {
return rest
})
} else {

View File

@ -0,0 +1,51 @@
import { fireEvent, render, screen } from '@testing-library/react'
import UserSidebarMenu from './UserSidebarMenu'
import { BrowserRouter } from 'react-router-dom'
import { GlobalStateProvider } from './GlobalStateProvider'
import CommandBarProvider from './CommandBar'
import {
NETWORK_CONTENT,
NetworkHealthIndicator,
} from './NetworkHealthIndicator'
function TestWrap({ children }: { children: React.ReactNode }) {
// wrap in router and xState context
return (
<BrowserRouter>
<CommandBarProvider>
<GlobalStateProvider>{children}</GlobalStateProvider>
</CommandBarProvider>
</BrowserRouter>
)
}
describe('NetworkHealthIndicator tests', () => {
test('Renders the network indicator', () => {
render(
<TestWrap>
<NetworkHealthIndicator />
</TestWrap>
)
fireEvent.click(screen.getByTestId('network-toggle'))
expect(screen.getByTestId('network-good')).toHaveTextContent(
NETWORK_CONTENT.good
)
})
test('Responds to network changes', () => {
render(
<TestWrap>
<NetworkHealthIndicator />
</TestWrap>
)
fireEvent.offline(window)
fireEvent.click(screen.getByTestId('network-toggle'))
expect(screen.getByTestId('network-bad')).toHaveTextContent(
NETWORK_CONTENT.bad
)
})
})

View File

@ -0,0 +1,112 @@
import {
faCheck,
faExclamation,
faWifi,
} from '@fortawesome/free-solid-svg-icons'
import { Popover } from '@headlessui/react'
import { useEffect, useState } from 'react'
import { ActionIcon } from './ActionIcon'
export const NETWORK_CONTENT = {
good: 'Network health is good',
bad: 'Network issue',
}
const NETWORK_MESSAGES = {
offline: 'You are offline',
}
export const NetworkHealthIndicator = () => {
const [networkIssues, setNetworkIssues] = useState<string[]>([])
const hasIssues = [...networkIssues.values()].length > 0
useEffect(() => {
const offlineListener = () =>
setNetworkIssues((issues) => {
return [
...issues.filter((issue) => issue !== NETWORK_MESSAGES.offline),
NETWORK_MESSAGES.offline,
]
})
window.addEventListener('offline', offlineListener)
const onlineListener = () =>
setNetworkIssues((issues) => {
return [...issues.filter((issue) => issue !== NETWORK_MESSAGES.offline)]
})
window.addEventListener('online', onlineListener)
return () => {
window.removeEventListener('offline', offlineListener)
window.removeEventListener('online', onlineListener)
}
}, [])
return (
<Popover className="relative">
<Popover.Button
className={
'p-0 border-none relative ' +
(hasIssues
? 'focus-visible:outline-destroy-80'
: 'focus-visible:outline-succeed-80')
}
data-testid="network-toggle"
>
<span className="sr-only">Network Health</span>
<ActionIcon
icon={faWifi}
iconClassName={
hasIssues
? 'text-destroy-80 dark:text-destroy-30'
: 'text-succeed-80 dark:text-succeed-30'
}
bgClassName={
hasIssues
? 'hover:bg-destroy-10/50 hover:dark:bg-destroy-80/50 rounded'
: 'hover:bg-succeed-10/50 hover:dark:bg-succeed-80/50 rounded'
}
/>
</Popover.Button>
<Popover.Panel className="absolute right-0 left-auto top-full mt-1 w-56 flex flex-col gap-1 divide-y divide-chalkboard-20 dark:divide-chalkboard-70 align-stretch py-2 bg-chalkboard-10 dark:bg-chalkboard-90 rounded shadow-lg border border-solid border-chalkboard-20/50 dark:border-chalkboard-80/50 text-sm">
{!hasIssues ? (
<span
className="flex items-center justify-center gap-1 px-4"
data-testid="network-good"
>
<ActionIcon
icon={faCheck}
bgClassName={'bg-succeed-10/50 dark:bg-succeed-80/50 rounded'}
iconClassName={'text-succeed-80 dark:text-succeed-30'}
/>
{NETWORK_CONTENT.good}
</span>
) : (
<ul className="divide-y divide-chalkboard-20 dark:divide-chalkboard-80">
<span
className="font-bold text-xs uppercase text-destroy-60 dark:text-destroy-50 px-4"
data-testid="network-bad"
>
{NETWORK_CONTENT.bad}
{networkIssues.length > 1 ? 's' : ''}
</span>
{networkIssues.map((issue) => (
<li
key={issue}
className="flex items-center gap-1 py-2 my-2 last:mb-0"
>
<ActionIcon
icon={faExclamation}
bgClassName={'bg-destroy-10/50 dark:bg-destroy-80/50 rounded'}
iconClassName={'text-destroy-80 dark:text-destroy-30'}
className="ml-4"
/>
<p className="flex-1 mr-4">{issue}</p>
</li>
))}
</ul>
)}
</Popover.Panel>
</Popover>
)
}

View File

@ -1,6 +1,9 @@
import { Dialog, Transition } from '@headlessui/react'
import { Fragment } from 'react'
import { useCalc, CreateNewVariable } from './AvailableVarsHelpers'
import { ActionButton } from './ActionButton'
import { faPlus } from '@fortawesome/free-solid-svg-icons'
import { toast } from 'react-hot-toast'
export const SetVarNameModal = ({
isOpen,
@ -19,67 +22,65 @@ export const SetVarNameModal = ({
return (
<Transition appear show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-10" onClose={onReject}>
<Dialog
as="div"
className="fixed inset-0 z-40 overflow-y-auto p-4 pt-[25vh]"
onClose={onReject}
>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
enterFrom="opacity-0 translate-y-4"
enterTo="opacity-100 translate-y-0"
leave="ease-in duration-75"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-black bg-opacity-25" />
<Dialog.Overlay className="fixed inset-0 bg-chalkboard-10/70 dark:bg-chalkboard-110/50" />
</Transition.Child>
<div className="fixed inset-0 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<Dialog.Panel className="rounded relative mx-auto px-4 py-8 bg-chalkboard-10 dark:bg-chalkboard-100 border dark:border-chalkboard-70 max-w-xl w-full shadow-lg">
<form
onSubmit={(e) => {
e.preventDefault()
onResolve({
variableName: newVariableName,
})
toast.success(`Added variable ${newVariableName}`)
}}
>
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all">
<Dialog.Title
as="h3"
className="text-lg font-medium leading-6 text-gray-900 capitalize"
<CreateNewVariable
setNewVariableName={setNewVariableName}
newVariableName={newVariableName}
isNewVariableNameUnique={isNewVariableNameUnique}
shouldCreateVariable={true}
showCheckbox={false}
/>
<div className="mt-8 flex justify-between">
<ActionButton
Element="button"
type="submit"
disabled={!isNewVariableNameUnique}
icon={{ icon: faPlus }}
>
Set {valueName}
</Dialog.Title>
<CreateNewVariable
setNewVariableName={setNewVariableName}
newVariableName={newVariableName}
isNewVariableNameUnique={isNewVariableNameUnique}
shouldCreateVariable={true}
setShouldCreateVariable={() => {}}
/>
<div className="mt-4">
<button
type="button"
disabled={!isNewVariableNameUnique}
className={`inline-flex justify-center rounded-md border border-transparent bg-blue-100 px-4 py-2 text-sm font-medium text-blue-900 hover:bg-blue-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 ${
!isNewVariableNameUnique
? 'opacity-50 cursor-not-allowed'
: ''
}`}
onClick={() =>
onResolve({
variableName: newVariableName,
})
}
>
Add variable
</button>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
Add variable
</ActionButton>
<ActionButton Element="button" onClick={() => onReject(false)}>
Cancel
</ActionButton>
</div>
</form>
</Dialog.Panel>
</Transition.Child>
</Dialog>
</Transition>
)

View File

@ -7,29 +7,52 @@ import {
} from 'react'
import { v4 as uuidv4 } from 'uuid'
import { useStore } from '../useStore'
import { getNormalisedCoordinates } from '../lib/utils'
import { getNormalisedCoordinates, roundOff } from '../lib/utils'
import Loading from './Loading'
import { cameraMouseDragGuards } from 'lib/cameraControls'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { CameraDragInteractionType_type } from '@kittycad/lib/dist/types/src/models'
import { Models } from '@kittycad/lib'
import { addStartSketch } from 'lang/modifyAst'
import { addNewSketchLn } from 'lang/std/sketch'
export const Stream = ({ className = '' }) => {
const [isLoading, setIsLoading] = useState(true)
const [clickCoords, setClickCoords] = useState<{ x: number; y: number }>()
const videoRef = useRef<HTMLVideoElement>(null)
const {
mediaStream,
engineCommandManager,
setIsMouseDownInStream,
setButtonDownInStream,
didDragInStream,
setDidDragInStream,
streamDimensions,
isExecuting,
guiMode,
ast,
updateAst,
setGuiMode,
programMemory,
} = useStore((s) => ({
mediaStream: s.mediaStream,
engineCommandManager: s.engineCommandManager,
isMouseDownInStream: s.isMouseDownInStream,
setIsMouseDownInStream: s.setIsMouseDownInStream,
setButtonDownInStream: s.setButtonDownInStream,
fileId: s.fileId,
didDragInStream: s.didDragInStream,
setDidDragInStream: s.setDidDragInStream,
streamDimensions: s.streamDimensions,
isExecuting: s.isExecuting,
guiMode: s.guiMode,
ast: s.ast,
updateAst: s.updateAst,
setGuiMode: s.setGuiMode,
programMemory: s.programMemory,
}))
const {
settings: {
context: { cameraControls },
},
} = useGlobalStateContext()
useEffect(() => {
if (
@ -42,39 +65,70 @@ export const Stream = ({ className = '' }) => {
videoRef.current.srcObject = mediaStream
}, [mediaStream, engineCommandManager])
const handleMouseDown: MouseEventHandler<HTMLVideoElement> = ({
clientX,
clientY,
ctrlKey,
}) => {
const handleMouseDown: MouseEventHandler<HTMLVideoElement> = (e) => {
if (!videoRef.current) return
const { x, y } = getNormalisedCoordinates({
clientX,
clientY,
clientX: e.clientX,
clientY: e.clientY,
el: videoRef.current,
...streamDimensions,
})
console.log('click', x, y)
const newId = uuidv4()
const interaction = ctrlKey ? 'pan' : 'rotate'
const interactionGuards = cameraMouseDragGuards[cameraControls]
let interaction: CameraDragInteractionType_type = 'rotate'
engineCommandManager?.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
type: 'camera_drag_start',
interaction,
window: { x, y },
},
cmd_id: newId,
})
if (
interactionGuards.pan.callback(e) ||
interactionGuards.pan.lenientDragStartButton === e.button
) {
interaction = 'pan'
} else if (
interactionGuards.rotate.callback(e) ||
interactionGuards.rotate.lenientDragStartButton === e.button
) {
interaction = 'rotate'
} else if (
interactionGuards.zoom.dragCallback(e) ||
interactionGuards.zoom.lenientDragStartButton === e.button
) {
interaction = 'zoom'
}
setIsMouseDownInStream(true)
if (guiMode.mode === 'sketch' && guiMode.sketchMode === ('move' as any)) {
engineCommandManager?.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
type: 'handle_mouse_drag_start',
window: { x, y },
},
cmd_id: newId,
})
} else if (
!(
guiMode.mode === 'sketch' &&
guiMode.sketchMode === ('sketch_line' as any)
)
) {
engineCommandManager?.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
type: 'camera_drag_start',
interaction,
window: { x, y },
},
cmd_id: newId,
})
}
setButtonDownInStream(e.button)
setClickCoords({ x, y })
}
const handleScroll: WheelEventHandler<HTMLVideoElement> = (e) => {
e.preventDefault()
if (!cameraMouseDragGuards[cameraControls].zoom.scrollCallback(e)) return
engineCommandManager?.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
@ -91,6 +145,7 @@ export const Stream = ({ className = '' }) => {
ctrlKey,
}) => {
if (!videoRef.current) return
setButtonDownInStream(undefined)
const { x, y } = getNormalisedCoordinates({
clientX,
clientY,
@ -101,7 +156,7 @@ export const Stream = ({ className = '' }) => {
const newCmdId = uuidv4()
const interaction = ctrlKey ? 'pan' : 'rotate'
engineCommandManager?.sendSceneCommand({
const command: Models['WebSocketRequest_type'] = {
type: 'modeling_cmd_req',
cmd: {
type: 'camera_drag_end',
@ -109,9 +164,8 @@ export const Stream = ({ className = '' }) => {
window: { x, y },
},
cmd_id: newCmdId,
})
}
setIsMouseDownInStream(false)
if (!didDragInStream) {
engineCommandManager?.sendSceneCommand({
type: 'modeling_cmd_req',
@ -123,7 +177,109 @@ export const Stream = ({ className = '' }) => {
cmd_id: uuidv4(),
})
}
if (!didDragInStream && guiMode.mode === 'default') {
command.cmd = {
type: 'select_with_point',
selection_type: 'add',
selected_at_window: { x, y },
}
} else if (
(!didDragInStream &&
guiMode.mode === 'sketch' &&
['move', 'select'].includes(guiMode.sketchMode)) ||
(guiMode.mode === 'sketch' &&
guiMode.sketchMode === ('sketch_line' as any))
) {
command.cmd = {
type: 'mouse_click',
window: { x, y },
}
} else if (
guiMode.mode === 'sketch' &&
guiMode.sketchMode === ('move' as any)
) {
command.cmd = {
type: 'handle_mouse_drag_end',
window: { x, y },
}
}
engineCommandManager?.sendSceneCommand(command).then(async ({ data }) => {
if (command.cmd.type !== 'mouse_click' || !ast) return
if (
!(
guiMode.mode === 'sketch' &&
guiMode.sketchMode === ('sketch_line' as any as 'line')
)
)
return
if (data?.data?.entities_modified?.length && guiMode.waitingFirstClick) {
const curve = await engineCommandManager?.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'curve_get_control_points',
curve_id: data?.data?.entities_modified[0],
},
})
const coords: { x: number; y: number }[] =
curve.data.data.control_points
const _addStartSketch = addStartSketch(
ast,
[roundOff(coords[0].x), roundOff(coords[0].y)],
[
roundOff(coords[1].x - coords[0].x),
roundOff(coords[1].y - coords[0].y),
]
)
const _modifiedAst = _addStartSketch.modifiedAst
const _pathToNode = _addStartSketch.pathToNode
setGuiMode({
...guiMode,
pathToNode: _pathToNode,
waitingFirstClick: false,
})
updateAst(_modifiedAst)
} else if (
data?.data?.entities_modified?.length &&
!guiMode.waitingFirstClick
) {
const curve = await engineCommandManager?.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'curve_get_control_points',
curve_id: data?.data?.entities_modified[0],
},
})
const coords: { x: number; y: number }[] =
curve.data.data.control_points
const _modifiedAst = addNewSketchLn({
node: ast,
programMemory,
to: [coords[1].x, coords[1].y],
fnName: 'line',
pathToNode: guiMode.pathToNode,
}).modifiedAst
updateAst(_modifiedAst)
}
})
setDidDragInStream(false)
setClickCoords(undefined)
}
const handleMouseMove: MouseEventHandler<HTMLVideoElement> = (e) => {
if (!clickCoords) return
const delta =
((clickCoords.x - e.clientX) ** 2 + (clickCoords.y - e.clientY) ** 2) **
0.5
if (delta > 5 && !didDragInStream) {
setDidDragInStream(true)
}
}
return (
@ -139,7 +295,9 @@ export const Stream = ({ className = '' }) => {
onContextMenuCapture={(e) => e.preventDefault()}
onWheel={handleScroll}
onPlay={() => setIsLoading(false)}
className="w-full h-full"
onMouseMoveCapture={handleMouseMove}
className={`w-full h-full ${isExecuting && 'blur-md'}`}
style={{ transitionDuration: '200ms', transitionProperty: 'filter' }}
/>
{isLoading && (
<div className="text-center absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">

View File

@ -0,0 +1,265 @@
import ReactCodeMirror, {
Extension,
ViewUpdate,
keymap,
} from '@uiw/react-codemirror'
import { FromServer, IntoServer } from 'editor/lsp/codec'
import Server from '../editor/lsp/server'
import Client from '../editor/lsp/client'
import { TEST } from 'env'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { useConvertToVariable } from 'hooks/useToolbarGuards'
import { Themes } from 'lib/theme'
import { useMemo } from 'react'
import { linter, lintGutter } from '@codemirror/lint'
import { Selections, useStore } from 'useStore'
import { LanguageServerClient } from 'editor/lsp'
import kclLanguage from 'editor/lsp/language'
import { isTauri } from 'lib/isTauri'
import { useParams } from 'react-router-dom'
import { writeTextFile } from '@tauri-apps/api/fs'
import { PROJECT_ENTRYPOINT } from 'lib/tauriFS'
import { toast } from 'react-hot-toast'
import {
EditorView,
addLineHighlight,
lineHighlightField,
} from 'editor/highlightextension'
import { isOverlap } from 'lib/utils'
import { kclErrToDiagnostic } from 'lang/errors'
import { CSSRuleObject } from 'tailwindcss/types/config'
export const editorShortcutMeta = {
formatCode: {
codeMirror: 'Alt-Shift-f',
display: 'Alt + Shift + F',
},
convertToVariable: {
codeMirror: 'Ctrl-Shift-c',
display: 'Ctrl + Shift + C',
},
}
export const TextEditor = ({
theme,
}: {
theme: Themes.Light | Themes.Dark
}) => {
const pathParams = useParams()
const {
code,
defferedSetCode,
editorView,
engineCommandManager,
formatCode,
isLSPServerReady,
selectionRanges,
selectionRangeTypeMap,
setEditorView,
setIsLSPServerReady,
setSelectionRanges,
sourceRangeMap,
} = useStore((s) => ({
code: s.code,
defferedSetCode: s.defferedSetCode,
editorView: s.editorView,
engineCommandManager: s.engineCommandManager,
formatCode: s.formatCode,
isLSPServerReady: s.isLSPServerReady,
selectionRanges: s.selectionRanges,
selectionRangeTypeMap: s.selectionRangeTypeMap,
setEditorView: s.setEditorView,
setIsLSPServerReady: s.setIsLSPServerReady,
setSelectionRanges: s.setSelectionRanges,
sourceRangeMap: s.sourceRangeMap,
}))
const {
settings: {
context: { textWrapping },
},
} = useGlobalStateContext()
const { setCommandBarOpen } = useCommandsContext()
const { enable: convertEnabled, handleClick: convertCallback } =
useConvertToVariable()
// So this is a bit weird, we need to initialize the lsp server and client.
// But the server happens async so we break this into two parts.
// Below is the client and server promise.
const { lspClient } = useMemo(() => {
const intoServer: IntoServer = new IntoServer()
const fromServer: FromServer = FromServer.create()
const client = new Client(fromServer, intoServer)
if (!TEST) {
Server.initialize(intoServer, fromServer).then((lspServer) => {
lspServer.start()
setIsLSPServerReady(true)
})
}
const lspClient = new LanguageServerClient({ client })
return { lspClient }
}, [setIsLSPServerReady])
// Here we initialize the plugin which will start the client.
// When we have multi-file support the name of the file will be a dep of
// this use memo, as well as the directory structure, which I think is
// a good setup becuase it will restart the client but not the server :)
// We do not want to restart the server, its just wasteful.
const kclLSP = useMemo(() => {
let plugin = null
if (isLSPServerReady && !TEST) {
// Set up the lsp plugin.
const lsp = kclLanguage({
// When we have more than one file, we'll need to change this.
documentUri: `file:///we-just-have-one-file-for-now.kcl`,
workspaceFolders: null,
client: lspClient,
})
plugin = lsp
}
return plugin
}, [lspClient, isLSPServerReady])
// const onChange = React.useCallback((value: string, viewUpdate: ViewUpdate) => {
const onChange = (value: string, viewUpdate: ViewUpdate) => {
defferedSetCode(value)
if (isTauri() && pathParams.id) {
// Save the file to disk
// Note that PROJECT_ENTRYPOINT is hardcoded until we support multiple files
writeTextFile(pathParams.id + '/' + PROJECT_ENTRYPOINT, value).catch(
(err) => {
// TODO: add Sentry per GH issue #254 (https://github.com/KittyCAD/modeling-app/issues/254)
console.error('error saving file', err)
toast.error('Error saving file, please check file permissions')
}
)
}
if (editorView) {
editorView?.dispatch({ effects: addLineHighlight.of([0, 0]) })
}
} //, []);
const onUpdate = (viewUpdate: ViewUpdate) => {
if (!editorView) {
setEditorView(viewUpdate.view)
}
const ranges = viewUpdate.state.selection.ranges
const isChange =
ranges.length !== selectionRanges.codeBasedSelections.length ||
ranges.some(({ from, to }, i) => {
return (
from !== selectionRanges.codeBasedSelections[i].range[0] ||
to !== selectionRanges.codeBasedSelections[i].range[1]
)
})
if (!isChange) return
const codeBasedSelections: Selections['codeBasedSelections'] = ranges.map(
({ from, to }) => {
if (selectionRangeTypeMap[to]) {
return {
type: selectionRangeTypeMap[to],
range: [from, to],
}
}
return {
type: 'default',
range: [from, to],
}
}
)
const idBasedSelections = codeBasedSelections
.map(({ type, range }) => {
const hasOverlap = Object.entries(sourceRangeMap).filter(
([_, sourceRange]) => {
return isOverlap(sourceRange, range)
}
)
if (hasOverlap.length) {
return {
type,
id: hasOverlap[0][0],
}
}
})
.filter(Boolean) as any
engineCommandManager?.cusorsSelected({
otherSelections: [],
idBasedSelections,
})
setSelectionRanges({
otherSelections: [],
codeBasedSelections,
})
}
const editorExtensions = useMemo(() => {
const extensions = [
lineHighlightField,
keymap.of([
{
key: 'Meta-k',
run: () => {
setCommandBarOpen(true)
return false
},
},
{
key: editorShortcutMeta.formatCode.codeMirror,
run: () => {
formatCode()
return true
},
},
{
key: editorShortcutMeta.convertToVariable.codeMirror,
run: () => {
if (convertEnabled) {
convertCallback()
return true
}
return false
},
},
]),
] as Extension[]
if (kclLSP) extensions.push(kclLSP)
// These extensions have proven to mess with vitest
if (!TEST) {
extensions.push(
lintGutter(),
linter((_view) => {
return kclErrToDiagnostic(useStore.getState().kclErrors)
})
)
if (textWrapping === 'On') extensions.push(EditorView.lineWrapping)
}
return extensions
}, [kclLSP, textWrapping])
return (
<div
id="code-mirror-override"
className="full-height-subtract"
style={{ '--height-subtract': '4.25rem' } as CSSRuleObject}
>
<ReactCodeMirror
className="h-full"
value={code}
extensions={editorExtensions}
onChange={onChange}
onUpdate={onUpdate}
theme={theme}
onCreateEditor={(_editorView) => setEditorView(_editorView)}
/>
</div>
)
}

View File

@ -1,61 +0,0 @@
import { useState, useEffect } from 'react'
import { create } from 'react-modal-promise'
import { useStore } from '../../useStore'
import { isNodeSafeToReplace } from '../../lang/queryAst'
import { SetVarNameModal } from '../SetVarNameModal'
import { moveValueIntoNewVariable } from '../../lang/modifyAst'
const getModalInfo = create(SetVarNameModal as any)
export const ConvertToVariable = () => {
const { guiMode, selectionRanges, ast, programMemory, updateAst } = useStore(
(s) => ({
guiMode: s.guiMode,
ast: s.ast,
updateAst: s.updateAst,
selectionRanges: s.selectionRanges,
programMemory: s.programMemory,
})
)
const [enableAngLen, setEnableAngLen] = useState(false)
useEffect(() => {
if (!ast) return
const { isSafe, value } = isNodeSafeToReplace(
ast,
selectionRanges.codeBasedSelections?.[0]?.range || []
)
const canReplace = isSafe && value.type !== 'Identifier'
const isOnlyOneSelection = selectionRanges.codeBasedSelections.length === 1
const _enableHorz = canReplace && isOnlyOneSelection
setEnableAngLen(_enableHorz)
}, [guiMode, selectionRanges])
return (
<button
onClick={async () => {
if (!ast) return
try {
const { variableName } = await getModalInfo({
valueName: 'var',
} as any)
const { modifiedAst: _modifiedAst } = moveValueIntoNewVariable(
ast,
programMemory,
selectionRanges.codeBasedSelections[0].range,
variableName
)
updateAst(_modifiedAst)
} catch (e) {
console.log('e', e)
}
}}
disabled={!enableAngLen}
>
ConvertToVariable
</button>
)
}

View File

@ -208,7 +208,13 @@ export class LanguageServerPlugin implements PluginValue {
filterText: filterText ?? label,
}
if (documentation) {
completion.info = formatContents(documentation)
completion.info = () => {
const htmlString = formatContents(documentation)
const htmlNode = document.createElement('div')
htmlNode.style.display = 'contents'
htmlNode.innerHTML = htmlString
return { dom: htmlNode }
}
}
return completion

View File

@ -8,8 +8,6 @@ export const VITE_KC_API_WS_MODELING_URL = import.meta.env
.VITE_KC_API_WS_MODELING_URL
export const VITE_KC_API_BASE_URL = import.meta.env.VITE_KC_API_BASE_URL
export const VITE_KC_SITE_BASE_URL = import.meta.env.VITE_KC_SITE_BASE_URL
export const VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS = import.meta.env
.VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS
export const VITE_KC_CONNECTION_TIMEOUT_MS = import.meta.env
.VITE_KC_CONNECTION_TIMEOUT_MS
export const VITE_KC_SENTRY_DSN = import.meta.env.VITE_KC_SENTRY_DSN

243
src/hooks/useAppMode.ts Normal file
View File

@ -0,0 +1,243 @@
// needed somewhere to dump this logic,
// Once we have xState this should be removed
import { useStore, Selections } from 'useStore'
import { useEffect, useState } from 'react'
import { v4 as uuidv4 } from 'uuid'
import { ArtifactMap, EngineCommandManager } from 'lang/std/engineConnection'
import { Models } from '@kittycad/lib/dist/types/src'
import { isReducedMotion } from 'lang/util'
import { isOverlap } from 'lib/utils'
interface DefaultPlanes {
xy: string
yz: string
xz: string
}
export function useAppMode() {
const {
guiMode,
setGuiMode,
selectionRanges,
engineCommandManager,
selectionRangeTypeMap,
} = useStore((s) => ({
guiMode: s.guiMode,
setGuiMode: s.setGuiMode,
selectionRanges: s.selectionRanges,
engineCommandManager: s.engineCommandManager,
selectionRangeTypeMap: s.selectionRangeTypeMap,
}))
const [defaultPlanes, setDefaultPlanes] = useState<DefaultPlanes | null>(null)
useEffect(() => {
if (
guiMode.mode === 'sketch' &&
guiMode.sketchMode === 'selectFace' &&
engineCommandManager
) {
if (!defaultPlanes) {
const xy = createPlane(engineCommandManager, {
x_axis: { x: 1, y: 0, z: 0 },
y_axis: { x: 0, y: 1, z: 0 },
color: { r: 0.7, g: 0.28, b: 0.28, a: 0.4 },
})
const yz = createPlane(engineCommandManager, {
x_axis: { x: 0, y: 1, z: 0 },
y_axis: { x: 0, y: 0, z: 1 },
color: { r: 0.28, g: 0.7, b: 0.28, a: 0.4 },
})
const xz = createPlane(engineCommandManager, {
x_axis: { x: 1, y: 0, z: 0 },
y_axis: { x: 0, y: 0, z: 1 },
color: { r: 0.28, g: 0.28, b: 0.7, a: 0.4 },
})
setDefaultPlanes({ xy, yz, xz })
} else {
hideDefaultPlanes(engineCommandManager, defaultPlanes)
}
}
if (guiMode.mode !== 'sketch' && defaultPlanes) {
Object.values(defaultPlanes).forEach((planeId) => {
engineCommandManager?.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'object_visible',
object_id: planeId,
hidden: true,
},
})
})
} else if (guiMode.mode === 'default') {
const pathId =
engineCommandManager &&
isCursorInSketchCommandRange(
engineCommandManager.artifactMap,
selectionRanges
)
if (pathId) {
setGuiMode({
mode: 'canEditSketch',
rotation: [0, 0, 0, 1],
position: [0, 0, 0],
pathToNode: [],
pathId,
})
}
} else if (guiMode.mode === 'canEditSketch') {
if (
!engineCommandManager ||
!isCursorInSketchCommandRange(
engineCommandManager.artifactMap,
selectionRanges
)
) {
setGuiMode({
mode: 'default',
})
}
}
}, [
guiMode,
guiMode.mode,
engineCommandManager,
selectionRanges,
selectionRangeTypeMap,
])
useEffect(() => {
const unSub = engineCommandManager?.subscribeTo({
event: 'select_with_point',
callback: async ({ data }) => {
if (!data.entity_id) return
if (!defaultPlanes) return
if (!Object.values(defaultPlanes || {}).includes(data.entity_id)) {
// user clicked something else in the scene
return
}
const sketchModeResponse = await engineCommandManager?.sendSceneCommand(
{
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'sketch_mode_enable',
plane_id: data.entity_id,
ortho: true,
animated: !isReducedMotion(),
},
}
)
hideDefaultPlanes(engineCommandManager, defaultPlanes)
const sketchUuid = uuidv4()
const proms: any[] = []
proms.push(
engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: sketchUuid,
cmd: {
type: 'start_path',
},
})
)
proms.push(
engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'edit_mode_enter',
target: sketchUuid,
},
})
)
const res = await Promise.all(proms)
console.log('res', res)
setGuiMode({
mode: 'sketch',
sketchMode: 'sketchEdit',
rotation: [0, 0, 0, 1],
position: [0, 0, 0],
pathToNode: [],
})
console.log('sketchModeResponse', sketchModeResponse)
},
})
return unSub
}, [engineCommandManager, defaultPlanes])
}
function createPlane(
engineCommandManager: EngineCommandManager,
{
x_axis,
y_axis,
color,
}: {
x_axis: Models['Point3d_type']
y_axis: Models['Point3d_type']
color: Models['Color_type']
}
) {
const planeId = uuidv4()
engineCommandManager?.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
type: 'make_plane',
size: 60,
origin: { x: 0, y: 0, z: 0 },
x_axis,
y_axis,
clobber: false,
},
cmd_id: planeId,
})
engineCommandManager?.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
type: 'plane_set_color',
plane_id: planeId,
color,
},
cmd_id: uuidv4(),
})
return planeId
}
function hideDefaultPlanes(
engineCommandManager: EngineCommandManager,
defaultPlanes: DefaultPlanes
) {
Object.values(defaultPlanes).forEach((planeId) => {
engineCommandManager?.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'object_visible',
object_id: planeId,
hidden: true,
},
})
})
}
function isCursorInSketchCommandRange(
artifactMap: ArtifactMap,
selectionRanges: Selections
): string | false {
const overlapingEntries = Object.entries(artifactMap || {}).filter(
([id, artifact]) =>
selectionRanges.codeBasedSelections.some(
(selection) =>
Array.isArray(selection.range) &&
Array.isArray(artifact.range) &&
isOverlap(selection.range, artifact.range) &&
(artifact.commandType === 'start_path' ||
artifact.commandType === 'extend_path' ||
'close_path')
)
)
return overlapingEntries.length === 1 && overlapingEntries[0][1].parentId
? overlapingEntries[0][1].parentId
: false
}

View File

@ -0,0 +1,56 @@
import { SetVarNameModal } from 'components/SetVarNameModal'
import { moveValueIntoNewVariable } from 'lang/modifyAst'
import { isNodeSafeToReplace } from 'lang/queryAst'
import { useEffect, useState } from 'react'
import { create } from 'react-modal-promise'
import { useStore } from 'useStore'
const getModalInfo = create(SetVarNameModal as any)
export function useConvertToVariable() {
const { guiMode, selectionRanges, ast, programMemory, updateAst } = useStore(
(s) => ({
guiMode: s.guiMode,
ast: s.ast,
updateAst: s.updateAst,
selectionRanges: s.selectionRanges,
programMemory: s.programMemory,
})
)
const [enable, setEnabled] = useState(false)
useEffect(() => {
if (!ast) return
const { isSafe, value } = isNodeSafeToReplace(
ast,
selectionRanges.codeBasedSelections?.[0]?.range || []
)
const canReplace = isSafe && value.type !== 'Identifier'
const isOnlyOneSelection = selectionRanges.codeBasedSelections.length === 1
const _enableHorz = canReplace && isOnlyOneSelection
setEnabled(_enableHorz)
}, [guiMode, selectionRanges])
const handleClick = async () => {
if (!ast) return
try {
const { variableName } = await getModalInfo({
valueName: 'var',
} as any)
const { modifiedAst: _modifiedAst } = moveValueIntoNewVariable(
ast,
programMemory,
selectionRanges.codeBasedSelections[0].range,
variableName
)
updateAst(_modifiedAst)
} catch (e) {
console.log('e', e)
}
}
return { enable, handleClick }
}

View File

@ -82,11 +82,22 @@ code {
monospace;
}
.full-height-subtract {
--height-subtract: 2.25rem;
height: 100%;
max-height: calc(100% - var(--height-subtract));
}
#code-mirror-override .cm-editor {
@apply bg-transparent;
@apply h-full bg-transparent;
}
#code-mirror-override .cm-scroller {
@apply h-full;
}
#code-mirror-override .cm-scroller::-webkit-scrollbar {
@apply h-0;
}
#code-mirror-override .cm-activeLine,
@ -137,14 +148,39 @@ code {
}
#code-mirror-override .cm-tooltip {
font-size: 80%;
@apply text-xs shadow-md;
@apply bg-chalkboard-10 text-chalkboard-80;
@apply rounded-sm border-solid border border-chalkboard-40/30 border-l-liquid-10;
}
.dark #code-mirror-override .cm-tooltip {
@apply bg-chalkboard-110 text-chalkboard-40;
@apply border-chalkboard-70/20 border-l-liquid-70;
}
#code-mirror-override .cm-tooltip-hover {
@apply py-1 px-2 w-max max-w-md;
}
#code-mirror-override .cm-tooltip-hover .documentation {
padding: 5;
#code-mirror-override .cm-completionInfo {
@apply px-4 rounded-l-none;
@apply bg-chalkboard-10 text-liquid-90;
@apply border-liquid-40/30;
}
.dark #code-mirror-override .cm-completionInfo {
@apply bg-liquid-120 text-liquid-50;
@apply border-liquid-90/60;
}
#code-mirror-override .cm-tooltip-autocomplete li {
@apply px-2 py-1;
}
#code-mirror-override .cm-tooltip-autocomplete li[aria-selected='true'] {
@apply bg-liquid-10 text-liquid-110;
}
.dark #code-mirror-override .cm-tooltip-autocomplete li[aria-selected='true'] {
@apply bg-liquid-100 text-liquid-20;
}
#code-mirror-override .cm-content {

View File

@ -1564,7 +1564,7 @@ const key = 'c'`
start: code.indexOf('\n// this is a comment'),
end: code.indexOf('const key'),
value: {
type: 'block',
type: 'blockComment',
value: 'this is a comment',
},
}
@ -1602,7 +1602,7 @@ const key = 'c'`
start: 106,
end: 166,
value: {
type: 'block',
type: 'blockComment',
value: 'this is\n a comment\n spanning a few lines',
},
})
@ -1625,7 +1625,7 @@ const key = 'c'`
start: 125,
end: 141,
value: {
type: 'block',
type: 'blockComment',
value: 'a comment',
},
})

View File

@ -21,7 +21,7 @@ show(mySketch001)`
)
expect(shown).toEqual([
{
type: 'sketchGroup',
type: 'SketchGroup',
start: {
to: [0, 0],
from: [0, 0],
@ -77,7 +77,7 @@ show(mySketch001)`
)
expect(shown).toEqual([
{
type: 'extrudeGroup',
type: 'ExtrudeGroup',
id: expect.any(String),
value: [],
height: 2,
@ -117,7 +117,7 @@ show(theExtrude, sk2)`
)
expect(geos).toEqual([
{
type: 'extrudeGroup',
type: 'ExtrudeGroup',
id: expect.any(String),
value: [],
height: 2,
@ -126,7 +126,7 @@ show(theExtrude, sk2)`
__meta: [{ sourceRange: [13, 34] }],
},
{
type: 'extrudeGroup',
type: 'ExtrudeGroup',
id: expect.any(String),
value: [],
height: 2,

View File

@ -1,7 +1,7 @@
import fs from 'node:fs'
import { parser_wasm } from './abstractSyntaxTree'
import { ProgramMemory } from './executor'
import { ProgramMemory, SketchGroup } from './executor'
import { initPromise } from './rust'
import { enginelessExecutor } from '../lib/testHelpers'
import { vi } from 'vitest'
@ -117,10 +117,10 @@ show(mySketch)
// ].join('\n')
// const { root } = await exe(code)
// expect(root.mySk1.value).toHaveLength(3)
// expect(root?.rotated?.type).toBe('sketchGroup')
// expect(root?.rotated?.type).toBe('SketchGroup')
// if (
// root?.mySk1?.type !== 'sketchGroup' ||
// root?.rotated?.type !== 'sketchGroup'
// root?.mySk1?.type !== 'SketchGroup' ||
// root?.rotated?.type !== 'SketchGroup'
// )
// throw new Error('not a sketch group')
// expect(root.mySk1.rotation).toEqual([0, 0, 0, 1])
@ -143,7 +143,7 @@ show(mySketch)
].join('\n')
const { root } = await exe(code)
expect(root.mySk1).toEqual({
type: 'sketchGroup',
type: 'SketchGroup',
start: {
to: [0, 0],
from: [0, 0],
@ -199,7 +199,7 @@ show(mySketch)
// TODO path to node is probably wrong here, zero indexes are not correct
expect(root).toEqual({
three: {
type: 'userVal',
type: 'UserVal',
value: 3,
__meta: [
{
@ -208,7 +208,7 @@ show(mySketch)
],
},
yo: {
type: 'userVal',
type: 'UserVal',
value: [1, '2', 3, 9],
__meta: [
{
@ -225,7 +225,7 @@ show(mySketch)
].join('\n')
const { root } = await exe(code)
expect(root.yo).toEqual({
type: 'userVal',
type: 'UserVal',
value: { aStr: 'str', anum: 2, identifier: 3, binExp: 9 },
__meta: [
{
@ -240,7 +240,7 @@ show(mySketch)
)
const { root } = await exe(code)
expect(root.myVar).toEqual({
type: 'userVal',
type: 'UserVal',
value: '123',
__meta: [
{
@ -338,7 +338,7 @@ describe('testing math operators', () => {
const { root } = await exe(code)
const sketch = root.part001
// result of `-legLen(5, min(3, 999))` should be -4
const yVal = sketch.value?.[0]?.to?.[1]
const yVal = (sketch as SketchGroup).value?.[0]?.to?.[1]
expect(yVal).toBe(-4)
})
it('test that % substitution feeds down CallExp->ArrExp->UnaryExp->CallExp', async () => {
@ -356,8 +356,8 @@ describe('testing math operators', () => {
const { root } = await exe(code)
const sketch = root.part001
// expect -legLen(segLen('seg01', %), myVar) to equal -4 setting the y value back to 0
expect(sketch.value?.[1]?.from).toEqual([3, 4])
expect(sketch.value?.[1]?.to).toEqual([6, 0])
expect((sketch as SketchGroup).value?.[1]?.from).toEqual([3, 4])
expect((sketch as SketchGroup).value?.[1]?.to).toEqual([6, 0])
const removedUnaryExp = code.replace(
`-legLen(segLen('seg01', %), myVar)`,
`legLen(segLen('seg01', %), myVar)`
@ -366,7 +366,9 @@ describe('testing math operators', () => {
const removedUnaryExpRootSketch = removedUnaryExpRoot.part001
// without the minus sign, the y value should be 8
expect(removedUnaryExpRootSketch.value?.[1]?.to).toEqual([6, 8])
expect((removedUnaryExpRootSketch as SketchGroup).value?.[1]?.to).toEqual([
6, 8,
])
})
it('with nested callExpression and binaryExpression', async () => {
const code = 'const myVar = 2 + min(100, -1 + legLen(5, 3))'
@ -397,7 +399,10 @@ show(theExtrude)`
// helpers
async function exe(code: string, programMemory: ProgramMemory = { root: {} }) {
async function exe(
code: string,
programMemory: ProgramMemory = { root: {}, return: null }
) {
const ast = parser_wasm(code)
const result = await enginelessExecutor(ast, programMemory)

View File

@ -5,96 +5,21 @@ import {
SourceRangeMap,
} from './std/engineConnection'
import { ProgramReturn } from '../wasm-lib/kcl/bindings/ProgramReturn'
import { MemoryItem } from '../wasm-lib/kcl/bindings/MemoryItem'
import { execute_wasm } from '../wasm-lib/pkg/wasm_lib'
import { KCLError } from './errors'
import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError'
import { rangeTypeFix } from './abstractSyntaxTree'
export type SourceRange = [number, number]
export type PathToNode = [string | number, string][] // [pathKey, nodeType][]
export type Metadata = {
sourceRange: SourceRange
}
export type Position = [number, number, number]
export type Rotation = [number, number, number, number]
export type { SourceRange } from '../wasm-lib/kcl/bindings/SourceRange'
export type { Position } from '../wasm-lib/kcl/bindings/Position'
export type { Rotation } from '../wasm-lib/kcl/bindings/Rotation'
export type { Path } from '../wasm-lib/kcl/bindings/Path'
export type { SketchGroup } from '../wasm-lib/kcl/bindings/SketchGroup'
export type { MemoryItem } from '../wasm-lib/kcl/bindings/MemoryItem'
export type { ExtrudeSurface } from '../wasm-lib/kcl/bindings/ExtrudeSurface'
interface BasePath {
from: [number, number]
to: [number, number]
name?: string
__geoMeta: {
id: string
sourceRange: SourceRange
}
}
export interface ToPoint extends BasePath {
type: 'toPoint'
}
export interface Base extends BasePath {
type: 'base'
}
export interface HorizontalLineTo extends BasePath {
type: 'horizontalLineTo'
x: number
}
export interface AngledLineTo extends BasePath {
type: 'angledLineTo'
angle: number
x?: number
y?: number
}
interface GeoMeta {
__geoMeta: {
id: string
sourceRange: SourceRange
}
}
export type Path = ToPoint | HorizontalLineTo | AngledLineTo | Base
export interface SketchGroup {
type: 'sketchGroup'
id: string
value: Path[]
start?: Base
position: Position
rotation: Rotation
__meta: Metadata[]
}
interface ExtrudePlane {
type: 'extrudePlane'
position: Position
rotation: Rotation
name?: string
}
export type ExtrudeSurface = GeoMeta &
ExtrudePlane /* | ExtrudeRadius | ExtrudeSpline */
export interface ExtrudeGroup {
type: 'extrudeGroup'
id: string
value: ExtrudeSurface[]
height: number
position: Position
rotation: Rotation
__meta: Metadata[]
}
/** UserVal not produced by one of our internal functions */
export interface UserVal {
type: 'userVal'
value: any
__meta: Metadata[]
}
type MemoryItem = UserVal | SketchGroup | ExtrudeGroup
export type PathToNode = [string | number, string][]
interface Memory {
[key: string]: MemoryItem
@ -102,12 +27,12 @@ interface Memory {
export interface ProgramMemory {
root: Memory
return?: ProgramReturn
return: ProgramReturn | null
}
export const executor = async (
node: Program,
programMemory: ProgramMemory = { root: {} },
programMemory: ProgramMemory = { root: {}, return: null },
engineCommandManager: EngineCommandManager,
// work around while the gemotry is still be stored on the frontend
// will be removed when the stream UI is added.
@ -132,7 +57,7 @@ export const executor = async (
export const _executor = async (
node: Program,
programMemory: ProgramMemory = { root: {} },
programMemory: ProgramMemory = { root: {}, return: null },
engineCommandManager: EngineCommandManager
): Promise<ProgramMemory> => {
try {

View File

@ -114,7 +114,8 @@ describe('Testing addSketchTo', () => {
expect(str).toBe(`const part001 = startSketchAt('default')
|> ry(90, %)
|> line('default', %)
show(part001)`)
show(part001)
`)
})
})
@ -179,7 +180,10 @@ describe('Testing moveValueIntoNewVariable', () => {
return x
}
`
const code = `${fn('def')}${fn('ghi')}${fn('jkl')}${fn('hmm')}
const code = `${fn('def')}${fn('jkl')}${fn('hmm')}
fn ghi = (x) => {
return 2
}
const abc = 3
const identifierGuy = 5
const yo = 5 + 6

View File

@ -28,6 +28,46 @@ import {
createFirstArg,
} from './std/sketch'
export function addStartSketch(
node: Program,
start: [number, number],
end: [number, number]
): { modifiedAst: Program; id: string; pathToNode: PathToNode } {
const _node = { ...node }
const _name = findUniqueName(node, 'part')
const startSketchAt = createCallExpression('startSketchAt', [
createArrayExpression([createLiteral(start[0]), createLiteral(start[1])]),
])
const initialLineTo = createCallExpression('line', [
createArrayExpression([createLiteral(end[0]), createLiteral(end[1])]),
createPipeSubstitution(),
])
const pipeBody = [startSketchAt, initialLineTo]
const variableDeclaration = createVariableDeclaration(
_name,
createPipeExpression(pipeBody)
)
_node.body = [...node.body, variableDeclaration]
let pathToNode: PathToNode = [
['body', ''],
['0', 'index'],
['declarations', 'VariableDeclaration'],
['0', 'index'],
['init', 'VariableDeclarator'],
]
return {
modifiedAst: _node,
id: _name,
pathToNode,
}
}
export function addSketchTo(
node: Program,
axis: 'xy' | 'xz' | 'yz',

View File

@ -11,26 +11,27 @@ describe('recast', () => {
const code = '1 + 2'
const { ast } = code2ast(code)
const recasted = recast(ast)
expect(recasted).toBe(code)
expect(recasted.trim()).toBe(code)
})
it('variable declaration', () => {
const code = 'const myVar = 5'
const { ast } = code2ast(code)
const recasted = recast(ast)
expect(recasted).toBe(code)
expect(recasted.trim()).toBe(code)
})
it("variable declaration that's binary with string", () => {
const code = "const myVar = 5 + 'yo'"
const { ast } = code2ast(code)
const recasted = recast(ast)
expect(recasted).toBe(code)
expect(recasted.trim()).toBe(code)
const codeWithOtherQuotes = 'const myVar = 5 + "yo"'
const { ast: ast2 } = code2ast(codeWithOtherQuotes)
expect(recast(ast2)).toBe(codeWithOtherQuotes)
expect(recast(ast2).trim()).toBe(codeWithOtherQuotes)
})
it('test assigning two variables, the second summing with the first', () => {
const code = `const myVar = 5
const newVar = myVar + 1`
const newVar = myVar + 1
`
const { ast } = code2ast(code)
const recasted = recast(ast)
expect(recasted).toBe(code)
@ -42,11 +43,12 @@ const newVar = myVar + 1`
)
const { ast } = code2ast(code)
const recasted = recast(ast)
expect(recasted).toBe(code.trim())
expect(recasted.trim()).toBe(code.trim())
})
it('test with function call', () => {
const code = `const myVar = "hello"
log(5, myVar)`
log(5, myVar)
`
const { ast } = code2ast(code)
const recasted = recast(ast)
expect(recasted).toBe(code)
@ -61,7 +63,7 @@ log(5, myVar)`
].join('\n')
const { ast } = code2ast(code)
const recasted = recast(ast)
expect(recasted).toBe(code)
expect(recasted.trim()).toBe(code)
})
it('recast sketch declaration', () => {
let code = `const mySketch = startSketchAt([0, 0])
@ -70,10 +72,11 @@ log(5, myVar)`
|> lineTo({ to: [1, 0], tag: "rightPath" }, %)
|> close(%)
show(mySketch)`
show(mySketch)
`
const { ast } = code2ast(code)
const recasted = recast(ast)
expect(recasted).toBe(code.trim())
expect(recasted).toBe(code)
})
it('sketch piped into callExpression', () => {
const code = [
@ -85,7 +88,7 @@ show(mySketch)`
].join('\n')
const { ast } = code2ast(code)
const recasted = recast(ast)
expect(recasted).toBe(code.trim())
expect(recasted.trim()).toBe(code.trim())
})
it('recast BinaryExpression piped into CallExpression', () => {
const code = [
@ -97,37 +100,37 @@ show(mySketch)`
].join('\n')
const { ast } = code2ast(code)
const recasted = recast(ast)
expect(recasted).toBe(code)
expect(recasted.trim()).toBe(code)
})
it('recast nested binary expression', () => {
const code = ['const myVar = 1 + 2 * 5'].join('\n')
const { ast } = code2ast(code)
const recasted = recast(ast)
expect(recasted).toBe(code.trim())
expect(recasted.trim()).toBe(code.trim())
})
it('recast nested binary expression with parans', () => {
const code = ['const myVar = 1 + (1 + 2) * 5'].join('\n')
const { ast } = code2ast(code)
const recasted = recast(ast)
expect(recasted).toBe(code.trim())
expect(recasted.trim()).toBe(code.trim())
})
it('unnecessary paran wrap will be remove', () => {
const code = ['const myVar = 1 + (2 * 5)'].join('\n')
const { ast } = code2ast(code)
const recasted = recast(ast)
expect(recasted).toBe(code.replace('(', '').replace(')', ''))
expect(recasted.trim()).toBe(code.replace('(', '').replace(')', ''))
})
it('complex nested binary expression', () => {
const code = ['1 * ((2 + 3) / 4 + 5)'].join('\n')
const { ast } = code2ast(code)
const recasted = recast(ast)
expect(recasted).toBe(code.trim())
expect(recasted.trim()).toBe(code.trim())
})
it('multiplied paren expressions', () => {
const code = ['3 + (1 + 2) * (3 + 4)'].join('\n')
const { ast } = code2ast(code)
const recasted = recast(ast)
expect(recasted).toBe(code.trim())
expect(recasted.trim()).toBe(code.trim())
})
it('recast array declaration', () => {
const code = ['const three = 3', "const yo = [1, '2', three, 4 + 5]"].join(
@ -135,7 +138,7 @@ show(mySketch)`
)
const { ast } = code2ast(code)
const recasted = recast(ast)
expect(recasted).toBe(code.trim())
expect(recasted.trim()).toBe(code.trim())
})
it('recast long array declaration', () => {
const code = [
@ -150,7 +153,7 @@ show(mySketch)`
].join('\n')
const { ast } = code2ast(code)
const recasted = recast(ast)
expect(recasted).toBe(code.trim())
expect(recasted.trim()).toBe(code.trim())
})
it('recast long object exectution', () => {
const code = `const three = 3
@ -159,26 +162,29 @@ const yo = {
anum: 2,
identifier: three,
binExp: 4 + 5
}`
}
`
const { ast } = code2ast(code)
const recasted = recast(ast)
expect(recasted).toBe(code.trim())
expect(recasted).toBe(code)
})
it('recast short object exectution', () => {
const code = `const yo = { key: 'val' }`
const code = `const yo = { key: 'val' }
`
const { ast } = code2ast(code)
const recasted = recast(ast)
expect(recasted).toBe(code.trim())
expect(recasted).toBe(code)
})
it('recast object execution with member expression', () => {
const code = `const yo = { a: { b: { c: '123' } } }
const key = 'c'
const myVar = yo.a['b'][key]
const key2 = 'b'
const myVar2 = yo['a'][key2].c`
const myVar2 = yo['a'][key2].c
`
const { ast } = code2ast(code)
const recasted = recast(ast)
expect(recasted).toBe(code.trim())
expect(recasted).toBe(code)
})
})
@ -186,7 +192,8 @@ describe('testing recasting with comments and whitespace', () => {
it('code with comments', () => {
const code = `const yo = { a: { b: { c: '123' } } }
// this is a comment
const key = 'c'`
const key = 'c'
`
const { ast } = code2ast(code)
const recasted = recast(ast)
@ -199,7 +206,8 @@ const key = 'c'`
/* this is
a
comment */
const yo = 'bing'`
const yo = 'bing'
`
const { ast } = code2ast(code)
const recasted = recast(ast)
expect(recasted).toBe(code)
@ -209,7 +217,8 @@ const yo = 'bing'`
const yo = { a: { b: { c: '123' } } }
const key = 'c'
// this is also a comment`
// this is also a comment
`
const { ast } = code2ast(code)
const recasted = recast(ast)
expect(recasted).toBe(code)
@ -223,7 +232,8 @@ const key = 'c'
comment */
const key = 'c'
// this is also a comment
}`
}
`
const { ast } = code2ast(code)
const recasted = recast(ast)
expect(recasted).toBe(code)
@ -239,7 +249,7 @@ const key = 'c'
].join('\n')
const { ast } = code2ast(code)
const recasted = recast(ast)
expect(recasted).toBe(code)
expect(recasted.trim()).toBe(code)
})
it('comments sprinkled in all over the place', () => {
const code = `
@ -261,7 +271,8 @@ const mySk1 = startSketchAt([0, 0])
|> rx(45, %)
/*
one more for good measure
*/`
*/
`
const { ast } = code2ast(code)
const recasted = recast(ast)
expect(recasted).toBe(`// comment at start
@ -278,7 +289,8 @@ a comment between pipe expression statements */
// and another with just white space between others below
|> ry(45, %)
|> rx(45, %)
// one more for good measure`)
// one more for good measure
`)
})
})
@ -287,19 +299,19 @@ describe('testing call Expressions in BinaryExpressions and UnaryExpressions', (
const code = 'const myVar = 2 + min(100, legLen(5, 3))'
const { ast } = code2ast(code)
const recasted = recast(ast)
expect(recasted).toBe(code)
expect(recasted.trim()).toBe(code)
})
it('nested callExpression in unaryExpression', () => {
const code = 'const myVar = -min(100, legLen(5, 3))'
const { ast } = code2ast(code)
const recasted = recast(ast)
expect(recasted).toBe(code)
expect(recasted.trim()).toBe(code)
})
it('with unaryExpression in callExpression', () => {
const code = 'const myVar = min(5, -legLen(5, 4))'
const { ast } = code2ast(code)
const recasted = recast(ast)
expect(recasted).toBe(code)
expect(recasted.trim()).toBe(code)
})
it('with unaryExpression in sketch situation', () => {
const code = [
@ -308,7 +320,7 @@ describe('testing call Expressions in BinaryExpressions and UnaryExpressions', (
].join('\n')
const { ast } = code2ast(code)
const recasted = recast(ast)
expect(recasted).toBe(code)
expect(recasted.trim()).toBe(code)
})
})
@ -318,12 +330,13 @@ describe('it recasts wrapped object expressions in pipe bodies with correct inde
|> line({ to: [0.62, 4.15], tag: 'seg01' }, %)
|> line([2.77, -1.24], %)
|> angledLineThatIntersects({
angle: 201,
offset: -1.35,
intersectTag: 'seg01'
angle: 201,
offset: -1.35,
intersectTag: 'seg01'
}, %)
|> line([-0.42, -1.72], %)
show(part001)`
show(part001)
`
const { ast } = code2ast(code)
const recasted = recast(ast)
expect(recasted).toBe(code)
@ -333,7 +346,8 @@ show(part001)`
angle: 201,
offset: -1.35,
intersectTag: 'seg01'
}, %)`
}, %)
`
const { ast } = code2ast(code)
const recasted = recast(ast)
expect(recasted).toBe(code)
@ -342,7 +356,8 @@ show(part001)`
describe('it recasts binary expression using brackets where needed', () => {
it('when there are two minus in a row', () => {
const code = `const part001 = 1 - (def - abc)`
const code = `const part001 = 1 - (def - abc)
`
const recasted = recast(code2ast(code).ast)
expect(recasted).toBe(code)
})

View File

@ -1,20 +1,23 @@
import { SourceRange } from 'lang/executor'
import { Selections } from 'useStore'
import {
VITE_KC_API_WS_MODELING_URL,
VITE_KC_CONNECTION_TIMEOUT_MS,
VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS,
} from 'env'
import { VITE_KC_API_WS_MODELING_URL, VITE_KC_CONNECTION_TIMEOUT_MS } from 'env'
import { Models } from '@kittycad/lib'
import { exportSave } from 'lib/exportSave'
import { v4 as uuidv4 } from 'uuid'
import * as Sentry from '@sentry/react'
interface ResultCommand {
let lastMessage = ''
interface CommandInfo {
commandType: CommandTypes
range: SourceRange
parentId?: string
}
interface ResultCommand extends CommandInfo {
type: 'result'
data: any
}
interface PendingCommand {
interface PendingCommand extends CommandInfo {
type: 'pending'
promise: Promise<any>
resolve: (val: any) => void
@ -34,6 +37,8 @@ interface NewTrackArgs {
type WebSocketResponse = Models['OkWebSocketResponseData_type']
type ClientMetrics = Models['ClientMetrics_type']
// EngineConnection encapsulates the connection(s) to the Engine
// for the EngineCommandManager; namely, the underlying WebSocket
// and WebRTC connections.
@ -53,6 +58,9 @@ export class EngineConnection {
private onClose: (engineConnection: EngineConnection) => void
private onNewTrack: (track: NewTrackArgs) => void
// TODO: actual type is ClientMetrics
private webrtcStatsCollector?: () => Promise<ClientMetrics>
constructor({
url,
token,
@ -188,15 +196,17 @@ export class EngineConnection {
)
}
Promise.all([
handshakeSpan.promise,
iceSpan.promise,
dataChannelSpan.promise,
mediaTrackSpan.promise,
]).then(() => {
console.log('All spans finished, reporting')
webrtcMediaTransaction?.finish()
})
if (this.shouldTrace()) {
Promise.all([
handshakeSpan.promise,
iceSpan.promise,
dataChannelSpan.promise,
mediaTrackSpan.promise,
]).then(() => {
console.log('All spans finished, reporting')
webrtcMediaTransaction?.finish()
})
}
this.onWebsocketOpen(this)
})
@ -297,7 +307,9 @@ export class EngineConnection {
this.pc.addEventListener('connectionstatechange', (event) => {
if (this.pc?.iceConnectionState === 'connected') {
iceSpan.resolve?.()
if (this.shouldTrace()) {
iceSpan.resolve?.()
}
}
})
@ -330,6 +342,17 @@ export class EngineConnection {
})
})
.catch(console.log)
} else if (resp.type === 'metrics_request') {
if (this.webrtcStatsCollector === undefined) {
// TODO: Error message here?
return
}
this.webrtcStatsCollector().then((client_metrics) => {
this.send({
type: 'metrics_response',
metrics: client_metrics,
})
})
}
// TODO(paultag): This ought to be both controllable, as well as something
@ -361,127 +384,58 @@ export class EngineConnection {
})
}
// Set up the background thread to keep an eye on statistical
// information about the WebRTC media stream from the server to
// us. We'll also eventually want more global statistical information,
// but this will give us a baseline.
if (parseInt(VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS) !== 0) {
setInterval(() => {
if (this.pc === undefined) {
return
}
if (!this.shouldTrace()) {
this.webrtcStatsCollector = (): Promise<ClientMetrics> => {
return new Promise((resolve, reject) => {
if (mediaStream.getVideoTracks().length !== 1) {
reject(new Error('too many video tracks to report'))
return
}
// Use the WebRTC Statistics API to collect statistical information
// about the WebRTC connection we're using to report to Sentry.
mediaStream.getVideoTracks().forEach((videoTrack) => {
let trackStats = new Map<string, any>()
this.pc?.getStats(videoTrack).then((videoTrackStats) => {
// Sentry only allows 10 metrics per transaction. We're going
// to have to pick carefully here, eventually send like a prom
// file or something to the peer.
let videoTrack = mediaStream.getVideoTracks()[0]
this.pc?.getStats(videoTrack).then((videoTrackStats) => {
// TODO(paultag): this needs type information from the KittyCAD typescript
// library once it's updated
let client_metrics: ClientMetrics = {
rtc_frames_decoded: 0,
rtc_frames_dropped: 0,
rtc_frames_received: 0,
rtc_frames_per_second: 0,
rtc_freeze_count: 0,
rtc_jitter_sec: 0.0,
rtc_keyframes_decoded: 0,
rtc_total_freezes_duration_sec: 0.0,
}
const transaction = Sentry.startTransaction({
name: 'webrtc-stats',
})
videoTrackStats.forEach((videoTrackReport) => {
if (videoTrackReport.type === 'inbound-rtp') {
// RTC Stream Info
// transaction.setMeasurement(
// 'mediaStreamTrack.framesDecoded',
// videoTrackReport.framesDecoded,
// 'frame'
// )
transaction.setMeasurement(
'rtcFramesDropped',
videoTrackReport.framesDropped,
''
)
// transaction.setMeasurement(
// 'mediaStreamTrack.framesReceived',
// videoTrackReport.framesReceived,
// 'frame'
// )
transaction.setMeasurement(
'rtcFramesPerSecond',
videoTrackReport.framesPerSecond,
'fps'
)
transaction.setMeasurement(
'rtcFreezeCount',
videoTrackReport.freezeCount,
''
)
transaction.setMeasurement(
'rtcJitter',
videoTrackReport.jitter,
'second'
)
// transaction.setMeasurement(
// 'mediaStreamTrack.jitterBufferDelay',
// videoTrackReport.jitterBufferDelay,
// ''
// )
// transaction.setMeasurement(
// 'mediaStreamTrack.jitterBufferEmittedCount',
// videoTrackReport.jitterBufferEmittedCount,
// ''
// )
// transaction.setMeasurement(
// 'mediaStreamTrack.jitterBufferMinimumDelay',
// videoTrackReport.jitterBufferMinimumDelay,
// ''
// )
// transaction.setMeasurement(
// 'mediaStreamTrack.jitterBufferTargetDelay',
// videoTrackReport.jitterBufferTargetDelay,
// ''
// )
transaction.setMeasurement(
'rtcKeyFramesDecoded',
videoTrackReport.keyFramesDecoded,
''
)
transaction.setMeasurement(
'rtcTotalFreezesDuration',
videoTrackReport.totalFreezesDuration,
'second'
)
// transaction.setMeasurement(
// 'mediaStreamTrack.totalInterFrameDelay',
// videoTrackReport.totalInterFrameDelay,
// ''
// )
transaction.setMeasurement(
'rtcTotalPausesDuration',
videoTrackReport.totalPausesDuration,
'second'
)
// transaction.setMeasurement(
// 'mediaStreamTrack.totalProcessingDelay',
// videoTrackReport.totalProcessingDelay,
// 'second'
// )
} else if (videoTrackReport.type === 'transport') {
// // Bytes i/o
// transaction.setMeasurement(
// 'mediaStreamTrack.bytesReceived',
// videoTrackReport.bytesReceived,
// 'byte'
// )
// transaction.setMeasurement(
// 'mediaStreamTrack.bytesSent',
// videoTrackReport.bytesSent,
// 'byte'
// )
}
})
transaction?.finish()
// TODO(paultag): Since we can technically have multiple WebRTC
// video tracks (even if the Server doesn't at the moment), we
// ought to send stats for every video track(?), and add the stream
// ID into it. This raises the cardinality of collected metrics
// when/if we do, but for now, just report the one stream.
videoTrackStats.forEach((videoTrackReport) => {
if (videoTrackReport.type === 'inbound-rtp') {
client_metrics.rtc_frames_decoded =
videoTrackReport.framesDecoded
client_metrics.rtc_frames_dropped =
videoTrackReport.framesDropped
client_metrics.rtc_frames_received =
videoTrackReport.framesReceived
client_metrics.rtc_frames_per_second =
videoTrackReport.framesPerSecond || 0
client_metrics.rtc_freeze_count = videoTrackReport.freezeCount
client_metrics.rtc_jitter_sec = videoTrackReport.jitter
client_metrics.rtc_keyframes_decoded =
videoTrackReport.keyFramesDecoded
client_metrics.rtc_total_freezes_duration_sec =
videoTrackReport.totalFreezesDuration
} else if (videoTrackReport.type === 'transport') {
// videoTrackReport.bytesReceived,
// videoTrackReport.bytesSent,
}
})
resolve(client_metrics)
})
}, VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS)
})
}
this.onNewTrack({
@ -490,10 +444,6 @@ export class EngineConnection {
})
})
// During startup, we'll track the time from `connect` being called
// until the 'done' event fires.
let connectionStarted = new Date()
this.pc.addEventListener('datachannel', (event) => {
this.unreliableDataChannel = event.channel
@ -537,6 +487,7 @@ export class EngineConnection {
this.websocket = undefined
this.pc = undefined
this.unreliableDataChannel = undefined
this.webrtcStatsCollector = undefined
this.onClose(this)
this.ready = false
@ -546,6 +497,8 @@ export class EngineConnection {
export type EngineCommand = Models['WebSocketRequest_type']
type ModelTypes = Models['OkModelingCmdResponse_type']['type']
type CommandTypes = Models['ModelingCmd_type']['type']
type UnreliableResponses = Extract<
Models['OkModelingCmdResponse_type'],
{ type: 'highlight_set_entity' }
@ -687,15 +640,22 @@ export class EngineCommandManager {
const resolve = command.resolve
this.artifactMap[id] = {
type: 'result',
range: command.range,
commandType: command.commandType,
parentId: command.parentId ? command.parentId : undefined,
data: modelingResponse,
}
resolve({
id,
commandType: command.commandType,
range: command.range,
data: modelingResponse,
})
} else {
this.artifactMap[id] = {
type: 'result',
commandType: command?.commandType,
range: command?.range,
data: modelingResponse,
}
}
@ -747,8 +707,29 @@ export class EngineCommandManager {
delete this.unreliableSubscriptions[event][id]
}
endSession() {
// this.websocket?.close()
// socket.off('command')
// TODO: instead of sending a single command with `object_ids: Object.keys(this.artifactMap)`
// we need to loop over them each individualy because if the engine doesn't recognise a single
// id the whole command fails.
Object.entries(this.artifactMap).forEach(([id, artifact]) => {
const artifactTypesToDelete: ArtifactMap[string]['commandType'][] = [
// 'start_path' creates a new scene object for the path, which is why it needs to be deleted,
// however all of the segments in the path are its children so there don't need to be deleted.
// this fact is very opaque in the api and docs (as to what should can be deleted).
// Using an array is the list is likely to grow.
'start_path',
]
if (!artifactTypesToDelete.includes(artifact.commandType)) return
const deletCmd: EngineCommand = {
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'remove_scene_objects',
object_ids: [id],
},
}
this.engineConnection?.send(deletCmd)
})
}
cusorsSelected(selections: {
otherSelections: Selections['otherSelections']
@ -775,6 +756,13 @@ export class EngineCommandManager {
})
}
sendSceneCommand(command: EngineCommand): Promise<any> {
if (
command.type === 'modeling_cmd_req' &&
command.cmd.type !== lastMessage
) {
console.log('sending command', command.cmd.type)
lastMessage = command.cmd.type
}
if (!this.engineConnection?.isReady()) {
console.log('socket not ready')
return Promise.resolve()
@ -782,7 +770,8 @@ export class EngineCommandManager {
if (command.type !== 'modeling_cmd_req') return Promise.resolve()
const cmd = command.cmd
if (
cmd.type === 'camera_drag_move' &&
(cmd.type === 'camera_drag_move' ||
cmd.type === 'handle_mouse_drag_move') &&
this.engineConnection?.unreliableDataChannel
) {
cmd.sequence = this.outSequence
@ -801,11 +790,20 @@ export class EngineCommandManager {
JSON.stringify(command)
)
return Promise.resolve()
} else if (
cmd.type === 'mouse_move' &&
this.engineConnection.unreliableDataChannel
) {
cmd.sequence = this.outSequence
this.outSequence++
this.engineConnection?.unreliableDataChannel?.send(
JSON.stringify(command)
)
return Promise.resolve()
}
console.log('sending command', command)
// since it's not mouse drag or highlighting send over TCP and keep track of the command
this.engineConnection?.send(command)
return this.handlePendingCommand(command.cmd_id)
return this.handlePendingCommand(command.cmd_id, command.cmd)
}
sendModelingCommand({
id,
@ -823,15 +821,35 @@ export class EngineCommandManager {
return Promise.resolve()
}
this.engineConnection?.send(command)
return this.handlePendingCommand(id)
if (typeof command !== 'string' && command.type === 'modeling_cmd_req') {
return this.handlePendingCommand(id, command?.cmd, range)
} else if (typeof command === 'string') {
const parseCommand: EngineCommand = JSON.parse(command)
if (parseCommand.type === 'modeling_cmd_req')
return this.handlePendingCommand(id, parseCommand?.cmd, range)
}
throw 'shouldnt reach here'
}
handlePendingCommand(id: string) {
handlePendingCommand(
id: string,
command: Models['ModelingCmd_type'],
range?: SourceRange
) {
let resolve: (val: any) => void = () => {}
const promise = new Promise((_resolve, reject) => {
resolve = _resolve
})
const getParentId = (): string | undefined => {
if (command.type === 'extend_path') {
return command.path
}
// TODO handle other commands that have a parent
}
this.artifactMap[id] = {
range: range || [0, 0],
type: 'pending',
commandType: command.type,
parentId: getParentId(),
promise,
resolve,
}

View File

@ -101,7 +101,8 @@ describe('testing changeSketchArguments', () => {
|> ${line}
|> lineTo([0.46, -5.82], %)
// |> rx(45, %)
show(mySketch001)`
show(mySketch001)
`
const code = genCode(lineToChange)
const expectedCode = genCode(lineAfterChange)
const ast = parser_wasm(code)
@ -164,7 +165,8 @@ show(mySketch001)`
|> lineTo([-1.59, -1.54], %)
|> lineTo([0.46, -5.82], %)
|> lineTo([2, 3], %)
show(mySketch001)`
show(mySketch001)
`
expect(recast(modifiedAst)).toBe(expectedCode)
})
})
@ -177,7 +179,8 @@ describe('testing addTagForSketchOnFace', () => {
// |> rx(45, %)
|> ${line}
|> lineTo([0.46, -5.82], %)
show(mySketch001)`
show(mySketch001)
`
const code = genCode(originalLine)
const ast = parser_wasm(code)
const programMemory = await enginelessExecutor(ast)

View File

@ -4,6 +4,7 @@ import {
SketchGroup,
SourceRange,
PathToNode,
MemoryItem,
} from '../executor'
import {
Program,
@ -20,7 +21,7 @@ import {
getNodePathFromSourceRange,
} from '../queryAst'
import { GuiModes, toolTips, TooTip } from '../../useStore'
import { splitPathAtPipeExpression } from '../modifyAst'
import { createPipeExpression, splitPathAtPipeExpression } from '../modifyAst'
import { generateUuidFromHashSeed } from '../../lib/uuid'
import { SketchLineHelper, ModifyAstBase, TransformCallback } from './stdTypes'
@ -185,7 +186,7 @@ export const line: SketchLineHelper = {
createCallback,
}) => {
const _node = { ...node }
const { node: pipe } = getNodeFromPath<PipeExpression>(
const { node: pipe } = getNodeFromPath<PipeExpression | CallExpression>(
_node,
pathToNode,
'PipeExpression'
@ -197,12 +198,12 @@ export const line: SketchLineHelper = {
)
const variableName = varDec.id.name
const sketch = previousProgramMemory?.root?.[variableName]
if (sketch.type !== 'sketchGroup') throw new Error('not a sketchGroup')
if (sketch.type !== 'SketchGroup') throw new Error('not a SketchGroup')
const newXVal = createLiteral(roundOff(to[0] - from[0], 2))
const newYVal = createLiteral(roundOff(to[1] - from[1], 2))
if (replaceExisting && createCallback) {
if (replaceExisting && createCallback && pipe.type !== 'CallExpression') {
const { index: callIndex } = splitPathAtPipeExpression(pathToNode)
const { callExp, valueUsedInTransform } = createCallback(
[newXVal, newYVal],
@ -220,7 +221,11 @@ export const line: SketchLineHelper = {
createArrayExpression([newXVal, newYVal]),
createPipeSubstitution(),
])
pipe.body = [...pipe.body, callExp]
if (pipe.type === 'PipeExpression') {
pipe.body = [...pipe.body, callExp]
} else {
varDec.init = createPipeExpression([varDec.init, callExp])
}
return {
modifiedAst: _node,
pathToNode,
@ -238,22 +243,10 @@ export const line: SketchLineHelper = {
createLiteral(roundOff(to[1] - from[1], 2)),
])
if (
callExpression.arguments?.[0].type === 'Literal' &&
callExpression.arguments?.[0].value === 'default'
) {
callExpression.arguments[0] = toArrExp
} else if (callExpression.arguments?.[0].type === 'ObjectExpression') {
if (callExpression.arguments?.[0].type === 'ObjectExpression') {
const toProp = callExpression.arguments?.[0].properties?.find(
({ key }) => key.name === 'to'
)
if (
toProp &&
toProp.value.type === 'Literal' &&
toProp.value.value === 'default'
) {
toProp.value = toArrExp
}
mutateObjExpProp(callExpression.arguments?.[0], toArrExp, 'to')
} else {
mutateArrExp(callExpression.arguments?.[0], toArrExp)
@ -546,7 +539,7 @@ export const angledLineOfXLength: SketchLineHelper = {
)
const variableName = varDec.id.name
const sketch = previousProgramMemory?.root?.[variableName]
if (sketch.type !== 'sketchGroup') throw new Error('not a sketchGroup')
if (sketch.type !== 'SketchGroup') throw new Error('not a SketchGroup')
const angle = createLiteral(roundOff(getAngle(from, to), 0))
const xLength = createLiteral(roundOff(Math.abs(from[0] - to[0]), 2) || 0.1)
const newLine = createCallback
@ -619,7 +612,7 @@ export const angledLineOfYLength: SketchLineHelper = {
)
const variableName = varDec.id.name
const sketch = previousProgramMemory?.root?.[variableName]
if (sketch.type !== 'sketchGroup') throw new Error('not a sketchGroup')
if (sketch.type !== 'SketchGroup') throw new Error('not a SketchGroup')
const angle = createLiteral(roundOff(getAngle(from, to), 0))
const yLength = createLiteral(roundOff(Math.abs(from[1] - to[1]), 2) || 0.1)
@ -876,7 +869,7 @@ export const angledLineThatIntersects: SketchLineHelper = {
const varName = varDec.declarations[0].id.name
const sketchGroup = previousProgramMemory.root[varName] as SketchGroup
const intersectPath = sketchGroup.value.find(
({ name }) => name === intersectTagName
({ name }: Path) => name === intersectTagName
)
let offset = 0
if (intersectPath) {
@ -968,60 +961,14 @@ export function addNewSketchLn({
pathToNode,
'VariableDeclarator'
)
const { node: pipeExp, shallowPath: pipePath } =
getNodeFromPath<PipeExpression>(node, pathToNode, 'PipeExpression')
const maybeStartSketchAt = pipeExp.body.find(
(exp) =>
exp.type === 'CallExpression' &&
exp.callee.name === 'startSketchAt' &&
exp.arguments[0].type === 'Literal' &&
exp.arguments[0].value === 'default'
)
const maybeDefaultLine = pipeExp.body.findIndex(
(exp) =>
exp.type === 'CallExpression' &&
exp.callee.name === 'line' &&
exp.arguments[0].type === 'Literal' &&
exp.arguments[0].value === 'default'
)
const defaultLinePath: PathToNode = [
...pipePath,
['body', ''],
[maybeDefaultLine, ''],
]
const { node: pipeExp, shallowPath: pipePath } = getNodeFromPath<
PipeExpression | CallExpression
>(node, pathToNode, 'PipeExpression')
const variableName = varDec.id.name
const sketch = previousProgramMemory?.root?.[variableName]
if (sketch.type !== 'sketchGroup') throw new Error('not a sketchGroup')
if (sketch.type !== 'SketchGroup') throw new Error('not a SketchGroup')
if (maybeStartSketchAt) {
const startSketchAt = maybeStartSketchAt as any
startSketchAt.arguments[0] = createArrayExpression([
createLiteral(to[0]),
createLiteral(to[1]),
])
return {
modifiedAst: node,
}
}
if (maybeDefaultLine !== -1) {
const defaultLine = getNodeFromPath<CallExpression>(
node,
defaultLinePath
).node
const { from } = getSketchSegmentFromSourceRange(sketch, [
defaultLine.start,
defaultLine.end,
]).segment
return updateArgs({
node,
previousProgramMemory,
pathToNode: defaultLinePath,
to,
from,
})
}
const last = sketch.value[sketch.value.length - 1]
const last = sketch.value[sketch.value.length - 1] || sketch.start
const from = last.to
return add({
@ -1198,14 +1145,6 @@ function getFirstArgValuesForXYFns(callExpression: CallExpression): {
} {
// used for lineTo, line
const firstArg = callExpression.arguments[0]
if (firstArg.type === 'Literal' && firstArg.value === 'default') {
return {
val:
callExpression.callee.name === 'startSketchAt'
? [createLiteral(0), createLiteral(0)]
: [createLiteral(1), createLiteral(1)],
}
}
if (firstArg.type === 'ArrayExpression') {
return { val: [firstArg.elements[0], firstArg.elements[1]] }
}
@ -1215,8 +1154,6 @@ function getFirstArgValuesForXYFns(callExpression: CallExpression): {
if (to?.type === 'ArrayExpression') {
const [x, y] = to.elements
return { val: [x, y], tag }
} else if (to?.type === 'Literal' && to.value === 'default') {
return { val: [createLiteral(0), createLiteral(0)], tag }
}
}
throw new Error('expected ArrayExpression or ObjectExpression')

View File

@ -59,19 +59,19 @@ describe('testing swaping out sketch calls with xLine/xLineTo', () => {
` |> lineTo({ to: [1, 1], tag: 'abc1' }, %)`,
` |> line({ to: [-2.04, -0.7], tag: 'abc2' }, %)`,
` |> angledLine({`,
` angle: 157,`,
` length: 1.69,`,
` tag: 'abc3'`,
` angle: 157,`,
` length: 1.69,`,
` tag: 'abc3'`,
` }, %)`,
` |> angledLineOfXLength({`,
` angle: 217,`,
` length: 0.86,`,
` tag: 'abc4'`,
` angle: 217,`,
` length: 0.86,`,
` tag: 'abc4'`,
` }, %)`,
` |> angledLineOfYLength({`,
` angle: 104,`,
` length: 1.58,`,
` tag: 'abc5'`,
` angle: 104,`,
` length: 1.58,`,
` tag: 'abc5'`,
` }, %)`,
` |> angledLineToX({ angle: 55, to: -2.89, tag: 'abc6' }, %)`,
` |> angledLineToY({ angle: 330, to: 2.53, tag: 'abc7' }, %)`,
@ -144,9 +144,9 @@ describe('testing swaping out sketch calls with xLine/xLineTo', () => {
inputCode: bigExample,
callToSwap: [
`angledLine({`,
` angle: 157,`,
` length: 1.69,`,
` tag: 'abc3'`,
` angle: 157,`,
` length: 1.69,`,
` tag: 'abc3'`,
` }, %)`,
].join('\n'),
constraintType: 'horizontal',
@ -172,9 +172,9 @@ describe('testing swaping out sketch calls with xLine/xLineTo', () => {
inputCode: bigExample,
callToSwap: [
`angledLineOfXLength({`,
` angle: 217,`,
` length: 0.86,`,
` tag: 'abc4'`,
` angle: 217,`,
` length: 0.86,`,
` tag: 'abc4'`,
` }, %)`,
].join('\n'),
constraintType: 'horizontal',
@ -201,9 +201,9 @@ describe('testing swaping out sketch calls with xLine/xLineTo', () => {
inputCode: bigExample,
callToSwap: [
`angledLineOfYLength({`,
` angle: 104,`,
` length: 1.58,`,
` tag: 'abc5'`,
` angle: 104,`,
` length: 1.58,`,
` tag: 'abc5'`,
` }, %)`,
].join('\n'),
constraintType: 'vertical',
@ -401,6 +401,11 @@ show(part001)`
programMemory.root['part001'] as SketchGroup,
[index, index]
).segment
expect(segment).toEqual({ to: [0, 0.04], from: [0, 0.04], name: '' })
expect(segment).toEqual({
to: [0, 0.04],
from: [0, 0.04],
name: '',
type: 'base',
})
})
})

View File

@ -4,7 +4,7 @@ import {
VariableDeclarator,
CallExpression,
} from '../abstractSyntaxTreeTypes'
import { SketchGroup, SourceRange } from '../executor'
import { SketchGroup, SourceRange, Path } from '../executor'
export function getSketchSegmentFromSourceRange(
sketchGroup: SketchGroup,
@ -20,10 +20,10 @@ export function getSketchSegmentFromSourceRange(
startSourceRange[1] >= rangeEnd &&
sketchGroup.start
)
return { segment: sketchGroup.start, index: -1 }
return { segment: { ...sketchGroup.start, type: 'base' }, index: -1 }
const lineIndex = sketchGroup.value.findIndex(
({ __geoMeta: { sourceRange } }) =>
({ __geoMeta: { sourceRange } }: Path) =>
sourceRange[0] <= rangeStart && sourceRange[1] >= rangeEnd
)
const line = sketchGroup.value[lineIndex]

View File

@ -124,7 +124,8 @@ const part001 = startSketchAt([0, 0])
|> yLine(1.04, %) // ln-yLine-free should sub in segLen
|> xLineTo(30, %) // ln-xLineTo-free should convert to xLine
|> yLineTo(20, %) // ln-yLineTo-free should convert to yLine
show(part001)`
show(part001)
`
const expectModifiedScript = `const myVar = 3
const myVar2 = 5
const myVar3 = 6
@ -133,69 +134,70 @@ const myAng2 = 134
const part001 = startSketchAt([0, 0])
|> line({ to: [1, 3.82], tag: 'seg01' }, %) // ln-should-get-tag
|> angledLineToX([
-angleToMatchLengthX('seg01', myVar, %),
myVar
], %) // ln-lineTo-xAbsolute should use angleToMatchLengthX helper
-angleToMatchLengthX('seg01', myVar, %),
myVar
], %) // ln-lineTo-xAbsolute should use angleToMatchLengthX helper
|> angledLineToY([
-angleToMatchLengthY('seg01', myVar, %),
myVar
], %) // ln-lineTo-yAbsolute should use angleToMatchLengthY helper
-angleToMatchLengthY('seg01', myVar, %),
myVar
], %) // ln-lineTo-yAbsolute should use angleToMatchLengthY helper
|> angledLine([45, segLen('seg01', %)], %) // ln-lineTo-free should become angledLine
|> angledLine([45, segLen('seg01', %)], %) // ln-angledLineToX-free should become angledLine
|> angledLine([myAng, segLen('seg01', %)], %) // ln-angledLineToX-angle should become angledLine
|> angledLineToX([
angleToMatchLengthX('seg01', myVar2, %),
myVar2
], %) // ln-angledLineToX-xAbsolute should use angleToMatchLengthX to get angle
angleToMatchLengthX('seg01', myVar2, %),
myVar2
], %) // ln-angledLineToX-xAbsolute should use angleToMatchLengthX to get angle
|> angledLine([-45, segLen('seg01', %)], %) // ln-angledLineToY-free should become angledLine
|> angledLine([myAng2, segLen('seg01', %)], %) // ln-angledLineToY-angle should become angledLine
|> angledLineToY([
angleToMatchLengthY('seg01', myVar3, %),
myVar3
], %) // ln-angledLineToY-yAbsolute should use angleToMatchLengthY to get angle
angleToMatchLengthY('seg01', myVar3, %),
myVar3
], %) // ln-angledLineToY-yAbsolute should use angleToMatchLengthY to get angle
|> line([
min(segLen('seg01', %), myVar),
legLen(segLen('seg01', %), myVar)
], %) // ln-should use legLen for y
min(segLen('seg01', %), myVar),
legLen(segLen('seg01', %), myVar)
], %) // ln-should use legLen for y
|> line([
min(segLen('seg01', %), myVar),
-legLen(segLen('seg01', %), myVar)
], %) // ln-legLen but negative
min(segLen('seg01', %), myVar),
-legLen(segLen('seg01', %), myVar)
], %) // ln-legLen but negative
|> angledLine([-112, segLen('seg01', %)], %) // ln-should become angledLine
|> angledLine([myVar, segLen('seg01', %)], %) // ln-use segLen for secound arg
|> angledLine([45, segLen('seg01', %)], %) // ln-segLen again
|> angledLine([54, segLen('seg01', %)], %) // ln-should be transformed to angledLine
|> angledLineOfXLength([
legAngX(segLen('seg01', %), myVar),
min(segLen('seg01', %), myVar)
], %) // ln-should use legAngX to calculate angle
legAngX(segLen('seg01', %), myVar),
min(segLen('seg01', %), myVar)
], %) // ln-should use legAngX to calculate angle
|> angledLineOfXLength([
180 + legAngX(segLen('seg01', %), myVar),
min(segLen('seg01', %), myVar)
], %) // ln-same as above but should have + 180 to match original quadrant
180 + legAngX(segLen('seg01', %), myVar),
min(segLen('seg01', %), myVar)
], %) // ln-same as above but should have + 180 to match original quadrant
|> line([
legLen(segLen('seg01', %), myVar),
min(segLen('seg01', %), myVar)
], %) // ln-legLen again but yRelative
legLen(segLen('seg01', %), myVar),
min(segLen('seg01', %), myVar)
], %) // ln-legLen again but yRelative
|> line([
-legLen(segLen('seg01', %), myVar),
min(segLen('seg01', %), myVar)
], %) // ln-negative legLen yRelative
-legLen(segLen('seg01', %), myVar),
min(segLen('seg01', %), myVar)
], %) // ln-negative legLen yRelative
|> angledLine([58, segLen('seg01', %)], %) // ln-angledLineOfYLength-free should become angledLine
|> angledLine([myAng, segLen('seg01', %)], %) // ln-angledLineOfYLength-angle should become angledLine
|> angledLineOfXLength([
legAngY(segLen('seg01', %), myVar),
min(segLen('seg01', %), myVar)
], %) // ln-angledLineOfYLength-yRelative use legAngY
legAngY(segLen('seg01', %), myVar),
min(segLen('seg01', %), myVar)
], %) // ln-angledLineOfYLength-yRelative use legAngY
|> angledLineOfXLength([
270 + legAngY(segLen('seg01', %), myVar),
min(segLen('seg01', %), myVar)
], %) // ln-angledLineOfYLength-yRelative with angle > 90 use binExp
270 + legAngY(segLen('seg01', %), myVar),
min(segLen('seg01', %), myVar)
], %) // ln-angledLineOfYLength-yRelative with angle > 90 use binExp
|> xLine(segLen('seg01', %), %) // ln-xLine-free should sub in segLen
|> yLine(segLen('seg01', %), %) // ln-yLine-free should sub in segLen
|> xLine(segLen('seg01', %), %) // ln-xLineTo-free should convert to xLine
|> yLine(segLen('seg01', %), %) // ln-yLineTo-free should convert to yLine
show(part001)`
show(part001)
`
it('should transform the ast', async () => {
const ast = parser_wasm(inputScript)
const selectionRanges: Selections['codeBasedSelections'] = inputScript
@ -254,7 +256,8 @@ const part001 = startSketchAt([0, 0])
|> angledLineToY([223, 7.68], %) // select for vertical constraint 9
|> angledLineToX([333, myVar3], %) // select for horizontal constraint 10
|> angledLineToY([301, myVar], %) // select for vertical constraint 10
show(part001)`
show(part001)
`
it('should transform horizontal lines the ast', async () => {
const expectModifiedScript = `const myVar = 2
const myVar2 = 12
@ -281,7 +284,8 @@ const part001 = startSketchAt([0, 0])
|> angledLineToY([223, 7.68], %) // select for vertical constraint 9
|> xLineTo(myVar3, %) // select for horizontal constraint 10
|> angledLineToY([301, myVar], %) // select for vertical constraint 10
show(part001)`
show(part001)
`
const ast = parser_wasm(inputScript)
const selectionRanges: Selections['codeBasedSelections'] = inputScript
.split('\n')
@ -338,7 +342,8 @@ const part001 = startSketchAt([0, 0])
|> yLineTo(7.68, %) // select for vertical constraint 9
|> angledLineToX([333, myVar3], %) // select for horizontal constraint 10
|> yLineTo(myVar, %) // select for vertical constraint 10
show(part001)`
show(part001)
`
const ast = parser_wasm(inputScript)
const selectionRanges: Selections['codeBasedSelections'] = inputScript
.split('\n')
@ -380,7 +385,8 @@ const part001 = startSketchAt([0, 0])
|> line([0.45, 1.46], %) // free
|> line([myVar, 0.01], %) // xRelative
|> line([0.7, myVar], %) // yRelative
show(part001)`
show(part001)
`
it('testing for free to horizontal and vertical distance', async () => {
const expectedHorizontalCode = await helperThing(
inputScript,
@ -406,9 +412,9 @@ show(part001)`
'setVertDistance'
)
expect(expectedCode).toContain(`|> lineTo([
lastSegX(%) + myVar,
segEndY('seg01', %) + 2.93
], %) // xRelative`)
lastSegX(%) + myVar,
segEndY('seg01', %) + 2.93
], %) // xRelative`)
})
it('testing for yRelative to horizontal distance', async () => {
const expectedCode = await helperThing(
@ -417,9 +423,9 @@ show(part001)`
'setHorzDistance'
)
expect(expectedCode).toContain(`|> lineTo([
segEndX('seg01', %) + 2.6,
lastSegY(%) + myVar
], %) // yRelative`)
segEndX('seg01', %) + 2.6,
lastSegY(%) + myVar
], %) // yRelative`)
})
})
})

View File

@ -28,6 +28,7 @@ import { createFirstArg, getFirstArg, replaceSketchLine } from './sketch'
import { PathToNode, ProgramMemory } from '../executor'
import { getSketchSegmentFromSourceRange } from './sketchConstraints'
import { getAngle, roundOff, normaliseAngle } from '../../lib/utils'
import { MemoryItem } from 'wasm-lib/kcl/bindings/MemoryItem'
type LineInputsType =
| 'xAbsolute'
@ -1426,7 +1427,6 @@ export function transformAstSketchLines({
selectionRanges.codeBasedSelections.forEach(({ range }, index) => {
const callBack = transformInfos?.[index].createNode
const transformTo = transformInfos?.[index].tooltip
console.log('transformTo', transformInfos)
if (!callBack || !transformTo) throw new Error('no callback helper')
const getNode = getNodeFromPathCurry(
@ -1453,7 +1453,7 @@ export function transformAstSketchLines({
const varName = varDec.id.name
const sketchGroup = programMemory.root?.[varName]
if (!sketchGroup || sketchGroup.type !== 'sketchGroup')
if (!sketchGroup || sketchGroup.type !== 'SketchGroup')
throw new Error('not a sketch group')
const seg = getSketchSegmentFromSourceRange(sketchGroup, range).segment
const referencedSegment = referencedSegmentRange

156
src/lib/cameraControls.ts Normal file
View File

@ -0,0 +1,156 @@
const noModifiersPressed = (e: React.MouseEvent) =>
!e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey
export type CameraSystem =
| 'KittyCAD'
| 'OnShape'
| 'Trackpad Friendly'
| 'Solidworks'
| 'NX'
| 'Creo'
| 'AutoCAD'
export const cameraSystems: CameraSystem[] = [
'KittyCAD',
'OnShape',
'Trackpad Friendly',
'Solidworks',
'NX',
'Creo',
'AutoCAD',
]
interface MouseGuardHandler {
description: string
callback: (e: React.MouseEvent) => boolean
lenientDragStartButton?: number
}
interface MouseGuardZoomHandler {
description: string
dragCallback: (e: React.MouseEvent) => boolean
scrollCallback: (e: React.MouseEvent) => boolean
lenientDragStartButton?: number
}
interface MouseGuard {
pan: MouseGuardHandler
zoom: MouseGuardZoomHandler
rotate: MouseGuardHandler
}
export const cameraMouseDragGuards: Record<CameraSystem, MouseGuard> = {
KittyCAD: {
pan: {
description: 'Right click + Shift + drag or middle click + drag',
callback: (e) =>
(e.button === 1 && noModifiersPressed(e)) ||
(e.button === 2 && e.shiftKey),
},
zoom: {
description: 'Scroll wheel or Right click + Ctrl + drag',
dragCallback: (e) => e.button === 2 && e.ctrlKey,
scrollCallback: () => true,
},
rotate: {
description: 'Right click + drag',
callback: (e) => e.button === 2 && noModifiersPressed(e),
},
},
OnShape: {
pan: {
description: 'Right click + Ctrl + drag or middle click + drag',
callback: (e) =>
(e.button === 2 && e.ctrlKey) ||
(e.button === 1 && noModifiersPressed(e)),
},
zoom: {
description: 'Scroll wheel',
dragCallback: () => false,
scrollCallback: () => true,
},
rotate: {
description: 'Right click + drag',
callback: (e) => e.button === 2 && noModifiersPressed(e),
},
},
'Trackpad Friendly': {
pan: {
description: 'Left click + Alt + Shift + drag or middle click + drag',
callback: (e) =>
(e.button === 0 && e.altKey && e.shiftKey && !e.metaKey) ||
(e.button === 1 && noModifiersPressed(e)),
},
zoom: {
description: 'Scroll wheel or Left click + Alt + OS + drag',
dragCallback: (e) => e.button === 0 && e.altKey && e.metaKey,
scrollCallback: () => true,
},
rotate: {
description: 'Left click + Alt + drag',
callback: (e) => e.button === 0 && e.altKey && !e.shiftKey && !e.metaKey,
lenientDragStartButton: 0,
},
},
Solidworks: {
pan: {
description: 'Right click + Ctrl + drag',
callback: (e) => e.button === 2 && e.ctrlKey,
lenientDragStartButton: 2,
},
zoom: {
description: 'Scroll wheel or Middle click + Shift + drag',
dragCallback: (e) => e.button === 1 && e.shiftKey,
scrollCallback: () => true,
},
rotate: {
description: 'Middle click + drag',
callback: (e) => e.button === 1 && noModifiersPressed(e),
},
},
NX: {
pan: {
description: 'Middle click + Shift + drag',
callback: (e) => e.button === 1 && e.shiftKey,
},
zoom: {
description: 'Scroll wheel or Middle click + Ctrl + drag',
dragCallback: (e) => e.button === 1 && e.ctrlKey,
scrollCallback: () => true,
},
rotate: {
description: 'Middle click + drag',
callback: (e) => e.button === 1 && noModifiersPressed(e),
},
},
Creo: {
pan: {
description: 'Middle click + Shift + drag',
callback: (e) => e.button === 1 && e.shiftKey,
},
zoom: {
description: 'Scroll wheel or Middle click + Ctrl + drag',
dragCallback: (e) => e.button === 1 && e.ctrlKey,
scrollCallback: () => true,
},
rotate: {
description: 'Middle click + drag',
callback: (e) => e.button === 1 && noModifiersPressed(e),
},
},
AutoCAD: {
pan: {
description: 'Middle click + drag',
callback: (e) => e.button === 1 && noModifiersPressed(e),
},
zoom: {
description: 'Scroll wheel',
dragCallback: () => false,
scrollCallback: () => true,
},
rotate: {
description: 'Middle click + Shift + drag',
callback: (e) => e.button === 1 && e.shiftKey,
},
},
}

View File

@ -5,7 +5,7 @@ import {
readDir,
writeTextFile,
} from '@tauri-apps/api/fs'
import { documentDir } from '@tauri-apps/api/path'
import { documentDir, homeDir } from '@tauri-apps/api/path'
import { isTauri } from './isTauri'
import { ProjectWithEntryPointMetadata } from '../Router'
import { metadata } from 'tauri-plugin-fs-extra-api'
@ -32,7 +32,13 @@ export async function initializeProjectDirectory(directory: string) {
return directory
}
const docDirectory = await documentDir()
let docDirectory: string
try {
docDirectory = await documentDir()
} catch (e) {
console.log(e)
docDirectory = await homeDir() // seems to work better on Linux
}
const INITIAL_DEFAULT_DIR = docDirectory + PROJECT_FOLDER

View File

@ -49,7 +49,7 @@ class MockEngineCommandManager {
export async function enginelessExecutor(
ast: Program,
pm: ProgramMemory = { root: {} }
pm: ProgramMemory = { root: {}, return: null }
): Promise<ProgramMemory> {
const mockEngineCommandManager = new MockEngineCommandManager({
setIsStreamReady: () => {},
@ -64,7 +64,7 @@ export async function enginelessExecutor(
export async function executor(
ast: Program,
pm: ProgramMemory = { root: {} }
pm: ProgramMemory = { root: {}, return: null }
): Promise<ProgramMemory> {
const engineCommandManager = new EngineCommandManager({
setIsStreamReady: () => {},

View File

@ -56,6 +56,27 @@ export function throttle<T>(
return throttled
}
// takes a function and executes it after the wait time, if the function is called again before the wait time is up, the timer is reset
export function defferExecution<T>(func: (args: T) => any, wait: number) {
let timeout: ReturnType<typeof setTimeout> | null
let latestArgs: T
function later() {
timeout = null
func(latestArgs)
}
function deffered(args: T) {
latestArgs = args
if (timeout) {
clearTimeout(timeout)
}
timeout = setTimeout(later, wait)
}
return deffered
}
export function getNormalisedCoordinates({
clientX,
clientY,

View File

@ -118,16 +118,14 @@ async function getUser(context: UserContext) {
if (!context.token && '__TAURI__' in window) throw 'not log in'
if (context.token) headers['Authorization'] = `Bearer ${context.token}`
if (SKIP_AUTH) return LOCAL_USER
try {
const response = await fetch(url, {
method: 'GET',
credentials: 'include',
headers,
})
const user = await response.json()
if ('error_code' in user) throw new Error(user.message)
return user
} catch (e) {
console.error(e)
}
const response = await fetch(url, {
method: 'GET',
credentials: 'include',
headers,
})
const user = await response.json()
if ('error_code' in user) throw new Error(user.message)
return user
}

View File

@ -1,29 +1,54 @@
import { assign, createMachine } from 'xstate'
import { BaseUnit, baseUnitsUnion } from '../useStore'
import { CommandBarMeta } from '../lib/commands'
import { Themes, getSystemTheme, setThemeClass } from '../lib/theme'
import { CameraSystem, cameraSystems } from 'lib/cameraControls'
export const DEFAULT_PROJECT_NAME = 'project-$nnn'
export enum UnitSystem {
Imperial = 'imperial',
Metric = 'metric',
}
export const baseUnits = {
imperial: ['in', 'ft'],
metric: ['mm', 'cm', 'm'],
} as const
export type BaseUnit = 'in' | 'ft' | 'mm' | 'cm' | 'm'
export const baseUnitsUnion = Object.values(baseUnits).flatMap((v) => v)
export type Toggle = 'On' | 'Off'
export const SETTINGS_PERSIST_KEY = 'SETTINGS_PERSIST_KEY'
export const settingsCommandBarMeta: CommandBarMeta = {
'Set Theme': {
displayValue: (args: string[]) => 'Change the app theme',
'Set Base Unit': {
displayValue: (args: string[]) => 'Set your default base unit',
args: [
{
name: 'theme',
name: 'baseUnit',
type: 'select',
defaultValue: 'theme',
options: Object.values(Themes).map((v) => ({ name: v })) as {
name: string
}[],
defaultValue: 'baseUnit',
options: Object.values(baseUnitsUnion).map((v) => ({ name: v })),
},
],
},
'Set Camera Controls': {
displayValue: (args: string[]) => 'Set your camera controls',
args: [
{
name: 'cameraControls',
type: 'select',
defaultValue: 'cameraControls',
options: Object.values(cameraSystems).map((v) => ({ name: v })),
},
],
},
'Set Default Directory': {
hide: 'both',
},
'Set Default Project Name': {
displayValue: (args: string[]) => 'Set a new default project name',
hide: 'web',
@ -37,9 +62,33 @@ export const settingsCommandBarMeta: CommandBarMeta = {
},
],
},
'Set Default Directory': {
'Set Onboarding Status': {
hide: 'both',
},
'Set Text Wrapping': {
displayValue: (args: string[]) => 'Set whether text in the editor wraps',
args: [
{
name: 'textWrapping',
type: 'select',
defaultValue: 'textWrapping',
options: [{ name: 'On' }, { name: 'Off' }],
},
],
},
'Set Theme': {
displayValue: (args: string[]) => 'Change the app theme',
args: [
{
name: 'theme',
type: 'select',
defaultValue: 'theme',
options: Object.values(Themes).map((v): { name: string } => ({
name: v,
})),
},
],
},
'Set Unit System': {
displayValue: (args: string[]) => 'Set your default unit system',
args: [
@ -51,20 +100,6 @@ export const settingsCommandBarMeta: CommandBarMeta = {
},
],
},
'Set Base Unit': {
displayValue: (args: string[]) => 'Set your default base unit',
args: [
{
name: 'baseUnit',
type: 'select',
defaultValue: 'baseUnit',
options: Object.values(baseUnitsUnion).map((v) => ({ name: v })),
},
],
},
'Set Onboarding Status': {
hide: 'both',
},
}
export const settingsMachine = createMachine(
@ -73,35 +108,34 @@ export const settingsMachine = createMachine(
id: 'Settings',
predictableActionArguments: true,
context: {
theme: Themes.System,
defaultProjectName: '',
unitSystem: UnitSystem.Imperial,
baseUnit: 'in' as BaseUnit,
cameraControls: 'KittyCAD' as CameraSystem,
defaultDirectory: '',
showDebugPanel: false,
defaultProjectName: DEFAULT_PROJECT_NAME,
onboardingStatus: '',
showDebugPanel: false,
textWrapping: 'On' as Toggle,
theme: Themes.System,
unitSystem: UnitSystem.Imperial,
},
initial: 'idle',
states: {
idle: {
entry: ['setThemeClass'],
on: {
'Set Theme': {
'Set Base Unit': {
actions: [
assign({
theme: (_, event) => event.data.theme,
}),
assign({ baseUnit: (_, event) => event.data.baseUnit }),
'persistSettings',
'toastSuccess',
'setThemeClass',
],
target: 'idle',
internal: true,
},
'Set Default Project Name': {
'Set Camera Controls': {
actions: [
assign({
defaultProjectName: (_, event) => event.data.defaultProjectName,
cameraControls: (_, event) => event.data.cameraControls,
}),
'persistSettings',
'toastSuccess',
@ -120,12 +154,11 @@ export const settingsMachine = createMachine(
target: 'idle',
internal: true,
},
'Set Unit System': {
'Set Default Project Name': {
actions: [
assign({
unitSystem: (_, event) => event.data.unitSystem,
baseUnit: (_, event) =>
event.data.unitSystem === 'imperial' ? 'in' : 'mm',
defaultProjectName: (_, event) =>
event.data.defaultProjectName.trim() || DEFAULT_PROJECT_NAME,
}),
'persistSettings',
'toastSuccess',
@ -133,9 +166,46 @@ export const settingsMachine = createMachine(
target: 'idle',
internal: true,
},
'Set Base Unit': {
'Set Onboarding Status': {
actions: [
assign({ baseUnit: (_, event) => event.data.baseUnit }),
assign({
onboardingStatus: (_, event) => event.data.onboardingStatus,
}),
'persistSettings',
],
target: 'idle',
internal: true,
},
'Set Text Wrapping': {
actions: [
assign({
textWrapping: (_, event) => event.data.textWrapping,
}),
'persistSettings',
'toastSuccess',
],
target: 'idle',
internal: true,
},
'Set Theme': {
actions: [
assign({
theme: (_, event) => event.data.theme,
}),
'persistSettings',
'toastSuccess',
'setThemeClass',
],
target: 'idle',
internal: true,
},
'Set Unit System': {
actions: [
assign({
unitSystem: (_, event) => event.data.unitSystem,
baseUnit: (_, event) =>
event.data.unitSystem === 'imperial' ? 'in' : 'mm',
}),
'persistSettings',
'toastSuccess',
],
@ -155,34 +225,29 @@ export const settingsMachine = createMachine(
target: 'idle',
internal: true,
},
'Set Onboarding Status': {
actions: [
assign({
onboardingStatus: (_, event) => event.data.onboardingStatus,
}),
'persistSettings',
],
target: 'idle',
internal: true,
},
},
},
},
tsTypes: {} as import('./settingsMachine.typegen').Typegen0,
schema: {
events: {} as
| { type: 'Set Theme'; data: { theme: Themes } }
| { type: 'Set Base Unit'; data: { baseUnit: BaseUnit } }
| {
type: 'Set Camera Controls'
data: { cameraControls: CameraSystem }
}
| { type: 'Set Default Directory'; data: { defaultDirectory: string } }
| {
type: 'Set Default Project Name'
data: { defaultProjectName: string }
}
| { type: 'Set Default Directory'; data: { defaultDirectory: string } }
| { type: 'Set Onboarding Status'; data: { onboardingStatus: string } }
| { type: 'Set Text Wrapping'; data: { textWrapping: Toggle } }
| { type: 'Set Theme'; data: { theme: Themes } }
| {
type: 'Set Unit System'
data: { unitSystem: UnitSystem }
}
| { type: 'Set Base Unit'; data: { baseUnit: BaseUnit } }
| { type: 'Set Onboarding Status'; data: { onboardingStatus: string } }
| { type: 'Toggle Debug Panel' },
},
},

View File

@ -15,25 +15,31 @@ export interface Typegen0 {
eventsCausingActions: {
persistSettings:
| 'Set Base Unit'
| 'Set Camera Controls'
| 'Set Default Directory'
| 'Set Default Project Name'
| 'Set Onboarding Status'
| 'Set Text Wrapping'
| 'Set Theme'
| 'Set Unit System'
| 'Toggle Debug Panel'
setThemeClass:
| 'Set Base Unit'
| 'Set Camera Controls'
| 'Set Default Directory'
| 'Set Default Project Name'
| 'Set Onboarding Status'
| 'Set Text Wrapping'
| 'Set Theme'
| 'Set Unit System'
| 'Toggle Debug Panel'
| 'xstate.init'
toastSuccess:
| 'Set Base Unit'
| 'Set Camera Controls'
| 'Set Default Directory'
| 'Set Default Project Name'
| 'Set Text Wrapping'
| 'Set Theme'
| 'Set Unit System'
| 'Toggle Debug Panel'

View File

@ -28,6 +28,7 @@ import {
import useStateMachineCommands from '../hooks/useStateMachineCommands'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { DEFAULT_PROJECT_NAME } from 'machines/settingsMachine'
// This route only opens in the Tauri desktop context for now,
// as defined in Router.tsx, so we can use the Tauri APIs and types.
@ -38,6 +39,7 @@ const Home = () => {
const {
settings: {
context: { defaultDirectory, defaultProjectName },
send: sendToSettings,
},
} = useGlobalStateContext()
@ -71,16 +73,33 @@ const Home = () => {
context: ContextFrom<typeof homeMachine>,
event: EventFrom<typeof homeMachine, 'Create project'>
) => {
let name =
let name = (
event.data && 'name' in event.data
? event.data.name
: defaultProjectName
).trim()
let shouldUpdateDefaultProjectName = false
// If there is no default project name, flag it to be set to the default
if (!name) {
name = DEFAULT_PROJECT_NAME
shouldUpdateDefaultProjectName = true
}
if (doesProjectNameNeedInterpolated(name)) {
const nextIndex = await getNextProjectIndex(name, projects)
name = interpolateProjectNameWithIndex(name, nextIndex)
}
await createNewProject(context.defaultDirectory + '/' + name)
if (shouldUpdateDefaultProjectName) {
sendToSettings({
type: 'Set Default Project Name',
data: { defaultProjectName: DEFAULT_PROJECT_NAME },
})
}
return `Successfully created "${name}"`
},
renameProject: async (

View File

@ -4,8 +4,8 @@ import { onboardingPaths, useDismiss, useNextClick } from '.'
import { useStore } from '../../useStore'
export default function Units() {
const { isMouseDownInStream } = useStore((s) => ({
isMouseDownInStream: s.isMouseDownInStream,
const { buttonDownInStream } = useStore((s) => ({
buttonDownInStream: s.buttonDownInStream,
}))
const dismiss = useDismiss()
const next = useNextClick(onboardingPaths.SKETCHING)
@ -15,7 +15,7 @@ export default function Units() {
<div
className={
'max-w-2xl flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded' +
(isMouseDownInStream ? '' : ' pointer-events-auto')
(buttonDownInStream ? '' : ' pointer-events-auto')
}
>
<h1 className="text-2xl font-bold">Camera</h1>

View File

@ -1,5 +1,5 @@
import { faArrowRight, faXmark } from '@fortawesome/free-solid-svg-icons'
import { BaseUnit, baseUnits } from '../../useStore'
import { BaseUnit, baseUnits } from '../../machines/settingsMachine'
import { ActionButton } from '../../components/ActionButton'
import { SettingsSection } from '../Settings'
import { Toggle } from '../../components/Toggle/Toggle'

View File

@ -6,13 +6,22 @@ import {
import { ActionButton } from '../components/ActionButton'
import { AppHeader } from '../components/AppHeader'
import { open } from '@tauri-apps/api/dialog'
import { BaseUnit, baseUnits } from '../useStore'
import {
BaseUnit,
DEFAULT_PROJECT_NAME,
baseUnits,
} from '../machines/settingsMachine'
import { Toggle } from '../components/Toggle/Toggle'
import { useLocation, useNavigate, useRouteLoaderData } from 'react-router-dom'
import { useHotkeys } from 'react-hotkeys-hook'
import { IndexLoaderData, paths } from '../Router'
import { Themes } from '../lib/theme'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import {
CameraSystem,
cameraSystems,
cameraMouseDragGuards,
} from 'lib/cameraControls'
import { UnitSystem } from 'machines/settingsMachine'
export const Settings = () => {
@ -25,12 +34,13 @@ export const Settings = () => {
send,
state: {
context: {
baseUnit,
cameraControls,
defaultDirectory,
defaultProjectName,
showDebugPanel,
defaultDirectory,
unitSystem,
baseUnit,
theme,
unitSystem,
},
},
},
@ -82,6 +92,42 @@ export const Settings = () => {
, and start a discussion if you don't see it! Your feedback will help
us prioritize what to build next.
</p>
<SettingsSection
title="Camera Controls"
description="How you want to control the camera in the 3D view"
>
<select
id="camera-controls"
className="block w-full px-3 py-1 border border-chalkboard-30 bg-transparent"
value={cameraControls}
onChange={(e) => {
send({
type: 'Set Camera Controls',
data: { cameraControls: e.target.value as CameraSystem },
})
}}
>
{cameraSystems.map((program) => (
<option key={program} value={program}>
{program}
</option>
))}
</select>
<ul className="text-sm my-2 mx-4 leading-relaxed">
<li>
<strong>Pan:</strong>{' '}
{cameraMouseDragGuards[cameraControls].pan.description}
</li>
<li>
<strong>Zoom:</strong>{' '}
{cameraMouseDragGuards[cameraControls].zoom.description}
</li>
<li>
<strong>Rotate:</strong>{' '}
{cameraMouseDragGuards[cameraControls].rotate.description}
</li>
</ul>
</SettingsSection>
{(window as any).__TAURI__ && (
<>
<SettingsSection
@ -118,10 +164,14 @@ export const Settings = () => {
className="block w-full px-3 py-1 border border-chalkboard-30 bg-transparent"
defaultValue={defaultProjectName}
onBlur={(e) => {
const newValue = e.target.value.trim() || DEFAULT_PROJECT_NAME
send({
type: 'Set Default Project Name',
data: { defaultProjectName: e.target.value },
data: {
defaultProjectName: newValue,
},
})
e.target.value = newValue
}}
autoCapitalize="off"
autoComplete="off"

View File

@ -19,6 +19,7 @@ import {
EngineCommandManager,
} from './lang/std/engineConnection'
import { KCLError } from './lang/errors'
import { defferExecution } from 'lib/utils'
export type Selection = {
type: 'default' | 'line-end' | 'line-mid'
@ -42,9 +43,12 @@ export type TooTip =
| 'yLineTo'
| 'angledLineThatIntersects'
export const toolTips: TooTip[] = [
'lineTo',
export const toolTips = [
'sketch_line',
'move',
// original tooltips
'line',
'lineTo',
'angledLine',
'angledLineOfXLength',
'angledLineOfYLength',
@ -55,7 +59,7 @@ export const toolTips: TooTip[] = [
'xLineTo',
'yLineTo',
'angledLineThatIntersects',
]
] as any as TooTip[]
export type GuiModes =
| {
@ -65,6 +69,7 @@ export type GuiModes =
mode: 'sketch'
sketchMode: TooTip
isTooltip: true
waitingFirstClick: boolean
rotation: Rotation
position: Position
id?: string
@ -83,6 +88,7 @@ export type GuiModes =
}
| {
mode: 'canEditSketch'
pathId: string
pathToNode: PathToNode
rotation: Rotation
position: Position
@ -94,15 +100,6 @@ export type GuiModes =
position: Position
}
export const baseUnits = {
imperial: ['in', 'ft'],
metric: ['mm', 'cm', 'm'],
} as const
export type BaseUnit = 'in' | 'ft' | 'mm' | 'cm' | 'm'
export const baseUnitsUnion = Object.values(baseUnits).flatMap((v) => v)
export type PaneType =
| 'code'
| 'variables'
@ -130,8 +127,8 @@ export interface StoreState {
kclErrors: KCLError[]
addKCLError: (err: KCLError) => void
resetKCLErrors: () => void
ast: Program | null
setAst: (ast: Program | null) => void
ast: Program
setAst: (ast: Program) => void
updateAst: (
ast: Program,
optionalParams?: {
@ -141,7 +138,9 @@ export interface StoreState {
) => void
updateAstAsync: (ast: Program, focusPath?: PathToNode) => void
code: string
defferedCode: string
setCode: (code: string) => void
defferedSetCode: (code: string) => void
formatCode: () => void
errorState: {
isError: boolean
@ -166,8 +165,8 @@ export interface StoreState {
setIsStreamReady: (isStreamReady: boolean) => void
isLSPServerReady: boolean
setIsLSPServerReady: (isLSPServerReady: boolean) => void
isMouseDownInStream: boolean
setIsMouseDownInStream: (isMouseDownInStream: boolean) => void
buttonDownInStream: number | undefined
setButtonDownInStream: (buttonDownInStream: number | undefined) => void
didDragInStream: boolean
setDidDragInStream: (didDragInStream: boolean) => void
fileId: string
@ -177,6 +176,8 @@ export interface StoreState {
streamWidth: number
streamHeight: number
}) => void
isExecuting: boolean
setIsExecuting: (isExecuting: boolean) => void
showHomeMenu: boolean
setHomeShowMenu: (showMenu: boolean) => void
@ -195,193 +196,220 @@ let pendingAstUpdates: number[] = []
export const useStore = create<StoreState>()(
persist(
(set, get) => ({
editorView: null,
setEditorView: (editorView) => {
set({ editorView })
},
highlightRange: [0, 0],
setHighlightRange: (selection) => {
set({ highlightRange: selection })
const editorView = get().editorView
if (editorView) {
editorView.dispatch({ effects: addLineHighlight.of(selection) })
}
},
setCursor: (selections) => {
const { editorView } = get()
if (!editorView) return
const ranges: ReturnType<typeof EditorSelection.cursor>[] = []
const selectionRangeTypeMap: { [key: number]: Selection['type'] } = {}
set({ selectionRangeTypeMap })
selections.codeBasedSelections.forEach(({ range, type }) => {
if (range?.[1]) {
ranges.push(EditorSelection.cursor(range[1]))
selectionRangeTypeMap[range[1]] = type
(set, get) => {
const setDefferedCode = defferExecution(
(code: string) => set({ defferedCode: code }),
600
)
return {
editorView: null,
setEditorView: (editorView) => {
set({ editorView })
},
highlightRange: [0, 0],
setHighlightRange: (selection) => {
set({ highlightRange: selection })
const editorView = get().editorView
if (editorView) {
editorView.dispatch({ effects: addLineHighlight.of(selection) })
}
})
setTimeout(() => {
editorView.dispatch({
selection: EditorSelection.create(
ranges,
selections.codeBasedSelections.length - 1
),
},
setCursor: (selections) => {
const { editorView } = get()
if (!editorView) return
const ranges: ReturnType<typeof EditorSelection.cursor>[] = []
const selectionRangeTypeMap: { [key: number]: Selection['type'] } = {}
set({ selectionRangeTypeMap })
selections.codeBasedSelections.forEach(({ range, type }) => {
if (range?.[1]) {
ranges.push(EditorSelection.cursor(range[1]))
selectionRangeTypeMap[range[1]] = type
}
})
})
},
setCursor2: (codeSelections) => {
const currestSelections = get().selectionRanges
const code = get().code
if (!codeSelections) {
get().setCursor({
otherSelections: currestSelections.otherSelections,
codeBasedSelections: [
{ range: [0, code.length - 1], type: 'default' },
],
})
return
}
const selections: Selections = {
...currestSelections,
codeBasedSelections: get().isShiftDown
? [...currestSelections.codeBasedSelections, codeSelections]
: [codeSelections],
}
get().setCursor(selections)
},
selectionRangeTypeMap: {},
selectionRanges: {
otherSelections: [],
codeBasedSelections: [],
},
setSelectionRanges: (selectionRanges) =>
set({ selectionRanges, selectionRangeTypeMap: {} }),
guiMode: { mode: 'default' },
lastGuiMode: { mode: 'default' },
setGuiMode: (guiMode) => {
set({ guiMode })
},
logs: [],
addLog: (log) => {
if (Array.isArray(log)) {
const cleanLog: any = log.map(({ __geoMeta, ...rest }) => rest)
set((state) => ({ logs: [...state.logs, cleanLog] }))
} else {
set((state) => ({ logs: [...state.logs, log] }))
}
},
resetLogs: () => {
set({ logs: [] })
},
kclErrors: [],
addKCLError: (e) => {
set((state) => ({ kclErrors: [...state.kclErrors, e] }))
},
resetKCLErrors: () => {
set({ kclErrors: [] })
},
ast: null,
setAst: (ast) => {
set({ ast })
},
updateAst: async (ast, { focusPath, callBack = () => {} } = {}) => {
const newCode = recast(ast)
const astWithUpdatedSource = parser_wasm(newCode)
callBack(astWithUpdatedSource)
set({ ast: astWithUpdatedSource, code: newCode })
if (focusPath) {
const { node } = getNodeFromPath<any>(astWithUpdatedSource, focusPath)
const { start, end } = node
if (!start || !end) return
setTimeout(() => {
ranges.length &&
editorView.dispatch({
selection: EditorSelection.create(
ranges,
selections.codeBasedSelections.length - 1
),
})
})
},
setCursor2: (codeSelections) => {
const currestSelections = get().selectionRanges
const code = get().code
if (!codeSelections) {
get().setCursor({
otherSelections: currestSelections.otherSelections,
codeBasedSelections: [
{
type: 'default',
range: [start, end],
},
{ range: [0, code.length - 1], type: 'default' },
],
otherSelections: [],
})
})
}
},
updateAstAsync: async (ast, focusPath) => {
// clear any pending updates
pendingAstUpdates.forEach((id) => clearTimeout(id))
pendingAstUpdates = []
// setup a new update
pendingAstUpdates.push(
setTimeout(() => {
get().updateAst(ast, { focusPath })
}, 100) as unknown as number
)
},
code: '',
setCode: (code) => {
set({ code })
},
formatCode: async () => {
const code = get().code
const ast = parser_wasm(code)
const newCode = recast(ast)
set({ code: newCode, ast })
},
errorState: {
isError: false,
error: '',
},
setError: (error = '') => {
set({ errorState: { isError: !!error, error } })
},
programMemory: { root: {}, pendingMemory: {} },
setProgramMemory: (programMemory) => set({ programMemory }),
isShiftDown: false,
setIsShiftDown: (isShiftDown) => set({ isShiftDown }),
artifactMap: {},
sourceRangeMap: {},
setArtifactNSourceRangeMaps: (maps) => set({ ...maps }),
setEngineCommandManager: (engineCommandManager) =>
set({ engineCommandManager }),
setMediaStream: (mediaStream) => set({ mediaStream }),
isStreamReady: false,
setIsStreamReady: (isStreamReady) => set({ isStreamReady }),
isLSPServerReady: false,
setIsLSPServerReady: (isLSPServerReady) => set({ isLSPServerReady }),
isMouseDownInStream: false,
setIsMouseDownInStream: (isMouseDownInStream) => {
set({ isMouseDownInStream })
},
didDragInStream: false,
setDidDragInStream: (didDragInStream) => {
set({ didDragInStream })
},
// For stream event handling
fileId: '',
setFileId: (fileId) => set({ fileId }),
streamDimensions: { streamWidth: 1280, streamHeight: 720 },
setStreamDimensions: (streamDimensions) => set({ streamDimensions }),
return
}
const selections: Selections = {
...currestSelections,
codeBasedSelections: get().isShiftDown
? [...currestSelections.codeBasedSelections, codeSelections]
: [codeSelections],
}
get().setCursor(selections)
},
selectionRangeTypeMap: {},
selectionRanges: {
otherSelections: [],
codeBasedSelections: [],
},
setSelectionRanges: (selectionRanges) =>
set({ selectionRanges, selectionRangeTypeMap: {} }),
guiMode: { mode: 'default' },
lastGuiMode: { mode: 'default' },
setGuiMode: (guiMode) => {
set({ guiMode })
},
logs: [],
addLog: (log) => {
if (Array.isArray(log)) {
const cleanLog: any = log.map(({ __geoMeta, ...rest }) => rest)
set((state) => ({ logs: [...state.logs, cleanLog] }))
} else {
set((state) => ({ logs: [...state.logs, log] }))
}
},
resetLogs: () => {
set({ logs: [] })
},
kclErrors: [],
addKCLError: (e) => {
set((state) => ({ kclErrors: [...state.kclErrors, e] }))
},
resetKCLErrors: () => {
set({ kclErrors: [] })
},
ast: {
start: 0,
end: 0,
body: [],
nonCodeMeta: {
noneCodeNodes: {},
start: null,
},
},
setAst: (ast) => {
set({ ast })
},
updateAst: async (ast, { focusPath, callBack = () => {} } = {}) => {
const newCode = recast(ast)
const astWithUpdatedSource = parser_wasm(newCode)
callBack(astWithUpdatedSource)
// tauri specific app settings
defaultDir: {
dir: '',
},
isBannerDismissed: false,
setBannerDismissed: (isBannerDismissed) => set({ isBannerDismissed }),
openPanes: ['code'],
setOpenPanes: (openPanes) => set({ openPanes }),
showHomeMenu: true,
setHomeShowMenu: (showHomeMenu) => set({ showHomeMenu }),
homeMenuItems: [],
setHomeMenuItems: (homeMenuItems) => set({ homeMenuItems }),
}),
set({
ast: astWithUpdatedSource,
code: newCode,
defferedCode: newCode,
})
if (focusPath) {
const { node } = getNodeFromPath<any>(
astWithUpdatedSource,
focusPath
)
const { start, end } = node
if (!start || !end) return
setTimeout(() => {
get().setCursor({
codeBasedSelections: [
{
type: 'default',
range: [start, end],
},
],
otherSelections: [],
})
})
}
},
updateAstAsync: async (ast, focusPath) => {
// clear any pending updates
pendingAstUpdates.forEach((id) => clearTimeout(id))
pendingAstUpdates = []
// setup a new update
pendingAstUpdates.push(
setTimeout(() => {
get().updateAst(ast, { focusPath })
}, 100) as unknown as number
)
},
code: '',
defferedCode: '',
setCode: (code) => set({ code, defferedCode: code }),
defferedSetCode: (code) => {
set({ code })
setDefferedCode(code)
},
formatCode: async () => {
const code = get().code
const ast = parser_wasm(code)
const newCode = recast(ast)
set({ code: newCode, ast })
},
errorState: {
isError: false,
error: '',
},
setError: (error = '') => {
set({ errorState: { isError: !!error, error } })
},
programMemory: { root: {}, return: null },
setProgramMemory: (programMemory) => set({ programMemory }),
isShiftDown: false,
setIsShiftDown: (isShiftDown) => set({ isShiftDown }),
artifactMap: {},
sourceRangeMap: {},
setArtifactNSourceRangeMaps: (maps) => set({ ...maps }),
setEngineCommandManager: (engineCommandManager) =>
set({ engineCommandManager }),
setMediaStream: (mediaStream) => set({ mediaStream }),
isStreamReady: false,
setIsStreamReady: (isStreamReady) => set({ isStreamReady }),
isLSPServerReady: false,
setIsLSPServerReady: (isLSPServerReady) => set({ isLSPServerReady }),
buttonDownInStream: undefined,
setButtonDownInStream: (buttonDownInStream) => {
set({ buttonDownInStream })
},
didDragInStream: false,
setDidDragInStream: (didDragInStream) => {
set({ didDragInStream })
},
// For stream event handling
fileId: '',
setFileId: (fileId) => set({ fileId }),
streamDimensions: { streamWidth: 1280, streamHeight: 720 },
setStreamDimensions: (streamDimensions) => set({ streamDimensions }),
isExecuting: false,
setIsExecuting: (isExecuting) => set({ isExecuting }),
// tauri specific app settings
defaultDir: {
dir: '',
},
isBannerDismissed: false,
setBannerDismissed: (isBannerDismissed) => set({ isBannerDismissed }),
openPanes: ['code'],
setOpenPanes: (openPanes) => set({ openPanes }),
showHomeMenu: true,
setHomeShowMenu: (showHomeMenu) => set({ showHomeMenu }),
homeMenuItems: [],
setHomeMenuItems: (homeMenuItems) => set({ homeMenuItems }),
}
},
{
name: 'store',
partialize: (state) =>
Object.fromEntries(
Object.entries(state).filter(([key]) =>
['code', 'openPanes'].includes(key)
['code', 'defferedCode', 'openPanes'].includes(key)
)
),
}

658
src/wasm-lib/Cargo.lock generated
View File

@ -229,6 +229,32 @@ dependencies = [
"serde",
]
[[package]]
name = "bindgen"
version = "0.64.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4243e6031260db77ede97ad86c27e501d646a27ab57b59a574f725d98ab1fb4"
dependencies = [
"bitflags 1.3.2",
"cexpr",
"clang-sys",
"lazy_static",
"lazycell",
"peeking_take_while",
"proc-macro2",
"quote",
"regex",
"rustc-hash",
"shlex",
"syn 1.0.109",
]
[[package]]
name = "bit_field"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61"
[[package]]
name = "bitflags"
version = "1.3.2"
@ -290,6 +316,12 @@ version = "3.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1"
[[package]]
name = "bytemuck"
version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6"
[[package]]
name = "byteorder"
version = "1.4.3"
@ -314,6 +346,15 @@ dependencies = [
"libc",
]
[[package]]
name = "cexpr"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
dependencies = [
"nom",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
@ -336,6 +377,17 @@ dependencies = [
"winapi",
]
[[package]]
name = "clang-sys"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c688fc74432808e3eb684cae8830a86be1d66a2bd58e1f248ed0960a590baf6f"
dependencies = [
"glob",
"libc",
"libloading",
]
[[package]]
name = "clap"
version = "3.2.25"
@ -418,6 +470,12 @@ version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961"
[[package]]
name = "color_quant"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
[[package]]
name = "colorchoice"
version = "1.0.0"
@ -482,6 +540,15 @@ dependencies = [
"libc",
]
[[package]]
name = "crc32fast"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
dependencies = [
"cfg-if",
]
[[package]]
name = "crossbeam-channel"
version = "0.5.8"
@ -492,6 +559,30 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-deque"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef"
dependencies = [
"cfg-if",
"crossbeam-epoch",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7"
dependencies = [
"autocfg",
"cfg-if",
"crossbeam-utils",
"memoffset",
"scopeguard",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.16"
@ -501,6 +592,12 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "crunchy"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
[[package]]
name = "crypto-common"
version = "0.1.6"
@ -521,7 +618,7 @@ dependencies = [
"hashbrown 0.14.0",
"lock_api",
"once_cell",
"parking_lot_core",
"parking_lot_core 0.9.8",
]
[[package]]
@ -667,12 +764,85 @@ dependencies = [
"similar",
]
[[package]]
name = "exr"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1e481eb11a482815d3e9d618db8c42a93207134662873809335a92327440c18"
dependencies = [
"bit_field",
"flume",
"half",
"lebe",
"miniz_oxide",
"rayon-core",
"smallvec",
"zune-inflate",
]
[[package]]
name = "fastrand"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764"
[[package]]
name = "fdeflate"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d329bdeac514ee06249dabc27877490f17f5d371ec693360768b838e19f3ae10"
dependencies = [
"simd-adler32",
]
[[package]]
name = "ffmpeg-next"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8af03c47ad26832ab3aabc4cdbf210af3d3b878783edd5a7ba044ba33aab7a60"
dependencies = [
"bitflags 1.3.2",
"ffmpeg-sys-next",
"libc",
]
[[package]]
name = "ffmpeg-sys-next"
version = "6.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf650f461ccf130f4eef4927affed703cc387b183bfc4a7dfee86a076c131127"
dependencies = [
"bindgen",
"cc",
"libc",
"num_cpus",
"pkg-config",
"vcpkg",
]
[[package]]
name = "flate2"
version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6c98ee8095e9d1dcbf2fcc6d95acccb90d1c81db1e44725c6a984b1dbdfb010"
dependencies = [
"crc32fast",
"miniz_oxide",
]
[[package]]
name = "flume"
version = "0.10.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577"
dependencies = [
"futures-core",
"futures-sink",
"nanorand",
"pin-project",
"spin 0.9.8",
]
[[package]]
name = "fnv"
version = "1.0.7"
@ -819,12 +989,28 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "gif"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80792593675e051cf94a4b111980da2ba60d4a83e43e0048c5693baab3977045"
dependencies = [
"color_quant",
"weezl",
]
[[package]]
name = "gimli"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0"
[[package]]
name = "glob"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
[[package]]
name = "gloo-utils"
version = "0.2.0"
@ -857,6 +1043,15 @@ dependencies = [
"tracing",
]
[[package]]
name = "half"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02b4af3693f1b705df946e9fe5631932443781d0aabb423b62fcd4d73f6d2fd0"
dependencies = [
"crunchy",
]
[[package]]
name = "hashbrown"
version = "0.12.3"
@ -1010,6 +1205,37 @@ dependencies = [
"unicode-normalization",
]
[[package]]
name = "image"
version = "0.24.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f3dfdbdd72063086ff443e297b61695500514b1e41095b6fb9a5ab48a70a711"
dependencies = [
"bytemuck",
"byteorder",
"color_quant",
"exr",
"gif",
"jpeg-decoder",
"num-rational",
"num-traits",
"png",
"qoi",
"tiff",
]
[[package]]
name = "image-compare"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "419d59423b7202f6a2a95d3b9cf11a8cc9bf83a29cf7dda4d617a90e8c5ccfcf"
dependencies = [
"image",
"itertools 0.10.5",
"rayon",
"thiserror",
]
[[package]]
name = "indexmap"
version = "1.9.3"
@ -1031,6 +1257,18 @@ dependencies = [
"hashbrown 0.14.0",
]
[[package]]
name = "instant"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
dependencies = [
"cfg-if",
"js-sys",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "ipnet"
version = "2.8.0"
@ -1072,6 +1310,15 @@ version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
[[package]]
name = "jpeg-decoder"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e"
dependencies = [
"rayon",
]
[[package]]
name = "js-sys"
version = "0.3.64"
@ -1094,7 +1341,7 @@ dependencies = [
[[package]]
name = "kcl-lib"
version = "0.1.20"
version = "0.1.26"
dependencies = [
"anyhow",
"bson",
@ -1126,23 +1373,36 @@ dependencies = [
[[package]]
name = "kittycad"
version = "0.2.23"
version = "0.2.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8b33e5df8f82b97e5f5af94ff1400ae37449d0f5f1bb79acedf17cf2193680f"
checksum = "d9cf962b1e81a0b4eb923a727e761b40672cbacc7f5f0b75e13579d346352bc7"
dependencies = [
"anyhow",
"async-trait",
"base64 0.21.2",
"bytes",
"chrono",
"data-encoding",
"format_serde_error",
"futures",
"http",
"itertools 0.10.5",
"log",
"parse-display",
"phonenumber",
"rand",
"reqwest",
"reqwest-conditional-middleware",
"reqwest-middleware 0.2.3",
"reqwest-retry",
"reqwest-tracing",
"schemars",
"serde",
"serde_bytes",
"serde_json",
"serde_urlencoded",
"thiserror",
"tracing",
"url",
"uuid",
]
@ -1153,12 +1413,34 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "lazycell"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
[[package]]
name = "lebe"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
[[package]]
name = "libc"
version = "0.2.147"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
[[package]]
name = "libloading"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f"
dependencies = [
"cfg-if",
"winapi",
]
[[package]]
name = "linked-hash-map"
version = "0.5.6"
@ -1212,12 +1494,27 @@ dependencies = [
"url",
]
[[package]]
name = "matchit"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed1202b2a6f884ae56f04cff409ab315c5ce26b5e58d7412e484f01fd52f52ef"
[[package]]
name = "memchr"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
[[package]]
name = "memoffset"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c"
dependencies = [
"autocfg",
]
[[package]]
name = "mime"
version = "0.3.17"
@ -1247,6 +1544,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7"
dependencies = [
"adler",
"simd-adler32",
]
[[package]]
@ -1260,6 +1558,15 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "nanorand"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3"
dependencies = [
"getrandom",
]
[[package]]
name = "newline-converter"
version = "0.3.0"
@ -1300,6 +1607,17 @@ dependencies = [
"num-traits",
]
[[package]]
name = "num-rational"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0"
dependencies = [
"autocfg",
"num-integer",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.16"
@ -1380,7 +1698,7 @@ dependencies = [
"rand",
"regex",
"reqwest",
"reqwest-middleware",
"reqwest-middleware 0.1.6",
"rustfmt-wrapper",
"schemars",
"serde",
@ -1415,12 +1733,42 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "opentelemetry"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6105e89802af13fdf48c49d7646d3b533a70e536d818aae7e78ba0433d01acb8"
dependencies = [
"async-trait",
"crossbeam-channel",
"futures-channel",
"futures-executor",
"futures-util",
"js-sys",
"lazy_static",
"percent-encoding",
"pin-project",
"rand",
"thiserror",
]
[[package]]
name = "os_str_bytes"
version = "6.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d5d9eb14b174ee9aa2ef96dc2b94637a2d4b6e7cb873c7e171f0c20c6cf3eac"
[[package]]
name = "parking_lot"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
dependencies = [
"instant",
"lock_api",
"parking_lot_core 0.8.6",
]
[[package]]
name = "parking_lot"
version = "0.12.1"
@ -1428,7 +1776,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
dependencies = [
"lock_api",
"parking_lot_core",
"parking_lot_core 0.9.8",
]
[[package]]
name = "parking_lot_core"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc"
dependencies = [
"cfg-if",
"instant",
"libc",
"redox_syscall 0.2.16",
"smallvec",
"winapi",
]
[[package]]
@ -1470,6 +1832,12 @@ dependencies = [
"syn 2.0.29",
]
[[package]]
name = "peeking_take_while"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099"
[[package]]
name = "percent-encoding"
version = "2.3.0"
@ -1538,6 +1906,25 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pkg-config"
version = "0.3.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964"
[[package]]
name = "png"
version = "0.17.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd75bf2d8dd3702b9707cdbc56a5b9ef42cec752eb8b3bafc01234558442aa64"
dependencies = [
"bitflags 1.3.2",
"crc32fast",
"fdeflate",
"flate2",
"miniz_oxide",
]
[[package]]
name = "ppv-lite86"
version = "0.2.17"
@ -1587,6 +1974,15 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "qoi"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001"
dependencies = [
"bytemuck",
]
[[package]]
name = "quick-xml"
version = "0.28.2"
@ -1641,6 +2037,28 @@ dependencies = [
"getrandom",
]
[[package]]
name = "rayon"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b"
dependencies = [
"either",
"rayon-core",
]
[[package]]
name = "rayon-core"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d"
dependencies = [
"crossbeam-channel",
"crossbeam-deque",
"crossbeam-utils",
"num_cpus",
]
[[package]]
name = "redox_syscall"
version = "0.2.16"
@ -1757,6 +2175,18 @@ dependencies = [
"winreg",
]
[[package]]
name = "reqwest-conditional-middleware"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59e50a2e70970896c99d1b8f20ddc30a70b30d3ac6e619a03a8353b64a49b277"
dependencies = [
"async-trait",
"reqwest",
"reqwest-middleware 0.2.3",
"task-local-extensions",
]
[[package]]
name = "reqwest-middleware"
version = "0.1.6"
@ -1773,6 +2203,73 @@ dependencies = [
"thiserror",
]
[[package]]
name = "reqwest-middleware"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff44108c7925d082f2861e683a88618b68235ad9cdc60d64d9d1188efc951cdb"
dependencies = [
"anyhow",
"async-trait",
"http",
"reqwest",
"serde",
"task-local-extensions",
"thiserror",
]
[[package]]
name = "reqwest-retry"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c6a11c05102e5bec712c0619b8c7b7eda8b21a558a0bd981ceee15c38df8be4"
dependencies = [
"anyhow",
"async-trait",
"chrono",
"futures",
"getrandom",
"http",
"hyper",
"parking_lot 0.11.2",
"reqwest",
"reqwest-middleware 0.2.3",
"retry-policies",
"task-local-extensions",
"tokio",
"tracing",
"wasm-timer",
]
[[package]]
name = "reqwest-tracing"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14b1e66540e0cac90acadaf7109bf99c90d95abcc94b4c096bfa16a2d7aa7a71"
dependencies = [
"anyhow",
"async-trait",
"getrandom",
"matchit",
"opentelemetry",
"reqwest",
"reqwest-middleware 0.2.3",
"task-local-extensions",
"tracing",
"tracing-opentelemetry",
]
[[package]]
name = "retry-policies"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e09bbcb5003282bcb688f0bae741b278e9c7e8f378f561522c9806c58e075d9b"
dependencies = [
"anyhow",
"chrono",
"rand",
]
[[package]]
name = "ring"
version = "0.16.20"
@ -1782,7 +2279,7 @@ dependencies = [
"cc",
"libc",
"once_cell",
"spin",
"spin 0.5.2",
"untrusted",
"web-sys",
"winapi",
@ -1794,6 +2291,12 @@ version = "0.1.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
[[package]]
name = "rustc-hash"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustfmt-wrapper"
version = "0.2.0"
@ -2102,6 +2605,21 @@ dependencies = [
"digest",
]
[[package]]
name = "sharded-slab"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31"
dependencies = [
"lazy_static",
]
[[package]]
name = "shlex"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380"
[[package]]
name = "signal-hook-registry"
version = "1.4.1"
@ -2111,6 +2629,12 @@ dependencies = [
"libc",
]
[[package]]
name = "simd-adler32"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
[[package]]
name = "similar"
version = "2.2.1"
@ -2223,6 +2747,15 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
[[package]]
name = "spin"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
dependencies = [
"lock_api",
]
[[package]]
name = "strsim"
version = "0.10.0"
@ -2367,6 +2900,17 @@ dependencies = [
"once_cell",
]
[[package]]
name = "tiff"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d172b0f4d3fba17ba89811858b9d3d97f928aece846475bbda076ca46736211"
dependencies = [
"flate2",
"jpeg-decoder",
"weezl",
]
[[package]]
name = "time"
version = "0.1.45"
@ -2434,7 +2978,7 @@ dependencies = [
"libc",
"mio",
"num_cpus",
"parking_lot",
"parking_lot 0.12.1",
"pin-project-lite",
"signal-hook-registry",
"socket2 0.5.3",
@ -2605,6 +3149,43 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a"
dependencies = [
"once_cell",
"valuable",
]
[[package]]
name = "tracing-log"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922"
dependencies = [
"lazy_static",
"log",
"tracing-core",
]
[[package]]
name = "tracing-opentelemetry"
version = "0.17.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbbe89715c1dbbb790059e2565353978564924ee85017b5fff365c872ff6721f"
dependencies = [
"once_cell",
"opentelemetry",
"tracing",
"tracing-core",
"tracing-log",
"tracing-subscriber",
]
[[package]]
name = "tracing-subscriber"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77"
dependencies = [
"sharded-slab",
"thread_local",
"tracing-core",
]
[[package]]
@ -2668,6 +3249,19 @@ dependencies = [
"utf-8",
]
[[package]]
name = "twenty-twenty"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c77f4c2039b998d194da9717843bb8c634609b5ccacac513be56985569f6e732"
dependencies = [
"anyhow",
"ffmpeg-next",
"image",
"image-compare",
"uuid",
]
[[package]]
name = "typenum"
version = "1.16.0"
@ -2769,6 +3363,18 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "valuable"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "version_check"
version = "0.9.4"
@ -2877,14 +3483,20 @@ checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1"
name = "wasm-lib"
version = "0.1.0"
dependencies = [
"anyhow",
"bson",
"futures",
"gloo-utils",
"image",
"js-sys",
"kcl-lib",
"kittycad",
"reqwest",
"serde_json",
"tokio",
"tower-lsp",
"twenty-twenty",
"uuid",
"wasm-bindgen",
"wasm-bindgen-futures",
"wasm-streams",
@ -2904,6 +3516,21 @@ dependencies = [
"web-sys",
]
[[package]]
name = "wasm-timer"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be0ecb0db480561e9a7642b5d3e4187c128914e58aa84330b9493e3eb68c5e7f"
dependencies = [
"futures",
"js-sys",
"parking_lot 0.11.2",
"pin-utils",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]]
name = "web-sys"
version = "0.3.64"
@ -2920,6 +3547,12 @@ version = "0.25.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc"
[[package]]
name = "weezl"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb"
[[package]]
name = "winapi"
version = "0.3.9"
@ -3125,3 +3758,12 @@ name = "yansi"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
[[package]]
name = "zune-inflate"
version = "0.2.54"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02"
dependencies = [
"simd-adler32",
]

View File

@ -11,11 +11,20 @@ crate-type = ["cdylib"]
bson = { version = "2.7.0", features = ["uuid-1", "chrono"] }
gloo-utils = "0.2.0"
kcl-lib = { path = "kcl" }
kittycad = { version = "0.2.23", default-features = false, features = ["js"] }
kittycad = { version = "0.2.25", default-features = false, features = ["js"] }
serde_json = "1.0.93"
wasm-bindgen = "0.2.87"
wasm-bindgen-futures = "0.4.37"
[dev-dependencies]
anyhow = "1"
image = "0.24.7"
kittycad = "0.2.25"
reqwest = { version = "0.11.20", default-features = false }
tokio = { version = "1.32.0", features = ["rt-multi-thread", "macros", "time"] }
twenty-twenty = "0.6.1"
uuid = { version = "1.4.1", features = ["v4", "js", "serde"] }
[target.'cfg(target_arch = "wasm32")'.dependencies]
futures = "0.3.28"
js-sys = "0.3.64"

View File

@ -1,7 +1,7 @@
[package]
name = "kcl-lib"
description = "KittyCAD Language"
version = "0.1.20"
version = "0.1.26"
edition = "2021"
license = "MIT"
@ -11,9 +11,9 @@ license = "MIT"
anyhow = { version = "1.0.75", features = ["backtrace"] }
clap = { version = "4.4.2", features = ["cargo", "derive", "env", "unicode"] }
dashmap = "5.5.3"
derive-docs = { version = "0.1.1" }
derive-docs = { version = "0.1.3" }
#derive-docs = { path = "../derive-docs" }
kittycad = { version = "0.2.23", default-features = false, features = ["js"] }
kittycad = { version = "0.2.25", default-features = false, features = ["js"] }
lazy_static = "1.4.0"
parse-display = "0.8.2"
regex = "1.7.1"

4
src/wasm-lib/kcl/fuzz/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
target
corpus
artifacts
coverage

2218
src/wasm-lib/kcl/fuzz/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,27 @@
[package]
name = "kcl-lib-fuzz"
version = "0.0.0"
publish = false
edition = "2021"
[package.metadata]
cargo-fuzz = true
[dependencies]
libfuzzer-sys = "0.4"
[dependencies.kcl-lib]
path = ".."
# Prevent this from interfering with workspaces
[workspace]
members = ["."]
[profile.release]
debug = 1
[[bin]]
name = "parser"
path = "fuzz_targets/parser.rs"
test = false
doc = false

View File

@ -0,0 +1,14 @@
#![no_main]
#[macro_use]
extern crate libfuzzer_sys;
extern crate kcl_lib;
fuzz_target!(|data: &[u8]| {
if let Ok(s) = std::str::from_utf8(data) {
let tokens = kcl_lib::tokeniser::lexer(s);
let parser = kcl_lib::parser::Parser::new(tokens);
if let Ok(_) = parser.ast() {
println!("OK");
}
}
});

File diff suppressed because it is too large Load Diff

View File

@ -87,6 +87,9 @@ impl EngineConnection {
if let Some(msg) = ws_resp.resp {
match msg {
OkWebSocketResponseData::MetricsRequest {} => {
// @paultag todo
}
OkWebSocketResponseData::IceServerInfo { ice_servers } => {
println!("got ice server info: {:?}", ice_servers);
}

View File

@ -98,16 +98,14 @@ impl ProgramReturn {
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type", rename_all = "camelCase")]
#[serde(tag = "type")]
pub enum MemoryItem {
UserVal {
value: serde_json::Value,
#[serde(rename = "__meta")]
meta: Vec<Metadata>,
},
UserVal(UserVal),
SketchGroup(SketchGroup),
ExtrudeGroup(ExtrudeGroup),
#[ts(skip)]
ExtrudeTransform(ExtrudeTransform),
#[ts(skip)]
Function {
#[serde(skip)]
func: Option<MemoryFunction>,
@ -119,7 +117,16 @@ pub enum MemoryItem {
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
#[serde(tag = "type", rename_all = "camelCase")]
pub struct UserVal {
pub value: serde_json::Value,
#[serde(rename = "__meta")]
pub meta: Vec<Metadata>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type", rename_all = "camelCase")]
pub struct ExtrudeTransform {
pub position: Position,
pub rotation: Rotation,
@ -138,7 +145,7 @@ pub type MemoryFunction = fn(
impl From<MemoryItem> for Vec<SourceRange> {
fn from(item: MemoryItem) -> Self {
match item {
MemoryItem::UserVal { meta, .. } => meta.iter().map(|m| m.source_range).collect(),
MemoryItem::UserVal(u) => u.meta.iter().map(|m| m.source_range).collect(),
MemoryItem::SketchGroup(s) => s.meta.iter().map(|m| m.source_range).collect(),
MemoryItem::ExtrudeGroup(e) => e.meta.iter().map(|m| m.source_range).collect(),
MemoryItem::ExtrudeTransform(e) => e.meta.iter().map(|m| m.source_range).collect(),
@ -149,8 +156,8 @@ impl From<MemoryItem> for Vec<SourceRange> {
impl MemoryItem {
pub fn get_json_value(&self) -> Result<serde_json::Value, KclError> {
if let MemoryItem::UserVal { value, .. } = self {
Ok(value.clone())
if let MemoryItem::UserVal(user_val) = self {
Ok(user_val.value.clone())
} else {
Err(KclError::Semantic(KclErrorDetails {
message: format!("Not a user value: {:?}", self),
@ -186,7 +193,7 @@ impl MemoryItem {
/// A sketch group is a collection of paths.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
#[serde(tag = "type", rename_all = "camelCase")]
pub struct SketchGroup {
/// The id of the sketch group.
pub id: uuid::Uuid,
@ -238,7 +245,7 @@ impl SketchGroup {
/// An extrude group is a collection of extrude surfaces.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
#[serde(tag = "type", rename_all = "camelCase")]
pub struct ExtrudeGroup {
/// The id of the extrude group.
pub id: uuid::Uuid,
@ -276,15 +283,15 @@ pub enum BodyType {
#[derive(Debug, Deserialize, Serialize, PartialEq, Copy, Clone, ts_rs::TS, JsonSchema)]
#[ts(export)]
pub struct Position(pub [f64; 3]);
pub struct Position(#[ts(type = "[number, number, number]")] pub [f64; 3]);
#[derive(Debug, Deserialize, Serialize, PartialEq, Copy, Clone, ts_rs::TS, JsonSchema)]
#[ts(export)]
pub struct Rotation(pub [f64; 4]);
pub struct Rotation(#[ts(type = "[number, number, number, number]")] pub [f64; 4]);
#[derive(Debug, Default, Deserialize, Serialize, PartialEq, Copy, Clone, ts_rs::TS, JsonSchema, Hash, Eq)]
#[ts(export)]
pub struct SourceRange(pub [usize; 2]);
pub struct SourceRange(#[ts(type = "[number, number]")] pub [usize; 2]);
impl SourceRange {
/// Create a new source range.
@ -401,8 +408,10 @@ impl From<SourceRange> for Metadata {
#[serde(rename_all = "camelCase")]
pub struct BasePath {
/// The from point.
#[ts(type = "[number, number]")]
pub from: [f64; 2],
/// The to point.
#[ts(type = "[number, number]")]
pub to: [f64; 2],
/// The name of the path.
pub name: String,
@ -595,7 +604,9 @@ pub fn execute(
memory.return_ = Some(ProgramReturn::Arguments(call_expr.arguments.clone()));
} else if let Some(func) = memory.clone().root.get(&fn_name) {
func.call_fn(&args, memory, engine)?;
let result = func.call_fn(&args, memory, engine)?;
memory.return_ = result;
} else {
return Err(KclError::Semantic(KclErrorDetails {
message: format!("No such name {} defined", fn_name),
@ -642,7 +653,7 @@ pub fn execute(
for (index, param) in function_expression.params.iter().enumerate() {
fn_memory.add(
&param.name,
args.clone().get(index).unwrap().clone(),
args.get(index).unwrap().clone(),
param.into(),
)?;
}
@ -696,11 +707,39 @@ pub fn execute(
let result = bin_expr.get_result(memory, &mut pipe_info, engine)?;
memory.return_ = Some(ProgramReturn::Value(result));
}
Value::UnaryExpression(unary_expr) => {
let result = unary_expr.get_result(memory, &mut pipe_info, engine)?;
memory.return_ = Some(ProgramReturn::Value(result));
}
Value::Identifier(identifier) => {
let value = memory.get(&identifier.name, identifier.into())?.clone();
memory.return_ = Some(ProgramReturn::Value(value));
}
_ => (),
Value::Literal(literal) => {
memory.return_ = Some(ProgramReturn::Value(literal.into()));
}
Value::ArrayExpression(array_expr) => {
let result = array_expr.execute(memory, &mut pipe_info, engine)?;
memory.return_ = Some(ProgramReturn::Value(result));
}
Value::ObjectExpression(obj_expr) => {
let result = obj_expr.execute(memory, &mut pipe_info, engine)?;
memory.return_ = Some(ProgramReturn::Value(result));
}
Value::CallExpression(call_expr) => {
let result = call_expr.execute(memory, &mut pipe_info, engine)?;
memory.return_ = Some(ProgramReturn::Value(result));
}
Value::MemberExpression(member_expr) => {
let result = member_expr.get_result(memory)?;
memory.return_ = Some(ProgramReturn::Value(result));
}
Value::PipeExpression(pipe_expr) => {
let result = pipe_expr.get_result(memory, &mut pipe_info, engine)?;
memory.return_ = Some(ProgramReturn::Value(result));
}
Value::PipeSubstitution(_) => {}
Value::FunctionExpression(_) => {}
},
}
}
@ -856,4 +895,116 @@ show(part001)"#;
parse_execute(ast).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn test_execute_with_function_literal_in_pipe() {
let ast = r#"const w = 20
const l = 8
const h = 10
fn thing = () => {
return -8
}
const firstExtrude = startSketchAt([0,0])
|> line([0, l], %)
|> line([w, 0], %)
|> line([0, thing()], %)
|> close(%)
|> extrude(h, %)
show(firstExtrude)"#;
parse_execute(ast).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn test_execute_with_function_unary_in_pipe() {
let ast = r#"const w = 20
const l = 8
const h = 10
fn thing = (x) => {
return -x
}
const firstExtrude = startSketchAt([0,0])
|> line([0, l], %)
|> line([w, 0], %)
|> line([0, thing(8)], %)
|> close(%)
|> extrude(h, %)
show(firstExtrude)"#;
parse_execute(ast).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn test_execute_with_function_array_in_pipe() {
let ast = r#"const w = 20
const l = 8
const h = 10
fn thing = (x) => {
return [0, -x]
}
const firstExtrude = startSketchAt([0,0])
|> line([0, l], %)
|> line([w, 0], %)
|> line(thing(8), %)
|> close(%)
|> extrude(h, %)
show(firstExtrude)"#;
parse_execute(ast).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn test_execute_with_function_call_in_pipe() {
let ast = r#"const w = 20
const l = 8
const h = 10
fn other_thing = (y) => {
return -y
}
fn thing = (x) => {
return other_thing(x)
}
const firstExtrude = startSketchAt([0,0])
|> line([0, l], %)
|> line([w, 0], %)
|> line([0, thing(8)], %)
|> close(%)
|> extrude(h, %)
show(firstExtrude)"#;
parse_execute(ast).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn test_execute_with_function_sketch() {
let ast = r#"const box = (h, l, w) => {
const myBox = startSketchAt([0,0])
|> line([0, l], %)
|> line([w, 0], %)
|> line([0, -l], %)
|> close(%)
|> extrude(h, %)
return myBox
}
const fnBox = box(3, 6, 10)
show(fnBox)"#;
parse_execute(ast).await.unwrap();
}
}

View File

@ -228,8 +228,8 @@ impl ReversePolishNotation {
.collect::<Vec<Token>>(),
);
return rpn.parse();
} else if current_token.value == ")" {
if !self.operators.is_empty() && self.operators[self.operators.len() - 1].value != "(" {
} else if current_token.value == ")" && !self.operators.is_empty() {
if self.operators[self.operators.len() - 1].value != "(" {
// pop operators off the stack and push them to postFix until we find the matching '('
let rpn = ReversePolishNotation::new(
&self.parser.tokens,

View File

@ -336,17 +336,26 @@ impl Parser {
value: if start_end_string.starts_with("\n\n") && is_new_line_comment {
// Preserve if they want a whitespace line before the comment.
// But let's just allow one.
NoneCodeValue::NewLineBlock { value: full_string }
NoneCodeValue::NewLineBlockComment { value: full_string }
} else if is_new_line_comment {
NoneCodeValue::Block { value: full_string }
NoneCodeValue::BlockComment { value: full_string }
} else {
NoneCodeValue::Inline { value: full_string }
NoneCodeValue::InlineComment { value: full_string }
},
};
Ok((Some(node), end_index - 1))
}
fn next_meaningful_token(&self, index: usize, offset: Option<usize>) -> Result<TokenReturnWithNonCode, KclError> {
// There is no next meaningful token.
if index >= self.tokens.len() - 1 {
return Ok(TokenReturnWithNonCode {
token: None,
index: self.tokens.len() - 1,
non_code_node: None,
});
}
let new_index = index + offset.unwrap_or(1);
let Ok(token) = self.get_token(new_index) else {
return Ok(TokenReturnWithNonCode {
@ -405,7 +414,7 @@ impl Parser {
if found_another_opening_brace {
return self.find_closing_brace(index + 1, brace_count + 1, search_opening_brace);
}
if found_another_closing_brace {
if found_another_closing_brace && brace_count > 0 {
return self.find_closing_brace(index + 1, brace_count - 1, search_opening_brace);
}
// non-brace token, increment and continue
@ -610,6 +619,12 @@ impl Parser {
fn make_member_expression(&self, index: usize) -> Result<MemberExpressionReturn, KclError> {
let current_token = self.get_token(index)?;
let mut keys_info = self.collect_object_keys(index, None)?;
if keys_info.is_empty() {
return Err(KclError::Syntax(KclErrorDetails {
source_ranges: vec![current_token.into()],
message: "expected to be started on a identifier or literal".to_string(),
}));
}
let last_key = keys_info[keys_info.len() - 1].clone();
let first_key = keys_info.remove(0);
let root = self.make_identifier(index)?;
@ -679,7 +694,11 @@ impl Parser {
return Ok(index);
}
let next_right = self.next_meaningful_token(maybe_operator.index, None)?;
self.find_end_of_binary_expression(next_right.index)
if next_right.index != index {
self.find_end_of_binary_expression(next_right.index)
} else {
Ok(index)
}
} else {
Ok(index)
}
@ -847,6 +866,8 @@ impl Parser {
fn make_array_expression(&self, index: usize) -> Result<ArrayReturn, KclError> {
let opening_brace_token = self.get_token(index)?;
let first_element_token = self.next_meaningful_token(index, None)?;
// Make sure there is a closing brace.
let _closing_brace = self.find_closing_brace(index, 0, "")?;
let array_elements = self.make_array_elements(first_element_token.index, Vec::new())?;
Ok(ArrayReturn {
expression: ArrayExpression {
@ -1018,7 +1039,7 @@ impl Parser {
} else {
return Err(KclError::Unimplemented(KclErrorDetails {
source_ranges: vec![argument_token_token.clone().into()],
message: format!("Unexpected token {} ", argument_token_token.value),
message: format!("Unexpected token {}", argument_token_token.value),
}));
};
}
@ -1043,18 +1064,18 @@ impl Parser {
Err(KclError::Unimplemented(KclErrorDetails {
source_ranges: vec![argument_token_token.clone().into()],
message: format!("Unexpected token {} ", argument_token_token.value),
message: format!("Unexpected token {}", argument_token_token.value),
}))
} else {
Err(KclError::Unimplemented(KclErrorDetails {
source_ranges: vec![brace_or_comma_token.into()],
message: format!("Unexpected token {} ", brace_or_comma_token.value),
message: format!("Unexpected token {}", brace_or_comma_token.value),
}))
}
} else {
Err(KclError::Unimplemented(KclErrorDetails {
source_ranges: vec![brace_or_comma_token.into()],
message: format!("Unexpected token {} ", brace_or_comma_token.value),
message: format!("Unexpected token {}", brace_or_comma_token.value),
}))
}
}
@ -1063,6 +1084,8 @@ impl Parser {
let current_token = self.get_token(index)?;
let brace_token = self.next_meaningful_token(index, None)?;
let callee = self.make_identifier(index)?;
// Make sure there is a closing brace.
let _closing_brace_token = self.find_closing_brace(brace_token.index, 0, "")?;
let args = self.make_arguments(brace_token.index, vec![])?;
let closing_brace_token = self.get_token(args.last_index)?;
let function = if let Some(stdlib_fn) = self.stdlib.get(&callee.name) {
@ -1105,42 +1128,42 @@ impl Parser {
) -> Result<VariableDeclaratorsReturn, KclError> {
let current_token = self.get_token(index)?;
let assignment = self.next_meaningful_token(index, None)?;
if let Some(assignment_token) = assignment.token {
let contents_start_token = self.next_meaningful_token(assignment.index, None)?;
let pipe_start_index = if assignment_token.token_type == TokenType::Operator {
contents_start_token.index
} else {
assignment.index
};
let next_pipe_operator = self.has_pipe_operator(pipe_start_index, None)?;
let init: Value;
let last_index = if next_pipe_operator.token.is_some() {
let pipe_expression_result = self.make_pipe_expression(assignment.index)?;
init = Value::PipeExpression(Box::new(pipe_expression_result.expression));
pipe_expression_result.last_index
} else {
let value_result = self.make_value(contents_start_token.index)?;
init = value_result.value;
value_result.last_index
};
let current_declarator = VariableDeclarator {
start: current_token.start,
end: self.get_token(last_index)?.end,
id: self.make_identifier(index)?,
init,
};
let mut declarations = previous_declarators;
declarations.push(current_declarator);
Ok(VariableDeclaratorsReturn {
declarations,
last_index,
})
} else {
Err(KclError::Unimplemented(KclErrorDetails {
let Some(assignment_token) = assignment.token else {
return Err(KclError::Unimplemented(KclErrorDetails {
source_ranges: vec![current_token.clone().into()],
message: format!("Unexpected token {} ", current_token.value),
}))
}
message: format!("Unexpected token {}", current_token.value),
}));
};
let contents_start_token = self.next_meaningful_token(assignment.index, None)?;
let pipe_start_index = if assignment_token.token_type == TokenType::Operator {
contents_start_token.index
} else {
assignment.index
};
let next_pipe_operator = self.has_pipe_operator(pipe_start_index, None)?;
let init: Value;
let last_index = if next_pipe_operator.token.is_some() {
let pipe_expression_result = self.make_pipe_expression(assignment.index)?;
init = Value::PipeExpression(Box::new(pipe_expression_result.expression));
pipe_expression_result.last_index
} else {
let value_result = self.make_value(contents_start_token.index)?;
init = value_result.value;
value_result.last_index
};
let current_declarator = VariableDeclarator {
start: current_token.start,
end: self.get_token(last_index)?.end,
id: self.make_identifier(index)?,
init,
};
let mut declarations = previous_declarators;
declarations.push(current_declarator);
Ok(VariableDeclaratorsReturn {
declarations,
last_index,
})
}
fn make_variable_declaration(&self, index: usize) -> Result<VariableDeclarationResult, KclError> {
@ -1184,7 +1207,7 @@ impl Parser {
} else {
Err(KclError::Unimplemented(KclErrorDetails {
source_ranges: vec![brace_or_comma_token.into()],
message: format!("Unexpected token {} ", brace_or_comma_token.value),
message: format!("Unexpected token {}", brace_or_comma_token.value),
}))
}
}
@ -1192,6 +1215,12 @@ impl Parser {
fn make_unary_expression(&self, index: usize) -> Result<UnaryExpressionResult, KclError> {
let current_token = self.get_token(index)?;
let next_token = self.next_meaningful_token(index, None)?;
if next_token.token.is_none() {
return Err(KclError::Syntax(KclErrorDetails {
source_ranges: vec![current_token.into()],
message: "expected another token".to_string(),
}));
}
let argument = self.make_value(next_token.index)?;
let argument_token = self.get_token(argument.last_index)?;
Ok(UnaryExpressionResult {
@ -1232,7 +1261,6 @@ impl Parser {
return Ok(ExpressionStatementResult {
expression: ExpressionStatement {
start: current_token.start,
// end: call_expression.last_index,
end,
expression: Value::CallExpression(Box::new(call_expression.expression)),
},
@ -1314,6 +1342,8 @@ impl Parser {
fn make_object_expression(&self, index: usize) -> Result<ObjectExpressionResult, KclError> {
let opening_brace_token = self.get_token(index)?;
// Make sure there is a closing brace.
let _closing_brace = self.find_closing_brace(index, 0, "")?;
let first_property_token = self.next_meaningful_token(index, None)?;
let object_properties = self.make_object_properties(first_property_token.index, vec![])?;
Ok(ObjectExpressionResult {
@ -1665,7 +1695,7 @@ const key = 'c'"#,
Some(NoneCodeNode {
start: 38,
end: 60,
value: NoneCodeValue::Block {
value: NoneCodeValue::BlockComment {
value: "this is a comment".to_string(),
},
}),
@ -1687,7 +1717,7 @@ const key = 'c'"#,
Some(NoneCodeNode {
start: 106,
end: 166,
value: NoneCodeValue::Block {
value: NoneCodeValue::BlockComment {
value: "this is\n a comment\n spanning a few lines".to_string(),
},
}),
@ -2716,4 +2746,139 @@ show(mySk1)"#;
assert!(result.is_err());
assert!(result.err().unwrap().to_string().contains("file is empty"));
}
#[test]
fn test_parse_half_pipe_small() {
let tokens = crate::tokeniser::lexer(
"const secondExtrude = startSketchAt([0,0])
|",
);
let parser = Parser::new(tokens);
let result = parser.ast();
assert!(result.is_err());
assert!(result.err().unwrap().to_string().contains("Unexpected token"));
}
#[test]
fn test_parse_half_pipe() {
let tokens = crate::tokeniser::lexer(
"const height = 10
const firstExtrude = startSketchAt([0,0])
|> line([0, 8], %)
|> line([20, 0], %)
|> line([0, -8], %)
|> close(%)
|> extrude(2, %)
show(firstExtrude)
const secondExtrude = startSketchAt([0,0])
|",
);
let parser = Parser::new(tokens);
let result = parser.ast();
assert!(result.is_err());
assert!(result.err().unwrap().to_string().contains("Unexpected token"));
}
#[test]
fn test_parse_greater_bang() {
let tokens = crate::tokeniser::lexer(">!");
let parser = Parser::new(tokens);
let result = parser.ast();
assert!(result.is_ok());
}
#[test]
fn test_parse_z_percent_parens() {
let tokens = crate::tokeniser::lexer("z%)");
let parser = Parser::new(tokens);
let result = parser.ast();
assert!(result.is_err());
assert!(result.err().unwrap().to_string().contains("Unexpected token"));
}
#[test]
fn test_parse_parens_unicode() {
let tokens = crate::tokeniser::lexer("");
let parser = Parser::new(tokens);
let result = parser.ast();
assert!(result.is_ok());
}
#[test]
fn test_parse_nested_open_brackets() {
let tokens = crate::tokeniser::lexer(
r#"
z(-[["#,
);
let parser = Parser::new(tokens);
let result = parser.ast();
assert!(result.is_err());
assert!(result.err().unwrap().to_string().contains("unexpected end"));
}
#[test]
fn test_parse_weird_new_line_function() {
let tokens = crate::tokeniser::lexer(
r#"z
(--#"#,
);
let parser = Parser::new(tokens);
let result = parser.ast();
assert!(result.is_err());
assert!(result.err().unwrap().to_string().contains("unexpected end"));
}
#[test]
fn test_parse_weird_lots_of_fancy_brackets() {
let tokens = crate::tokeniser::lexer(r#"zz({{{{{{{{)iegAng{{{{{{{##"#);
let parser = Parser::new(tokens);
let result = parser.ast();
assert!(result.is_err());
assert!(result.err().unwrap().to_string().contains("unexpected end"));
}
#[test]
fn test_parse_weird_close_before_open() {
let tokens = crate::tokeniser::lexer(
r#"fn)n
e
["#,
);
let parser = Parser::new(tokens);
let result = parser.ast();
assert!(result.is_err());
assert!(result
.err()
.unwrap()
.to_string()
.contains("expected to be started on a identifier or literal"));
}
#[test]
fn test_parse_weird_close_before_nada() {
let tokens = crate::tokeniser::lexer(r#"fn)n-"#);
let parser = Parser::new(tokens);
let result = parser.ast();
assert!(result.is_err());
assert!(result.err().unwrap().to_string().contains("expected another token"));
}
#[test]
fn test_parse_weird_lots_of_slashes() {
let tokens = crate::tokeniser::lexer(
r#"J///////////o//+///////////P++++*++++++P///////˟
++4"#,
);
let parser = Parser::new(tokens);
let result = parser.ast();
assert!(result.is_err());
assert!(result
.err()
.unwrap()
.to_string()
.contains("unexpected end of expression"));
}
}

View File

@ -233,6 +233,7 @@ impl LanguageServer for Backend {
document_symbol_provider: Some(OneOf::Left(true)),
hover_provider: Some(HoverProviderCapability::Simple(true)),
inlay_hint_provider: Some(OneOf::Left(true)),
rename_provider: Some(OneOf::Left(true)),
semantic_tokens_provider: Some(SemanticTokensServerCapabilities::SemanticTokensRegistrationOptions(
SemanticTokensRegistrationOptions {
text_document_registration_options: {
@ -552,19 +553,14 @@ impl LanguageServer for Backend {
return Ok(None);
};
// Now recast it.
// Make spaces for the tab size.
/*let mut tab_size = String::new();
for _ in 0..params.options.tab_size {
tab_size.push(' ');
}*/
// TODO: use the tab size.
let mut recast = ast.recast("", false).trim().to_string();
if let Some(insert_final_newline) = params.options.insert_final_newline {
if insert_final_newline {
recast.push('\n');
}
}
let recast = ast.recast(
&crate::abstract_syntax_tree_types::FormatOptions {
tab_size: params.options.tab_size as usize,
insert_final_newline: params.options.insert_final_newline.unwrap_or(false),
use_tabs: !params.options.insert_spaces,
},
0,
);
let source_range = SourceRange([0, current_code.len() - 1]);
let range = source_range.to_lsp_range(&current_code);
Ok(Some(vec![TextEdit {
@ -572,6 +568,43 @@ impl LanguageServer for Backend {
range,
}]))
}
async fn rename(&self, params: RenameParams) -> RpcResult<Option<WorkspaceEdit>> {
let filename = params.text_document_position.text_document.uri.to_string();
let Some(current_code) = self.current_code_map.get(&filename) else {
return Ok(None);
};
// Parse the ast.
// I don't know if we need to do this again since it should be updated in the context.
// But I figure better safe than sorry since this will write back out to the file.
let tokens = crate::tokeniser::lexer(&current_code);
let parser = crate::parser::Parser::new(tokens);
let Ok(mut ast) = parser.ast() else {
return Ok(None);
};
// Let's convert the position to a character index.
let pos = position_to_char_index(params.text_document_position.position, &current_code);
// Now let's perform the rename on the ast.
ast.rename_symbol(&params.new_name, pos);
// Now recast it.
let recast = ast.recast(&Default::default(), 0);
let source_range = SourceRange([0, current_code.len() - 1]);
let range = source_range.to_lsp_range(&current_code);
Ok(Some(WorkspaceEdit {
changes: Some(HashMap::from([(
params.text_document_position.text_document.uri,
vec![TextEdit {
new_text: recast,
range,
}],
)])),
document_changes: None,
change_annotations: None,
}))
}
}
/// Get completions from our stdlib.

View File

@ -103,12 +103,12 @@ impl<'a> Args<'a> {
}
fn make_user_val_from_json(&self, j: serde_json::Value) -> Result<MemoryItem, KclError> {
Ok(MemoryItem::UserVal {
Ok(MemoryItem::UserVal(crate::executor::UserVal {
value: j,
meta: vec![Metadata {
source_range: self.source_range,
}],
})
}))
}
fn make_user_val_from_f64(&self, f: f64) -> Result<MemoryItem, KclError> {

View File

@ -382,6 +382,20 @@ fn inner_angled_line(
},
};
args.send_modeling_cmd(
id,
ModelingCmd::ExtendPath {
path: sketch_group.id,
segment: kittycad::types::PathSegment::Line {
end: Point3D {
x: to[0],
y: to[1],
z: 0.0,
},
},
},
)?;
let mut new_sketch_group = sketch_group.clone();
new_sketch_group.value.push(current_path);
Ok(new_sketch_group)

View File

@ -206,8 +206,8 @@ fn is_block_comment(character: &str) -> bool {
BLOCKCOMMENT.is_match(character)
}
fn match_first(str: &str, regex: &Regex) -> Option<String> {
regex.find(str).map(|the_match| the_match.as_str().to_string())
fn match_first(s: &str, regex: &Regex) -> Option<String> {
regex.find(s).map(|the_match| the_match.as_str().to_string())
}
fn make_token(token_type: TokenType, value: &str, start: usize) -> Token {
@ -219,8 +219,8 @@ fn make_token(token_type: TokenType, value: &str, start: usize) -> Token {
}
}
fn return_token_at_index(str: &str, start_index: usize) -> Option<Token> {
let str_from_index = &str[start_index..];
fn return_token_at_index(s: &str, start_index: usize) -> Option<Token> {
let str_from_index = &s.chars().skip(start_index).collect::<String>();
if is_string(str_from_index) {
return Some(make_token(
TokenType::String,
@ -348,21 +348,22 @@ fn return_token_at_index(str: &str, start_index: usize) -> Option<Token> {
None
}
pub fn lexer(str: &str) -> Vec<Token> {
fn recursively_tokenise(str: &str, current_index: usize, previous_tokens: Vec<Token>) -> Vec<Token> {
if current_index >= str.len() {
return previous_tokens;
}
let token = return_token_at_index(str, current_index);
let Some(token) = token else {
return recursively_tokenise(str, current_index + 1, previous_tokens);
};
let mut new_tokens = previous_tokens;
let token_length = token.value.len();
new_tokens.push(token);
recursively_tokenise(str, current_index + token_length, new_tokens)
fn recursively_tokenise(s: &str, current_index: usize, previous_tokens: Vec<Token>) -> Vec<Token> {
if current_index >= s.len() {
return previous_tokens;
}
recursively_tokenise(str, 0, Vec::new())
let token = return_token_at_index(s, current_index);
let Some(token) = token else {
return recursively_tokenise(s, current_index + 1, previous_tokens);
};
let mut new_tokens = previous_tokens;
let token_length = token.value.len();
new_tokens.push(token);
recursively_tokenise(s, current_index + token_length, new_tokens)
}
pub fn lexer(s: &str) -> Vec<Token> {
recursively_tokenise(s, 0, Vec::new())
}
#[cfg(test)]

View File

@ -76,7 +76,8 @@ pub fn recast_wasm(json_str: &str) -> Result<JsValue, JsError> {
let program: kcl_lib::abstract_syntax_tree_types::Program =
serde_json::from_str(json_str).map_err(JsError::from)?;
let result = program.recast("", false);
// Use the default options until we integrate into the UI the ability to change them.
let result = program.recast(&Default::default(), 0);
Ok(JsValue::from_serde(&result)?)
}

View File

@ -0,0 +1,98 @@
use anyhow::Result;
/// Executes a kcl program and takes a snapshot of the result.
/// This returns the bytes of the snapshot.
async fn execute_and_snapshot(code: &str) -> Result<image::DynamicImage> {
let user_agent = concat!(env!("CARGO_PKG_NAME"), ".rs/", env!("CARGO_PKG_VERSION"),);
let http_client = reqwest::Client::builder()
.user_agent(user_agent)
// For file conversions we need this to be long.
.timeout(std::time::Duration::from_secs(600))
.connect_timeout(std::time::Duration::from_secs(60));
let ws_client = reqwest::Client::builder()
.user_agent(user_agent)
// For file conversions we need this to be long.
.timeout(std::time::Duration::from_secs(600))
.connect_timeout(std::time::Duration::from_secs(60))
.tcp_keepalive(std::time::Duration::from_secs(600))
.http1_only();
let token = std::env::var("KITTYCAD_API_TOKEN").expect("KITTYCAD_API_TOKEN not set");
// Create the client.
let client = kittycad::Client::new_from_reqwest(token, http_client, ws_client);
let ws = client
.modeling()
.commands_ws(None, None, None, None, Some(false))
.await?;
// Create a temporary file to write the output to.
let output_file = std::env::temp_dir().join(format!("kcl_output_{}.png", uuid::Uuid::new_v4()));
let tokens = kcl_lib::tokeniser::lexer(code);
let parser = kcl_lib::parser::Parser::new(tokens);
let program = parser.ast()?;
let mut mem: kcl_lib::executor::ProgramMemory = Default::default();
let mut engine = kcl_lib::engine::EngineConnection::new(
ws,
std::env::temp_dir().display().to_string().as_str(),
output_file.display().to_string().as_str(),
)
.await?;
let _ = kcl_lib::executor::execute(program, &mut mem, kcl_lib::executor::BodyType::Root, &mut engine)?;
// Send a snapshot request to the engine.
engine.send_modeling_cmd(
uuid::Uuid::new_v4(),
kcl_lib::executor::SourceRange::default(),
kittycad::types::ModelingCmd::TakeSnapshot {
format: kittycad::types::ImageFormat::Png,
},
)?;
// Wait for the snapshot to be taken.
engine.wait_for_snapshot().await;
// Read the output file.
let actual = image::io::Reader::open(output_file).unwrap().decode().unwrap();
Ok(actual)
}
#[tokio::test(flavor = "multi_thread")]
async fn test_execute_with_function_sketch() {
let code = r#"const box = (h, l, w) => {
const myBox = startSketchAt([0,0])
|> line([0, l], %)
|> line([w, 0], %)
|> line([0, -l], %)
|> close(%)
|> extrude(h, %)
return myBox
}
const fnBox = box(3, 6, 10)
show(fnBox)"#;
let result = execute_and_snapshot(code).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/function_sketch.png", &result, 1.0);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_execute_with_angled_line() {
let code = r#"const part001 = startSketchAt([4.83, 12.56])
|> line([15.1, 2.48], %)
|> line({ to: [3.15, -9.85], tag: 'seg01' }, %)
|> line([-15.17, -4.1], %)
|> angledLine([segAng('seg01', %), 12.35], %)
|> line([-13.02, 10.03], %)
|> close(%)
|> extrude(4, %)
show(part001)"#;
let result = execute_and_snapshot(code).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/angled_line.png", &result, 1.0);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

@ -1530,10 +1530,10 @@
resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz#08d6c5e20cf7e4cc02fd181c4b0c225cd31dbb60"
integrity sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==
"@kittycad/lib@^0.0.35":
version "0.0.35"
resolved "https://registry.yarnpkg.com/@kittycad/lib/-/lib-0.0.35.tgz#bde8868048f9fd53f8309e7308aeba622898b935"
integrity sha512-qM8AyP2QUlDfPWNxb1Fs/Pq9AebGVDN1OHjByxbGomKCy0jFdN2TsyDdhQH/CAZGfBCgPEfr5bq6rkUBGSXcNw==
"@kittycad/lib@^0.0.37":
version "0.0.37"
resolved "https://registry.yarnpkg.com/@kittycad/lib/-/lib-0.0.37.tgz#ec4f6c4fb5d06402a19339f3374036b6582d2265"
integrity sha512-P8p9FeLV79/0Lfd0RioBta1drzhmpROnU4YV38+zsAA4LhibQCTjeekRkxVvHztGumPxz9pPsAeeLJyuz2RWKQ==
dependencies:
node-fetch "3.3.2"
openapi-types "^12.0.0"