Compare commits
32 Commits
achalmers/
...
franknoiro
| Author | SHA1 | Date | |
|---|---|---|---|
| e2e22a7ad8 | |||
| 3b15bc12f7 | |||
| 8eedee328b | |||
| 49b321feb5 | |||
| 35b5ad7d9b | |||
| 8fad9ef3c2 | |||
| b257b202c3 | |||
| c6af62797d | |||
| 16a9acad56 | |||
| 8a80a88ad3 | |||
| 71d1bb70ef | |||
| 4853872614 | |||
| 1ca5204a1a | |||
| 7baed0b5bd | |||
| e4969857bd | |||
| 9b7cc7afa4 | |||
| 714917429e | |||
| 5af9c6b22d | |||
| 396a994fe6 | |||
| 872da51da5 | |||
| 05cd8cfec9 | |||
| 2a02f6e039 | |||
| 5b90686e5e | |||
| 298269d117 | |||
| b379f6518f | |||
| 6b22c8789d | |||
| cb4683e70b | |||
| 0a020d9959 | |||
| 7aae3dccdc | |||
| 818bf96d0b | |||
| 03bc2eaf22 | |||
| 8ad1476c13 |
83
.github/workflows/ci.yml
vendored
@ -7,6 +7,10 @@ on:
|
||||
- main
|
||||
release:
|
||||
types: [published]
|
||||
schedule:
|
||||
- cron: '0 4 * * *'
|
||||
# Daily at 04:00 AM UTC
|
||||
# Will checkout the last commit from the default branch (main as of 2023-10-04)
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
@ -42,8 +46,6 @@ jobs:
|
||||
|
||||
build-test-web:
|
||||
runs-on: ubuntu-20.04
|
||||
outputs:
|
||||
version: ${{ steps.export_version.outputs.version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@ -66,11 +68,37 @@ jobs:
|
||||
|
||||
- run: yarn test:cov
|
||||
|
||||
prepare-json-files:
|
||||
runs-on: ubuntu-20.04 # seperate job on Ubuntu for easy string manipulations (compared to Windows)
|
||||
outputs:
|
||||
version: ${{ steps.export_version.outputs.version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Set nightly version
|
||||
if: github.event_name == 'schedule'
|
||||
run: |
|
||||
VERSION=$(date +'%-y.%-m.%-d') yarn bump-jsons
|
||||
echo "$(jq --arg url 'https://dl.kittycad.io/releases/modeling-app/test/nightly/last_update.json' \
|
||||
'.tauri.updater.endpoints[]=$url' src-tauri/tauri.conf.json --indent 2)" > src-tauri/tauri.conf.json
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: github.event_name == 'schedule'
|
||||
with:
|
||||
path: |
|
||||
package.json
|
||||
src-tauri/tauri.conf.json
|
||||
|
||||
- id: export_version
|
||||
run: echo "version=`cat package.json | jq -r '.version'`" >> "$GITHUB_OUTPUT"
|
||||
|
||||
build-apps:
|
||||
needs: [check-format, build-test-web, check-types]
|
||||
needs: [check-format, build-test-web, prepare-json-files, check-types]
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
@ -78,6 +106,15 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
|
||||
- name: Copy updated .json files
|
||||
if: github.event_name == 'schedule'
|
||||
run: |
|
||||
ls -l artifact
|
||||
cp artifact/package.json package.json
|
||||
cp artifact/src-tauri/tauri.conf.json src-tauri/tauri.conf.json
|
||||
|
||||
- name: install ubuntu system dependencies
|
||||
if: matrix.os == 'ubuntu-20.04'
|
||||
run: |
|
||||
@ -104,7 +141,8 @@ jobs:
|
||||
with:
|
||||
workspaces: './src/wasm-lib'
|
||||
|
||||
- name: wasm prep
|
||||
- name: wasm prep - linux/mac
|
||||
if: matrix.os != 'windows-latest'
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir src/wasm-lib/pkg; cd src/wasm-lib
|
||||
@ -112,6 +150,15 @@ jobs:
|
||||
cd ../../
|
||||
cp src/wasm-lib/pkg/wasm_lib_bg.wasm public
|
||||
|
||||
- name: wasm prep - windows
|
||||
if: matrix.os == 'windows-latest'
|
||||
shell: pwsh
|
||||
run: |
|
||||
mkdir src\wasm-lib\pkg; cd src\wasm-lib
|
||||
npx wasm-pack build --target web --out-dir pkg
|
||||
cd ..\..\
|
||||
cp src\wasm-lib\pkg\wasm_lib_bg.wasm public
|
||||
|
||||
- name: Fix format
|
||||
run: yarn fmt
|
||||
|
||||
@ -156,6 +203,7 @@ jobs:
|
||||
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
with:
|
||||
args: ${{ matrix.os == 'macos-latest' && '--target universal-apple-darwin' || '' }}
|
||||
|
||||
@ -165,12 +213,14 @@ jobs:
|
||||
|
||||
publish-apps-release:
|
||||
runs-on: ubuntu-20.04
|
||||
if: github.event_name == 'release'
|
||||
needs: [build-test-web, build-apps]
|
||||
if: ${{ github.event_name == 'release' || github.event_name == 'schedule' }}
|
||||
needs: [build-test-web, prepare-json-files, build-apps]
|
||||
env:
|
||||
VERSION_NO_V: ${{ needs.build-test-web.outputs.version }}
|
||||
PUB_DATE: ${{ github.event.release.created_at }}
|
||||
NOTES: ${{ github.event.release.body }}
|
||||
VERSION_NO_V: ${{ needs.prepare-json-files.outputs.version }}
|
||||
VERSION: ${{ github.event_name == 'release' && format('v{0}', needs.prepare-json-files.outputs.version) || needs.prepare-json-files.outputs.version }}
|
||||
PUB_DATE: ${{ github.event_name == 'release' && github.event.release.created_at || github.event.repository.updated_at }}
|
||||
NOTES: ${{ github.event_name == 'release' && github.event.release.body || format('Nightly build, commit {0}', github.sha) }}
|
||||
BUCKET_DIR: ${{ github.event_name == 'release' && 'dl.kittycad.io/releases/modeling-app' || 'dl.kittycad.io/releases/modeling-app/nightly' }}
|
||||
steps:
|
||||
- uses: actions/download-artifact@v3
|
||||
|
||||
@ -180,9 +230,9 @@ jobs:
|
||||
DARWIN_SIG=`cat artifact/macos/*.app.tar.gz.sig`
|
||||
LINUX_SIG=`cat artifact/appimage/*.AppImage.tar.gz.sig`
|
||||
WINDOWS_SIG=`cat artifact/msi/*.msi.zip.sig`
|
||||
RELEASE_DIR=https://dl.kittycad.io/releases/modeling-app/v${VERSION_NO_V}
|
||||
RELEASE_DIR=https://${BUCKET_DIR}/${VERSION}
|
||||
jq --null-input \
|
||||
--arg version "v${VERSION_NO_V}" \
|
||||
--arg version "${VERSION}" \
|
||||
--arg pub_date "${PUB_DATE}" \
|
||||
--arg notes "${NOTES}" \
|
||||
--arg darwin_sig "$DARWIN_SIG" \
|
||||
@ -218,9 +268,9 @@ jobs:
|
||||
|
||||
- name: Generate the download static endpoint
|
||||
run: |
|
||||
RELEASE_DIR=https://dl.kittycad.io/releases/modeling-app/v${VERSION_NO_V}
|
||||
RELEASE_DIR=https://${BUCKET_DIR}/${VERSION}
|
||||
jq --null-input \
|
||||
--arg version "v${VERSION_NO_V}" \
|
||||
--arg version "${VERSION}" \
|
||||
--arg pub_date "${PUB_DATE}" \
|
||||
--arg notes "${NOTES}" \
|
||||
--arg darwin_url "$RELEASE_DIR/dmg/KittyCAD%20Modeling_${VERSION_NO_V}_universal.dmg" \
|
||||
@ -260,21 +310,22 @@ jobs:
|
||||
path: artifact
|
||||
glob: '*/*itty*'
|
||||
parent: false
|
||||
destination: dl.kittycad.io/releases/modeling-app/v${{ env.VERSION_NO_V }}
|
||||
destination: ${{ env.BUCKET_DIR }}/${{ env.VERSION }}
|
||||
|
||||
- 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
|
||||
destination: ${{ env.BUCKET_DIR }}
|
||||
|
||||
- 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
|
||||
destination: ${{ env.BUCKET_DIR }}
|
||||
|
||||
- name: Upload release files to Github
|
||||
if: ${{ github.event_name == 'release' }}
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: artifact/*/*itty*
|
||||
|
||||
@ -29,6 +29,7 @@ The 3D view in KittyCAD Modeling App is just a video stream from our hosted geom
|
||||
- [React](https://react.dev/)
|
||||
- [Headless UI](https://headlessui.com/)
|
||||
- [TailwindCSS](https://tailwindcss.com/)
|
||||
- [XState](https://xstate.js.org/)
|
||||
- Networking
|
||||
- WebSockets (via [KittyCAD TS client](https://github.com/KittyCAD/kittycad.ts))
|
||||
- Code Editor
|
||||
@ -56,7 +57,7 @@ yarn install
|
||||
followed by:
|
||||
|
||||
```
|
||||
yarn build:wasm
|
||||
yarn build:wasm-dev
|
||||
```
|
||||
|
||||
That will build the WASM binary and put in the `public` dir (though gitignored)
|
||||
@ -97,7 +98,7 @@ but you will need to have install ffmpeg prior to.
|
||||
|
||||
## Tauri
|
||||
|
||||
To spin up up tauri dev, `yarn install` and `yarn build:wasm` need to have been done before hand then
|
||||
To spin up up tauri dev, `yarn install` and `yarn build:wasm-dev` need to have been done before hand then
|
||||
|
||||
```
|
||||
yarn tauri dev
|
||||
|
||||
1339
docs/kcl/std.json
1412
docs/kcl/std.md
@ -16,7 +16,7 @@
|
||||
"@react-hook/resize-observer": "^1.2.6",
|
||||
"@replit/codemirror-interact": "^6.3.0",
|
||||
"@sentry/react": "^7.65.0",
|
||||
"@tauri-apps/api": "^1.3.0",
|
||||
"@tauri-apps/api": "^1.5.0",
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
"@testing-library/react": "^13.0.0",
|
||||
"@testing-library/user-event": "^13.2.1",
|
||||
@ -73,6 +73,7 @@
|
||||
"simpleserver": "yarn pretest && http-server ./public --cors -p 3000",
|
||||
"fmt": "prettier --write ./src",
|
||||
"fmt-check": "prettier --check ./src",
|
||||
"build:wasm-dev": "(cd src/wasm-lib && wasm-pack build --dev --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt",
|
||||
"build:wasm": "(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",
|
||||
"build:wasm-clean": "yarn wasm-prep && yarn build:wasm",
|
||||
"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\"",
|
||||
@ -101,7 +102,7 @@
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||
"@babel/preset-env": "^7.22.9",
|
||||
"@tauri-apps/cli": "^1.3.1",
|
||||
"@tauri-apps/cli": "^1.5.0",
|
||||
"@types/crypto-js": "^4.1.1",
|
||||
"@types/debounce-promise": "^3.1.6",
|
||||
"@types/isomorphic-fetch": "^0.0.36",
|
||||
|
||||
3
public/Icon/Icon/Projects/Create File.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M4 3H4.5H11H11.2071L11.3536 3.14645L15.8536 7.64646L16 7.7929V8.00001V11.3773C15.6992 11.1362 15.3628 10.9376 15 10.7908V8.50001H11H10.5V8.00001V4H5V16H9.79076C9.93763 16.3628 10.1362 16.6992 10.3773 17H4.5H4V16.5V3.5V3ZM11.5 4.70711L14.2929 7.50001H11.5V4.70711ZM13 12V14H11V15H13V17H14V15H16V14H14V12H13Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 475 B |
3
public/Icon/Icon/Projects/Create Folder.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.5 3.5H4H7H7.16667L7.3 3.6L9.16667 5H16H16.5V5.5V7.5V10.3773C16.1992 10.1362 15.8628 9.93763 15.5 9.79076V8H4.5V15.5H10.5351C10.7529 15.8764 11.0302 16.2141 11.3542 16.5H4H3.5V16V7.5V4V3.5ZM4.5 4.5V7H15.5V6H9H8.83333L8.7 5.9L6.83333 4.5H4.5ZM13.5 11V13H11.5V14H13.5V16H14.5V14H16.5V13H14.5V11H13.5Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 469 B |
3
public/Icon/Icon/Projects/File.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11 3.5H4.5V16.5H15.5V8.00001M11 3.5L15.5 8.00001M11 3.5V8.00001H15.5" stroke="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 200 B |
3
public/kcl-icon.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M40 0H0V40H40V0ZM7.34715 27.2143V15.6577L2.976 15.987V36.7949H7.34715V32.0645L8.00582 31.5256C8.24533 31.326 8.47487 31.1264 8.69442 30.9268L12.1075 36.7949H17.0475C16.1893 35.3978 15.311 33.9906 14.4128 32.5735C13.5346 31.1563 12.6664 29.7392 11.8081 28.3221L15.8499 24.9389C15.4308 24.4399 15.0017 23.931 14.5625 23.412L13.3051 21.8552L7.34715 27.2143ZM22.2581 26.6754C22.8769 25.9169 23.6753 25.5377 24.6533 25.5377C25.272 25.5377 25.8309 25.6175 26.3299 25.7772C26.8289 25.9169 27.4177 26.1465 28.0963 26.4658L29.3238 23.3521C28.5853 22.7933 27.7371 22.4041 26.779 22.1845C25.8409 21.9649 25.0625 21.8552 24.4437 21.8552C22.0885 21.8552 20.2223 22.5537 18.845 23.9509C17.4878 25.3281 16.8092 27.1944 16.8092 29.5496C16.8092 31.9048 17.4878 33.7611 18.845 35.1183C20.2223 36.4756 22.0885 37.1542 24.4437 37.1542C25.0625 37.1542 25.8509 37.0444 26.8089 36.8249C27.767 36.6053 28.6053 36.2161 29.3238 35.6572L28.0963 32.5435C27.4177 32.8629 26.8289 33.0924 26.3299 33.2321C25.8309 33.3718 25.272 33.4417 24.6533 33.4417C23.6753 33.4417 22.8769 33.0924 22.2581 32.3938C21.6594 31.6753 21.36 30.7272 21.36 29.5496C21.36 28.372 21.6594 27.4139 22.2581 26.6754ZM36.2796 36.7949V15.6577L31.9085 15.987V36.7949H36.2796Z" fill="#D0FF00"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
22
src-tauri/Cargo.lock
generated
@ -1658,9 +1658,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kittycad"
|
||||
version = "0.2.31"
|
||||
version = "0.2.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "539b323537b877fc8dd130362b8f1af9af8051c19208bb8bfd816ab7c330f2bb"
|
||||
checksum = "d341a81a4dfef43460d395c87d86c17e24affb96db0e7f4a35e8688f0e092344"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@ -3208,9 +3208,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.188"
|
||||
version = "1.0.189"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e"
|
||||
checksum = "8e422a44e74ad4001bdc8eede9a4570ab52f71190e9c076d14369f38b9200537"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
@ -3226,9 +3226,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.188"
|
||||
version = "1.0.189"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2"
|
||||
checksum = "1e48d1f918009ce3145511378cf68d613e3b3d9137d67272562080d68a2b32d5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -3712,9 +3712,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri"
|
||||
version = "1.5.1"
|
||||
version = "1.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0238c5063bf9613054149a1b6bce4935922e532b7d8211f36989a490a79806be"
|
||||
checksum = "9bfe673cf125ef364d6f56b15e8ce7537d9ca7e4dae1cf6fbbdeed2e024db3d9"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64 0.21.2",
|
||||
@ -3828,7 +3828,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "tauri-plugin-fs-extra"
|
||||
version = "0.0.0"
|
||||
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#9f27e6e4415ddf6c40f846d50c0d95c768cded77"
|
||||
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#9b20f28d747f6ec3ba5a80bfcd5edc1d573b4c90"
|
||||
dependencies = [
|
||||
"log",
|
||||
"serde",
|
||||
@ -4007,9 +4007,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.32.0"
|
||||
version = "1.33.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9"
|
||||
checksum = "4f38200e3ef7995e5ef13baec2f432a6da0aa9ac495b2c0e8f3b7eec2c92d653"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"bytes",
|
||||
|
||||
@ -16,13 +16,13 @@ tauri-build = { version = "1.5.0", features = [] }
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
kittycad = "0.2.31"
|
||||
kittycad = "0.2.33"
|
||||
oauth2 = "4.4.2"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
tauri = { version = "1.5.1", features = [ "os-all", "dialog-all", "fs-all", "http-request", "path-all", "shell-open", "shell-open-api", "updater", "devtools"] }
|
||||
tauri = { version = "1.5.2", features = [ "os-all", "dialog-all", "fs-all", "http-request", "path-all", "shell-open", "shell-open-api", "updater", "devtools"] }
|
||||
tauri-plugin-fs-extra = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
|
||||
tokio = { version = "1.32.0", features = ["time"] }
|
||||
tokio = { version = "1.33.0", features = ["time"] }
|
||||
toml = "0.8.2"
|
||||
|
||||
[features]
|
||||
|
||||
12
src/App.tsx
@ -35,7 +35,7 @@ import { kclManager } from 'lang/KclSinglton'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
|
||||
export function App() {
|
||||
const { code: loadedCode, project } = useLoaderData() as IndexLoaderData
|
||||
const { code: loadedCode, project, file } = useLoaderData() as IndexLoaderData
|
||||
|
||||
useHotKeyListener()
|
||||
const {
|
||||
@ -86,7 +86,13 @@ export function App() {
|
||||
// on mount, and overwrite any locally-stored code
|
||||
useEffect(() => {
|
||||
if (isTauri() && loadedCode !== null) {
|
||||
kclManager.setCode(loadedCode)
|
||||
if (kclManager.engineCommandManager.engineConnection?.isReady()) {
|
||||
// If the engine is ready, promptly execute the loaded code
|
||||
kclManager.setCodeAndExecute(loadedCode)
|
||||
} else {
|
||||
// Otherwise, just set the code and wait for the connection to complete
|
||||
kclManager.setCode(loadedCode)
|
||||
}
|
||||
}
|
||||
return () => {
|
||||
// Clear code on unmount if in desktop app
|
||||
@ -182,7 +188,7 @@ export function App() {
|
||||
paneOpacity +
|
||||
(buttonDownInStream ? ' pointer-events-none' : '')
|
||||
}
|
||||
project={project}
|
||||
project={{ project, file }}
|
||||
enableMenu={true}
|
||||
/>
|
||||
<ModalContainer />
|
||||
|
||||
@ -42,6 +42,7 @@ import { TEST, VITE_KC_SENTRY_DSN } from './env'
|
||||
import * as Sentry from '@sentry/react'
|
||||
import ModelingMachineProvider from 'components/ModelingMachineProvider'
|
||||
import { KclContextProvider } from 'lang/KclSinglton'
|
||||
import FileMachineProvider from 'components/FileMachineProvider'
|
||||
|
||||
if (VITE_KC_SENTRY_DSN && !TEST) {
|
||||
Sentry.init({
|
||||
@ -101,10 +102,11 @@ export const BROWSER_FILE_NAME = 'new'
|
||||
export type IndexLoaderData = {
|
||||
code: string | null
|
||||
project?: ProjectWithEntryPointMetadata
|
||||
file?: FileEntry
|
||||
}
|
||||
|
||||
export type ProjectWithEntryPointMetadata = FileEntry & {
|
||||
entrypoint_metadata: Metadata
|
||||
entrypointMetadata: Metadata
|
||||
}
|
||||
export type HomeLoaderData = {
|
||||
projects: ProjectWithEntryPointMetadata[]
|
||||
@ -143,11 +145,13 @@ const router = createBrowserRouter(
|
||||
element: (
|
||||
<Auth>
|
||||
<Outlet />
|
||||
<KclContextProvider>
|
||||
<ModelingMachineProvider>
|
||||
<App />
|
||||
</ModelingMachineProvider>
|
||||
</KclContextProvider>
|
||||
<FileMachineProvider>
|
||||
<KclContextProvider>
|
||||
<ModelingMachineProvider>
|
||||
<App />
|
||||
</ModelingMachineProvider>
|
||||
</KclContextProvider>
|
||||
</FileMachineProvider>
|
||||
{!isTauri() && import.meta.env.PROD && <DownloadAppBanner />}
|
||||
</Auth>
|
||||
),
|
||||
@ -177,21 +181,41 @@ const router = createBrowserRouter(
|
||||
)
|
||||
}
|
||||
|
||||
const defaultDir = persistedSettings.defaultDirectory || ''
|
||||
|
||||
if (params.id && params.id !== BROWSER_FILE_NAME) {
|
||||
const decodedId = decodeURIComponent(params.id)
|
||||
const projectAndFile = decodedId.replace(defaultDir + '/', '')
|
||||
const firstSlashIndex = projectAndFile.indexOf('/')
|
||||
const projectName = projectAndFile.slice(0, firstSlashIndex)
|
||||
const projectPath = defaultDir + '/' + projectName
|
||||
const currentFileName = projectAndFile.slice(firstSlashIndex + 1)
|
||||
|
||||
if (firstSlashIndex === -1 || !currentFileName)
|
||||
return redirect(
|
||||
`${paths.FILE}/${encodeURIComponent(
|
||||
`${params.id}/${PROJECT_ENTRYPOINT}`
|
||||
)}`
|
||||
)
|
||||
|
||||
// Note that PROJECT_ENTRYPOINT is hardcoded until we support multiple files
|
||||
const code = await readTextFile(params.id + '/' + PROJECT_ENTRYPOINT)
|
||||
const entrypoint_metadata = await metadata(
|
||||
params.id + '/' + PROJECT_ENTRYPOINT
|
||||
const code = await readTextFile(decodedId)
|
||||
const entrypointMetadata = await metadata(
|
||||
projectPath + '/' + PROJECT_ENTRYPOINT
|
||||
)
|
||||
const children = await readDir(params.id)
|
||||
const children = await readDir(projectPath, { recursive: true })
|
||||
|
||||
return {
|
||||
code,
|
||||
project: {
|
||||
name: params.id.slice(params.id.lastIndexOf('/') + 1),
|
||||
path: params.id,
|
||||
name: projectName,
|
||||
path: projectPath,
|
||||
children,
|
||||
entrypoint_metadata,
|
||||
entrypointMetadata,
|
||||
},
|
||||
file: {
|
||||
name: currentFileName,
|
||||
path: params.id,
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -245,7 +269,7 @@ const router = createBrowserRouter(
|
||||
)
|
||||
const projects = await Promise.all(
|
||||
projectsNoMeta.map(async (p) => ({
|
||||
entrypoint_metadata: await metadata(
|
||||
entrypointMetadata: await metadata(
|
||||
p.path + '/' + PROJECT_ENTRYPOINT
|
||||
),
|
||||
...p,
|
||||
|
||||
@ -1,26 +1,13 @@
|
||||
import { useStore, toolTips, ToolTip } from './useStore'
|
||||
import { extrudeSketch, sketchOnExtrudedFace } from './lang/modifyAst'
|
||||
import { getNodePathFromSourceRange } from './lang/queryAst'
|
||||
// import { HorzVert } from './components/Toolbar/HorzVert'
|
||||
// import { RemoveConstrainingValues } from './components/Toolbar/RemoveConstrainingValues'
|
||||
// import { EqualLength } from './components/Toolbar/EqualLength'
|
||||
// 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 { SetAbsDistance } from './components/Toolbar/SetAbsDistance'
|
||||
// import { SetAngleBetween } from './components/Toolbar/SetAngleBetween'
|
||||
import { ToolTip } from './useStore'
|
||||
import { Fragment, WheelEvent, useRef, useMemo } from 'react'
|
||||
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 { isCursorInSketchCommandRange } from 'lang/util'
|
||||
import { ActionIcon } from 'components/ActionIcon'
|
||||
import { engineCommandManager } from './lang/std/engineConnection'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
import { kclManager } from 'lang/KclSinglton'
|
||||
|
||||
export const sketchButtonClassnames = {
|
||||
background:
|
||||
@ -178,24 +165,6 @@ export const Toolbar = () => {
|
||||
Extrude
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* <HorzVert horOrVert="horizontal" />
|
||||
<HorzVert horOrVert="vertical" />
|
||||
<EqualLength />
|
||||
<EqualAngle />
|
||||
<SetHorzVertDistance buttonType="alignEndsVertically" />
|
||||
<SetHorzVertDistance buttonType="setHorzDistance" />
|
||||
<SetAbsDistance buttonType="snapToYAxis" />
|
||||
<SetAbsDistance buttonType="xAbs" />
|
||||
<SetHorzVertDistance buttonType="alignEndsHorizontally" />
|
||||
<SetAbsDistance buttonType="snapToXAxis" />
|
||||
<SetHorzVertDistance buttonType="setVertDistance" />
|
||||
<SetAbsDistance buttonType="yAbs" />
|
||||
<SetAngleLength angleOrLength="setAngle" />
|
||||
<SetAngleLength angleOrLength="setLength" />
|
||||
<Intersect />
|
||||
<RemoveConstrainingValues />
|
||||
<SetAngleBetween /> */}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { Toolbar } from '../Toolbar'
|
||||
import UserSidebarMenu from './UserSidebarMenu'
|
||||
import { ProjectWithEntryPointMetadata } from '../Router'
|
||||
import { IndexLoaderData } from '../Router'
|
||||
import ProjectSidebarMenu from './ProjectSidebarMenu'
|
||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||
import styles from './AppHeader.module.css'
|
||||
@ -8,7 +8,7 @@ import { NetworkHealthIndicator } from './NetworkHealthIndicator'
|
||||
|
||||
interface AppHeaderProps extends React.PropsWithChildren {
|
||||
showToolbar?: boolean
|
||||
project?: ProjectWithEntryPointMetadata
|
||||
project?: Omit<IndexLoaderData, 'code'>
|
||||
className?: string
|
||||
enableMenu?: boolean
|
||||
}
|
||||
@ -32,7 +32,13 @@ export const AppHeader = ({
|
||||
className
|
||||
}
|
||||
>
|
||||
<ProjectSidebarMenu renderAsLink={!enableMenu} project={project} />
|
||||
{project && (
|
||||
<ProjectSidebarMenu
|
||||
renderAsLink={!enableMenu}
|
||||
project={project.project}
|
||||
file={project.file}
|
||||
/>
|
||||
)}
|
||||
{/* Toolbar if the context deems it */}
|
||||
{showToolbar && (
|
||||
<div className="max-w-lg md:max-w-xl lg:max-w-2xl xl:max-w-4xl 2xl:max-w-5xl">
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
export type CustomIconName =
|
||||
| 'createFile'
|
||||
| 'createFolder'
|
||||
| 'equal'
|
||||
| 'exit'
|
||||
| 'extrude'
|
||||
| 'file'
|
||||
| 'horizontal'
|
||||
| 'line'
|
||||
| 'move'
|
||||
@ -16,6 +19,38 @@ export const CustomIcon = ({
|
||||
name: CustomIconName
|
||||
} & React.SVGProps<SVGSVGElement>) => {
|
||||
switch (name) {
|
||||
case 'createFile':
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M4 3H4.5H11H11.2071L11.3536 3.14645L15.8536 7.64646L16 7.7929V8.00001V11.3773C15.6992 11.1362 15.3628 10.9376 15 10.7908V8.50001H11H10.5V8.00001V4H5V16H9.79076C9.93763 16.3628 10.1362 16.6992 10.3773 17H4.5H4V16.5V3.5V3ZM11.5 4.70711L14.2929 7.50001H11.5V4.70711ZM13 12V14H11V15H13V17H14V15H16V14H14V12H13Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
case 'createFolder':
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M3.5 3.5H4H7H7.16667L7.3 3.6L9.16667 5H16H16.5V5.5V7.5V10.3773C16.1992 10.1362 15.8628 9.93763 15.5 9.79076V8H4.5V15.5H10.5351C10.7529 15.8764 11.0302 16.2141 11.3542 16.5H4H3.5V16V7.5V4V3.5ZM4.5 4.5V7H15.5V6H9H8.83333L8.7 5.9L6.83333 4.5H4.5ZM13.5 11V13H11.5V14H13.5V16H14.5V14H16.5V13H14.5V11H13.5Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
case 'equal':
|
||||
return (
|
||||
<svg
|
||||
@ -61,6 +96,20 @@ export const CustomIcon = ({
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
case 'file':
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M11 3.5H4.5V16.5H15.5V8.00001M11 3.5L15.5 8.00001M11 3.5V8.00001H15.5"
|
||||
stroke="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
case 'horizontal':
|
||||
return (
|
||||
<svg
|
||||
|
||||
@ -16,8 +16,8 @@ type StorageUnion = ExtractStorageTypes<OutputFormat>
|
||||
interface ExportButtonProps extends React.PropsWithChildren {
|
||||
className?: {
|
||||
button?: string
|
||||
// If we wanted more classname configuration of sub-elements,
|
||||
// put them here
|
||||
icon?: string
|
||||
bg?: string
|
||||
}
|
||||
}
|
||||
|
||||
@ -109,7 +109,11 @@ export const ExportButton = ({ children, className }: ExportButtonProps) => {
|
||||
<ActionButton
|
||||
onClick={openModal}
|
||||
Element="button"
|
||||
icon={{ icon: faFileExport }}
|
||||
icon={{
|
||||
icon: faFileExport,
|
||||
iconClassName: className?.icon,
|
||||
bgClassName: className?.bg,
|
||||
}}
|
||||
className={className?.button}
|
||||
>
|
||||
{children || 'Export'}
|
||||
|
||||
157
src/components/FileMachineProvider.tsx
Normal file
@ -0,0 +1,157 @@
|
||||
import { useMachine } from '@xstate/react'
|
||||
import { useNavigate, useRouteLoaderData } from 'react-router-dom'
|
||||
import { IndexLoaderData, paths } from '../Router'
|
||||
import React, { createContext } from 'react'
|
||||
import { toast } from 'react-hot-toast'
|
||||
import {
|
||||
AnyStateMachine,
|
||||
ContextFrom,
|
||||
EventFrom,
|
||||
InterpreterFrom,
|
||||
Prop,
|
||||
StateFrom,
|
||||
} from 'xstate'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { DEFAULT_FILE_NAME, fileMachine } from 'machines/fileMachine'
|
||||
import {
|
||||
createDir,
|
||||
removeDir,
|
||||
removeFile,
|
||||
renameFile,
|
||||
writeFile,
|
||||
} from '@tauri-apps/api/fs'
|
||||
import { FILE_EXT, readProject } from 'lib/tauriFS'
|
||||
import { isTauri } from 'lib/isTauri'
|
||||
|
||||
type MachineContext<T extends AnyStateMachine> = {
|
||||
state: StateFrom<T>
|
||||
context: ContextFrom<T>
|
||||
send: Prop<InterpreterFrom<T>, 'send'>
|
||||
}
|
||||
|
||||
export const FileContext = createContext(
|
||||
{} as MachineContext<typeof fileMachine>
|
||||
)
|
||||
|
||||
export const FileMachineProvider = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) => {
|
||||
const navigate = useNavigate()
|
||||
const { setCommandBarOpen } = useCommandsContext()
|
||||
const { project } = useRouteLoaderData(paths.FILE) as IndexLoaderData
|
||||
|
||||
const [state, send] = useMachine(fileMachine, {
|
||||
context: {
|
||||
project,
|
||||
selectedDirectory: project,
|
||||
},
|
||||
actions: {
|
||||
navigateToFile: (
|
||||
context: ContextFrom<typeof fileMachine>,
|
||||
event: EventFrom<typeof fileMachine>
|
||||
) => {
|
||||
if (event.data && 'name' in event.data) {
|
||||
setCommandBarOpen(false)
|
||||
navigate(
|
||||
`${paths.FILE}/${encodeURIComponent(
|
||||
context.selectedDirectory + '/' + event.data.name
|
||||
)}`
|
||||
)
|
||||
}
|
||||
},
|
||||
toastSuccess: (_, event) =>
|
||||
event.data && toast.success((event.data || '') + ''),
|
||||
toastError: (_, event) => toast.error((event.data || '') + ''),
|
||||
},
|
||||
services: {
|
||||
readFiles: async (context: ContextFrom<typeof fileMachine>) => {
|
||||
const newFiles = isTauri()
|
||||
? await readProject(context.project.path)
|
||||
: []
|
||||
return {
|
||||
...context.project,
|
||||
children: newFiles,
|
||||
}
|
||||
},
|
||||
createFile: async (
|
||||
context: ContextFrom<typeof fileMachine>,
|
||||
event: EventFrom<typeof fileMachine, 'Create file'>
|
||||
) => {
|
||||
let name = event.data.name.trim() || DEFAULT_FILE_NAME
|
||||
|
||||
if (event.data.makeDir) {
|
||||
await createDir(context.selectedDirectory.path + '/' + name)
|
||||
} else {
|
||||
await writeFile(
|
||||
context.selectedDirectory.path +
|
||||
'/' +
|
||||
name +
|
||||
(name.endsWith(FILE_EXT) ? '' : FILE_EXT),
|
||||
''
|
||||
)
|
||||
}
|
||||
|
||||
return `Successfully created "${name}"`
|
||||
},
|
||||
renameFile: async (
|
||||
context: ContextFrom<typeof fileMachine>,
|
||||
event: EventFrom<typeof fileMachine, 'Rename file'>
|
||||
) => {
|
||||
const { oldName, newName, isDir } = event.data
|
||||
let name = newName ? newName : DEFAULT_FILE_NAME
|
||||
|
||||
await renameFile(
|
||||
context.selectedDirectory.path + '/' + oldName,
|
||||
context.selectedDirectory.path +
|
||||
'/' +
|
||||
name +
|
||||
(name.endsWith(FILE_EXT) || isDir ? '' : FILE_EXT)
|
||||
)
|
||||
return (
|
||||
oldName !== name && `Successfully renamed "${oldName}" to "${name}"`
|
||||
)
|
||||
},
|
||||
deleteFile: async (
|
||||
context: ContextFrom<typeof fileMachine>,
|
||||
event: EventFrom<typeof fileMachine, 'Delete file'>
|
||||
) => {
|
||||
const isDir = !!event.data.children
|
||||
|
||||
if (isDir) {
|
||||
await removeDir(event.data.path, {
|
||||
recursive: true,
|
||||
}).catch((e) => console.error('Error deleting directory', e))
|
||||
} else {
|
||||
await removeFile(event.data.path).catch((e) =>
|
||||
console.error('Error deleting file', e)
|
||||
)
|
||||
}
|
||||
return `Successfully deleted ${isDir ? 'folder' : 'file'} "${
|
||||
event.data.name
|
||||
}"`
|
||||
},
|
||||
},
|
||||
guards: {
|
||||
'Has at least 1 file': (_, event: EventFrom<typeof fileMachine>) => {
|
||||
if (event.type !== 'done.invoke.read-files') return false
|
||||
return !!event?.data?.children && event.data.children.length > 0
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<FileContext.Provider
|
||||
value={{
|
||||
send,
|
||||
state,
|
||||
context: state.context, // just a convenience, can remove if we need to save on memory
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</FileContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export default FileMachineProvider
|
||||
16
src/components/FileTree.module.css
Normal file
@ -0,0 +1,16 @@
|
||||
.folder {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.folder::after {
|
||||
content: '';
|
||||
width: 1px;
|
||||
z-index: -1;
|
||||
@apply absolute top-0 bottom-0;
|
||||
left: calc(var(--indent-line-left, 1rem) + 0.25rem);
|
||||
@apply bg-chalkboard-30;
|
||||
}
|
||||
|
||||
:global(.dark) .folder::after {
|
||||
@apply bg-chalkboard-80;
|
||||
}
|
||||
400
src/components/FileTree.tsx
Normal file
@ -0,0 +1,400 @@
|
||||
import { IndexLoaderData, paths } from 'Router'
|
||||
import { ActionButton } from './ActionButton'
|
||||
import Tooltip from './Tooltip'
|
||||
import { FileEntry } from '@tauri-apps/api/fs'
|
||||
import { Dispatch, useEffect, useRef, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Dialog, Disclosure } from '@headlessui/react'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faChevronRight, faTrashAlt } from '@fortawesome/free-solid-svg-icons'
|
||||
import { useFileContext } from 'hooks/useFileContext'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import { kclManager } from 'lang/KclSinglton'
|
||||
import styles from './FileTree.module.css'
|
||||
import { sortProject } from 'lib/tauriFS'
|
||||
|
||||
function getIndentationCSS(level: number) {
|
||||
return `calc(1rem * ${level + 1})`
|
||||
}
|
||||
|
||||
function RenameForm({
|
||||
fileOrDir,
|
||||
setIsRenaming,
|
||||
level = 0,
|
||||
}: {
|
||||
fileOrDir: FileEntry
|
||||
setIsRenaming: Dispatch<React.SetStateAction<boolean>>
|
||||
level?: number
|
||||
}) {
|
||||
const { send } = useFileContext()
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
function handleRenameSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault()
|
||||
setIsRenaming(false)
|
||||
send({
|
||||
type: 'Rename file',
|
||||
data: {
|
||||
oldName: fileOrDir.name || '',
|
||||
newName: inputRef.current?.value || fileOrDir.name || '',
|
||||
isDir: fileOrDir.children !== undefined,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function handleKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
|
||||
if (e.key === 'Escape') {
|
||||
e.stopPropagation()
|
||||
setIsRenaming(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleRenameSubmit}>
|
||||
<label>
|
||||
<span className="sr-only">Rename file</span>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
autoFocus
|
||||
placeholder={fileOrDir.name}
|
||||
className="w-full py-1 bg-transparent text-chalkboard-100 placeholder:text-chalkboard-70 dark:text-chalkboard-10 dark:placeholder:text-chalkboard-50 focus:outline-none focus:ring-0"
|
||||
onKeyDown={handleKeyDown}
|
||||
onBlur={() => setIsRenaming(false)}
|
||||
style={{ paddingInlineStart: getIndentationCSS(level) }}
|
||||
/>
|
||||
</label>
|
||||
<button className="sr-only" type="submit">
|
||||
Submit
|
||||
</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
function DeleteConfirmationDialog({
|
||||
fileOrDir,
|
||||
setIsOpen,
|
||||
}: {
|
||||
fileOrDir: FileEntry
|
||||
setIsOpen: Dispatch<React.SetStateAction<boolean>>
|
||||
}) {
|
||||
const { send } = useFileContext()
|
||||
return (
|
||||
<Dialog
|
||||
open={true}
|
||||
onClose={() => setIsOpen(false)}
|
||||
className="relative z-50"
|
||||
>
|
||||
<div className="fixed inset-0 bg-chalkboard-110/80 grid place-content-center">
|
||||
<Dialog.Panel className="rounded p-4 bg-chalkboard-10 dark:bg-chalkboard-100 border border-destroy-80 max-w-2xl">
|
||||
<Dialog.Title as="h2" className="text-2xl font-bold mb-4">
|
||||
Delete {fileOrDir.children !== undefined ? 'Folder' : 'File'}
|
||||
</Dialog.Title>
|
||||
<Dialog.Description className="my-6">
|
||||
This will permanently delete "{fileOrDir.name || 'this file'}"
|
||||
{fileOrDir.children !== undefined
|
||||
? ' and all of its contents. '
|
||||
: '. '}
|
||||
This action cannot be undone.
|
||||
</Dialog.Description>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={async () => {
|
||||
send({ type: 'Delete file', data: fileOrDir })
|
||||
setIsOpen(false)
|
||||
}}
|
||||
icon={{
|
||||
icon: faTrashAlt,
|
||||
bgClassName: 'bg-destroy-80',
|
||||
iconClassName:
|
||||
'text-destroy-20 group-hover:text-destroy-10 hover:text-destroy-10 dark:text-destroy-20 dark:group-hover:text-destroy-10 dark:hover:text-destroy-10',
|
||||
}}
|
||||
className="hover:border-destroy-40 dark:hover:border-destroy-40"
|
||||
>
|
||||
Delete
|
||||
</ActionButton>
|
||||
<ActionButton Element="button" onClick={() => setIsOpen(false)}>
|
||||
Cancel
|
||||
</ActionButton>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</div>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
const FileTreeItem = ({
|
||||
project,
|
||||
currentFile,
|
||||
fileOrDir,
|
||||
closePanel,
|
||||
level = 0,
|
||||
}: {
|
||||
project?: IndexLoaderData['project']
|
||||
currentFile?: IndexLoaderData['file']
|
||||
fileOrDir: FileEntry
|
||||
closePanel: (
|
||||
focusableElement?:
|
||||
| HTMLElement
|
||||
| React.MutableRefObject<HTMLElement | null>
|
||||
| undefined
|
||||
) => void
|
||||
level?: number
|
||||
}) => {
|
||||
const { send, context } = useFileContext()
|
||||
const navigate = useNavigate()
|
||||
const [isRenaming, setIsRenaming] = useState(false)
|
||||
const [isConfirmingDelete, setIsConfirmingDelete] = useState(false)
|
||||
const isCurrentFile = fileOrDir.path === currentFile?.path
|
||||
|
||||
function handleKeyUp(e: React.KeyboardEvent<HTMLButtonElement>) {
|
||||
if (e.metaKey && e.key === 'Backspace') {
|
||||
// Open confirmation dialog
|
||||
setIsConfirmingDelete(true)
|
||||
} else if (e.key === 'Enter') {
|
||||
// Show the renaming form
|
||||
setIsRenaming(true)
|
||||
} else if (e.code === 'Space') {
|
||||
openFile()
|
||||
}
|
||||
}
|
||||
|
||||
function openFile() {
|
||||
if (fileOrDir.children !== undefined) return // Don't open directories
|
||||
kclManager.setCode('')
|
||||
navigate(`${paths.FILE}/${encodeURIComponent(fileOrDir.path)}`)
|
||||
closePanel()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{fileOrDir.children === undefined ? (
|
||||
<li
|
||||
className={
|
||||
'group m-0 p-0 border-solid border-0 text-energy-100 hover:text-energy-70 hover:bg-energy-10/50 dark:text-energy-30 dark:hover:!text-energy-20 dark:hover:bg-energy-90/50 focus-within:bg-energy-10/80 dark:focus-within:bg-energy-80/50 hover:focus-within:bg-energy-10/80 dark:hover:focus-within:bg-energy-80/50 ' +
|
||||
(isCurrentFile ? 'bg-energy-10/50 dark:bg-energy-90/50' : '')
|
||||
}
|
||||
>
|
||||
{!isRenaming ? (
|
||||
<button
|
||||
className="flex gap-1 items-center py-0.5 rounded-none border-none p-0 m-0 text-sm w-full hover:!bg-transparent text-left !text-inherit"
|
||||
style={{ paddingInlineStart: getIndentationCSS(level) }}
|
||||
onDoubleClick={openFile}
|
||||
onClick={(e) => e.currentTarget.focus()}
|
||||
onKeyUp={handleKeyUp}
|
||||
>
|
||||
<KclIcon
|
||||
className={
|
||||
'inline-block w-3 ' +
|
||||
(isCurrentFile
|
||||
? 'text-energy-90 dark:text-energy-10'
|
||||
: 'text-energy-50 dark:text-energy-50')
|
||||
}
|
||||
/>
|
||||
{fileOrDir.name}
|
||||
</button>
|
||||
) : (
|
||||
<RenameForm
|
||||
fileOrDir={fileOrDir}
|
||||
setIsRenaming={setIsRenaming}
|
||||
level={level}
|
||||
/>
|
||||
)}
|
||||
</li>
|
||||
) : (
|
||||
<Disclosure defaultOpen={currentFile?.path.includes(fileOrDir.path)}>
|
||||
{({ open }) => (
|
||||
<div className="group">
|
||||
{!isRenaming ? (
|
||||
<Disclosure.Button
|
||||
className={
|
||||
' group border-none text-sm rounded-none p-0 m-0 flex items-center justify-start w-full py-0.5 text-chalkboard-70 dark:text-chalkboard-30 hover:bg-energy-10/50 dark:hover:bg-energy-90/50' +
|
||||
(context.selectedDirectory.path.includes(fileOrDir.path)
|
||||
? ' group-focus-within:bg-chalkboard-20/50 dark:group-focus-within:bg-chalkboard-80/20 hover:group-focus-within:bg-chalkboard-20 dark:hover:group-focus-within:bg-chalkboard-80/20 group-active:bg-chalkboard-20/50 dark:group-active:bg-chalkboard-80/20 hover:group-active:bg-chalkboard-20/50 dark:hover:group-active:bg-chalkboard-80/20'
|
||||
: '')
|
||||
}
|
||||
style={{ paddingInlineStart: getIndentationCSS(level) }}
|
||||
onClick={(e) => e.currentTarget.focus()}
|
||||
onClickCapture={(e) =>
|
||||
send({ type: 'Set selected directory', data: fileOrDir })
|
||||
}
|
||||
onFocusCapture={(e) =>
|
||||
send({ type: 'Set selected directory', data: fileOrDir })
|
||||
}
|
||||
onKeyDown={(e) => e.key === 'Enter' && e.preventDefault()}
|
||||
onKeyUp={handleKeyUp}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faChevronRight}
|
||||
className={
|
||||
'inline-block mr-2 m-0 p-0 w-2 h-2 ' +
|
||||
(open ? 'transform rotate-90' : '')
|
||||
}
|
||||
/>
|
||||
{fileOrDir.name}
|
||||
</Disclosure.Button>
|
||||
) : (
|
||||
<div
|
||||
className="flex items-center"
|
||||
style={{ paddingInlineStart: getIndentationCSS(level) }}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faChevronRight}
|
||||
className={
|
||||
'inline-block mr-2 m-0 p-0 w-2 h-2 ' +
|
||||
(open ? 'transform rotate-90' : '')
|
||||
}
|
||||
/>
|
||||
<RenameForm
|
||||
fileOrDir={fileOrDir}
|
||||
setIsRenaming={setIsRenaming}
|
||||
level={-1}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<Disclosure.Panel
|
||||
className={styles.folder}
|
||||
style={
|
||||
{
|
||||
'--indent-line-left': getIndentationCSS(level),
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<ul
|
||||
className="m-0 p-0"
|
||||
onClickCapture={(e) => {
|
||||
send({ type: 'Set selected directory', data: fileOrDir })
|
||||
}}
|
||||
onFocusCapture={(e) =>
|
||||
send({ type: 'Set selected directory', data: fileOrDir })
|
||||
}
|
||||
>
|
||||
{fileOrDir.children?.map((child) => (
|
||||
<FileTreeItem
|
||||
fileOrDir={child}
|
||||
project={project}
|
||||
currentFile={currentFile}
|
||||
closePanel={closePanel}
|
||||
level={level + 1}
|
||||
key={level + '-' + child.path}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</Disclosure.Panel>
|
||||
</div>
|
||||
)}
|
||||
</Disclosure>
|
||||
)}
|
||||
{isConfirmingDelete && (
|
||||
<DeleteConfirmationDialog
|
||||
fileOrDir={fileOrDir}
|
||||
setIsOpen={setIsConfirmingDelete}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
interface FileTreeProps {
|
||||
className?: string
|
||||
file?: IndexLoaderData['file']
|
||||
closePanel: (
|
||||
focusableElement?:
|
||||
| HTMLElement
|
||||
| React.MutableRefObject<HTMLElement | null>
|
||||
| undefined
|
||||
) => void
|
||||
}
|
||||
|
||||
export const FileTree = ({
|
||||
className = '',
|
||||
file,
|
||||
closePanel,
|
||||
}: FileTreeProps) => {
|
||||
const { send, context } = useFileContext()
|
||||
useHotkeys('meta + n', createFile)
|
||||
useHotkeys('meta + shift + n', createFolder)
|
||||
|
||||
async function createFile() {
|
||||
send({ type: 'Create file', data: { name: '', makeDir: false } })
|
||||
}
|
||||
|
||||
async function createFolder() {
|
||||
send({ type: 'Create file', data: { name: '', makeDir: true } })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className="flex items-center gap-1 px-4 py-1 bg-chalkboard-30/50 dark:bg-chalkboard-70/50">
|
||||
<h2 className="flex-1 m-0 p-0 text-sm mono">Files</h2>
|
||||
<ActionButton
|
||||
Element="button"
|
||||
icon={{
|
||||
icon: 'createFile',
|
||||
iconClassName: '!text-energy-80 dark:!text-energy-20',
|
||||
bgClassName: 'hover:bg-energy-10/50 dark:hover:bg-transparent',
|
||||
}}
|
||||
className="!p-0 border-none bg-transparent !outline-none"
|
||||
onClick={createFile}
|
||||
>
|
||||
<Tooltip position="inlineStart" delay={750}>
|
||||
Create File
|
||||
</Tooltip>
|
||||
</ActionButton>
|
||||
|
||||
<ActionButton
|
||||
Element="button"
|
||||
icon={{
|
||||
icon: 'createFolder',
|
||||
iconClassName: '!text-energy-80 dark:!text-energy-20',
|
||||
bgClassName: 'hover:bg-energy-10/50 dark:hover:bg-transparent',
|
||||
}}
|
||||
className="!p-0 border-none bg-transparent !outline-none"
|
||||
onClick={createFolder}
|
||||
>
|
||||
<Tooltip position="inlineStart" delay={750}>
|
||||
Create Folder
|
||||
</Tooltip>
|
||||
</ActionButton>
|
||||
</div>
|
||||
<div className="overflow-auto max-h-full pb-12">
|
||||
<ul
|
||||
className="m-0 p-0 text-sm"
|
||||
onClickCapture={(e) => {
|
||||
send({ type: 'Set selected directory', data: context.project })
|
||||
}}
|
||||
>
|
||||
{sortProject(context.project.children || []).map((fileOrDir) => (
|
||||
<FileTreeItem
|
||||
project={context.project}
|
||||
currentFile={file}
|
||||
fileOrDir={fileOrDir}
|
||||
closePanel={closePanel}
|
||||
key={fileOrDir.path}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function KclIcon({ className = '' }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
viewBox="0 0 40 40"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M40 0H0V40H40V0ZM7.34715 27.2143V15.6577L2.976 15.987V36.7949H7.34715V32.0645L8.00582 31.5256C8.24533 31.326 8.47487 31.1264 8.69442 30.9268L12.1075 36.7949H17.0475C16.1893 35.3978 15.311 33.9906 14.4128 32.5735C13.5346 31.1563 12.6664 29.7392 11.8081 28.3221L15.8499 24.9389C15.4308 24.4399 15.0017 23.931 14.5625 23.412L13.3051 21.8552L7.34715 27.2143ZM22.2581 26.6754C22.8769 25.9169 23.6753 25.5377 24.6533 25.5377C25.272 25.5377 25.8309 25.6175 26.3299 25.7772C26.8289 25.9169 27.4177 26.1465 28.0963 26.4658L29.3238 23.3521C28.5853 22.7933 27.7371 22.4041 26.779 22.1845C25.8409 21.9649 25.0625 21.8552 24.4437 21.8552C22.0885 21.8552 20.2223 22.5537 18.845 23.9509C17.4878 25.3281 16.8092 27.1944 16.8092 29.5496C16.8092 31.9048 17.4878 33.7611 18.845 35.1183C20.2223 36.4756 22.0885 37.1542 24.4437 37.1542C25.0625 37.1542 25.8509 37.0444 26.8089 36.8249C27.767 36.6053 28.6053 36.2161 29.3238 35.6572L28.0963 32.5435C27.4177 32.8629 26.8289 33.0924 26.3299 33.2321C25.8309 33.3718 25.272 33.4417 24.6533 33.4417C23.6753 33.4417 22.8769 33.0924 22.2581 32.3938C21.6594 31.6753 21.36 30.7272 21.36 29.5496C21.36 28.372 21.6594 27.4139 22.2581 26.6754ZM36.2796 36.7949V15.6577L31.9085 15.987V36.7949H36.2796Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@ -16,7 +16,14 @@ import { engineCommandManager } from 'lang/std/engineConnection'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { addStartSketch } from 'lang/modifyAst'
|
||||
import { roundOff } from 'lib/utils'
|
||||
import { recast, parse, Program, VariableDeclarator } from 'lang/wasm'
|
||||
import {
|
||||
recast,
|
||||
parse,
|
||||
Program,
|
||||
VariableDeclarator,
|
||||
PipeExpression,
|
||||
CallExpression,
|
||||
} from 'lang/wasm'
|
||||
import { getNodeFromPath } from 'lang/queryAst'
|
||||
import {
|
||||
addCloseToPipe,
|
||||
@ -29,11 +36,9 @@ import { applyConstraintAngleBetween } from './Toolbar/SetAngleBetween'
|
||||
import { applyConstraintAngleLength } from './Toolbar/setAngleLength'
|
||||
import { toast } from 'react-hot-toast'
|
||||
import { pathMapToSelections } from 'lang/util'
|
||||
import {
|
||||
dispatchCodeMirrorCursor,
|
||||
setCodeMirrorCursor,
|
||||
useStore,
|
||||
} from 'useStore'
|
||||
import { useStore } from 'useStore'
|
||||
import { handleSelectionBatch, handleSelectionWithShift } from 'lib/selections'
|
||||
import { applyConstraintIntersect } from './Toolbar/Intersect'
|
||||
|
||||
type MachineContext<T extends AnyStateMachine> = {
|
||||
state: StateFrom<T>
|
||||
@ -83,16 +88,16 @@ export const ModelingMachineProvider = ({
|
||||
'show default planes': () => {
|
||||
kclManager.showPlanes()
|
||||
},
|
||||
'create path': async () => {
|
||||
const sketchUuid = uuidv4()
|
||||
const proms = [
|
||||
'create path': assign({
|
||||
sketchEnginePathId: () => {
|
||||
const sketchUuid = uuidv4()
|
||||
engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: sketchUuid,
|
||||
cmd: {
|
||||
type: 'start_path',
|
||||
},
|
||||
}),
|
||||
})
|
||||
engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
@ -100,38 +105,77 @@ export const ModelingMachineProvider = ({
|
||||
type: 'edit_mode_enter',
|
||||
target: sketchUuid,
|
||||
},
|
||||
}),
|
||||
]
|
||||
await Promise.all(proms)
|
||||
},
|
||||
'AST start new sketch': assign((_, { data: { coords, axis } }) => {
|
||||
// Something really weird must have happened for this to happen.
|
||||
if (!axis) {
|
||||
console.error('axis is undefined for starting a new sketch')
|
||||
return {}
|
||||
}
|
||||
|
||||
const _addStartSketch = addStartSketch(
|
||||
kclManager.ast,
|
||||
axis,
|
||||
[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
|
||||
const newCode = recast(_modifiedAst)
|
||||
const astWithUpdatedSource = parse(newCode)
|
||||
|
||||
kclManager.executeAstMock(astWithUpdatedSource, true)
|
||||
|
||||
return {
|
||||
sketchPathToNode: _pathToNode,
|
||||
}
|
||||
})
|
||||
return sketchUuid
|
||||
},
|
||||
}),
|
||||
'AST add line segment': ({ sketchPathToNode }, { data: { coords } }) => {
|
||||
'AST start new sketch': assign(
|
||||
({ sketchEnginePathId }, { data: { coords, axis, segmentId } }) => {
|
||||
if (!axis) {
|
||||
// Something really weird must have happened for this to happen.
|
||||
console.error('axis is undefined for starting a new sketch')
|
||||
return {}
|
||||
}
|
||||
if (!segmentId) {
|
||||
// Something really weird must have happened for this to happen.
|
||||
console.error('segmentId is undefined for starting a new sketch')
|
||||
return {}
|
||||
}
|
||||
|
||||
const _addStartSketch = addStartSketch(
|
||||
kclManager.ast,
|
||||
axis,
|
||||
[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
|
||||
const newCode = recast(_modifiedAst)
|
||||
const astWithUpdatedSource = parse(newCode)
|
||||
const updatedPipeNode = getNodeFromPath<PipeExpression>(
|
||||
astWithUpdatedSource,
|
||||
_pathToNode
|
||||
).node
|
||||
const startProfileAtCallExp = updatedPipeNode.body.find(
|
||||
(exp) =>
|
||||
exp.type === 'CallExpression' &&
|
||||
exp.callee.name === 'startProfileAt'
|
||||
)
|
||||
if (startProfileAtCallExp)
|
||||
engineCommandManager.artifactMap[sketchEnginePathId] = {
|
||||
type: 'result',
|
||||
range: [startProfileAtCallExp.start, startProfileAtCallExp.end],
|
||||
commandType: 'extend_path',
|
||||
data: null,
|
||||
raw: {} as any,
|
||||
}
|
||||
const lineCallExp = updatedPipeNode.body.find(
|
||||
(exp) => exp.type === 'CallExpression' && exp.callee.name === 'line'
|
||||
)
|
||||
if (lineCallExp)
|
||||
engineCommandManager.artifactMap[segmentId] = {
|
||||
type: 'result',
|
||||
range: [lineCallExp.start, lineCallExp.end],
|
||||
commandType: 'extend_path',
|
||||
parentId: sketchEnginePathId,
|
||||
data: null,
|
||||
raw: {} as any,
|
||||
}
|
||||
|
||||
kclManager.executeAstMock(astWithUpdatedSource, true)
|
||||
|
||||
return {
|
||||
sketchPathToNode: _pathToNode,
|
||||
}
|
||||
}
|
||||
),
|
||||
'AST add line segment': (
|
||||
{ sketchPathToNode, sketchEnginePathId },
|
||||
{ data: { coords, segmentId } }
|
||||
) => {
|
||||
if (!sketchPathToNode) return
|
||||
const lastCoord = coords[coords.length - 1]
|
||||
|
||||
@ -152,15 +196,29 @@ export const ModelingMachineProvider = ({
|
||||
|
||||
let _modifiedAst: Program
|
||||
if (!isClose) {
|
||||
_modifiedAst = addNewSketchLn({
|
||||
const newSketchLn = addNewSketchLn({
|
||||
node: kclManager.ast,
|
||||
programMemory: kclManager.programMemory,
|
||||
to: [lastCoord.x, lastCoord.y],
|
||||
fnName: 'line',
|
||||
pathToNode: sketchPathToNode,
|
||||
}).modifiedAst
|
||||
kclManager.executeAstMock(_modifiedAst, true)
|
||||
// kclManager.updateAst(_modifiedAst, false)
|
||||
})
|
||||
const _modifiedAst = newSketchLn.modifiedAst
|
||||
kclManager.executeAstMock(_modifiedAst, true).then(() => {
|
||||
const lineCallExp = getNodeFromPath<CallExpression>(
|
||||
kclManager.ast,
|
||||
newSketchLn.pathToNode
|
||||
).node
|
||||
if (segmentId)
|
||||
engineCommandManager.artifactMap[segmentId] = {
|
||||
type: 'result',
|
||||
range: [lineCallExp.start, lineCallExp.end],
|
||||
commandType: 'extend_path',
|
||||
parentId: sketchEnginePathId,
|
||||
data: null,
|
||||
raw: {} as any,
|
||||
}
|
||||
})
|
||||
} else {
|
||||
_modifiedAst = addCloseToPipe({
|
||||
node: kclManager.ast,
|
||||
@ -209,25 +267,37 @@ export const ModelingMachineProvider = ({
|
||||
// I've found this the best way to deal with the editor without causing an infinite loop
|
||||
// and really we want the editor to be in charge of cursor positions and for `selectionRanges` mirror it
|
||||
// because we want to respect the user manually placing the cursor too.
|
||||
const selectionRangeTypeMap = setCodeMirrorCursor({
|
||||
codeSelection: setSelections.selection,
|
||||
currestSelections: selectionRanges,
|
||||
editorView,
|
||||
isShiftDown,
|
||||
})
|
||||
return {
|
||||
selectionRangeTypeMap,
|
||||
|
||||
// for more details on how selections see `src/lib/selections.ts`.
|
||||
const { codeMirrorSelection, selectionRangeTypeMap } =
|
||||
handleSelectionWithShift({
|
||||
codeSelection: setSelections.selection,
|
||||
currestSelections: selectionRanges,
|
||||
isShiftDown,
|
||||
})
|
||||
if (codeMirrorSelection) {
|
||||
setTimeout(() => {
|
||||
editorView.dispatch({
|
||||
selection: codeMirrorSelection,
|
||||
})
|
||||
})
|
||||
}
|
||||
return { selectionRangeTypeMap }
|
||||
}
|
||||
// This DOES NOT set the `selectionRanges` in xstate context
|
||||
// same as comment above
|
||||
const selectionRangeTypeMap = dispatchCodeMirrorCursor({
|
||||
selections: setSelections.selection,
|
||||
editorView,
|
||||
})
|
||||
return {
|
||||
selectionRangeTypeMap,
|
||||
const { codeMirrorSelection, selectionRangeTypeMap } =
|
||||
handleSelectionBatch({
|
||||
selections: setSelections.selection,
|
||||
})
|
||||
if (codeMirrorSelection) {
|
||||
setTimeout(() => {
|
||||
editorView.dispatch({
|
||||
selection: codeMirrorSelection,
|
||||
})
|
||||
})
|
||||
}
|
||||
return { selectionRangeTypeMap }
|
||||
}),
|
||||
},
|
||||
guards: {
|
||||
@ -312,6 +382,22 @@ export const ModelingMachineProvider = ({
|
||||
),
|
||||
}
|
||||
},
|
||||
'Get perpendicular distance info': async ({
|
||||
selectionRanges,
|
||||
}): Promise<SetSelections> => {
|
||||
const { modifiedAst, pathToNodeMap } = await applyConstraintIntersect({
|
||||
selectionRanges,
|
||||
})
|
||||
await kclManager.updateAst(modifiedAst, true)
|
||||
return {
|
||||
selectionType: 'completeSelection',
|
||||
selection: pathMapToSelections(
|
||||
kclManager.ast,
|
||||
selectionRanges,
|
||||
pathToNodeMap
|
||||
),
|
||||
}
|
||||
},
|
||||
},
|
||||
devTools: true,
|
||||
})
|
||||
@ -325,7 +411,7 @@ export const ModelingMachineProvider = ({
|
||||
})
|
||||
}
|
||||
})
|
||||
}, [kclManager.defaultPlanes, modelingSend, modelingState.nextEvents])
|
||||
}, [modelingSend, modelingState.nextEvents])
|
||||
|
||||
// useStateMachineCommands({
|
||||
// state: settingsState,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { FormEvent, useState } from 'react'
|
||||
import { FormEvent, useEffect, useState } from 'react'
|
||||
import { type ProjectWithEntryPointMetadata, paths } from '../Router'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { ActionButton } from './ActionButton'
|
||||
@ -8,7 +8,7 @@ import {
|
||||
faTrashAlt,
|
||||
faX,
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { FILE_EXT } from '../lib/tauriFS'
|
||||
import { FILE_EXT, getPartsCount, readProject } from '../lib/tauriFS'
|
||||
import { Dialog } from '@headlessui/react'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
|
||||
@ -28,6 +28,8 @@ function ProjectCard({
|
||||
useHotkeys('esc', () => setIsEditing(false))
|
||||
const [isEditing, setIsEditing] = useState(false)
|
||||
const [isConfirmingDelete, setIsConfirmingDelete] = useState(false)
|
||||
const [numberOfParts, setNumberOfParts] = useState(1)
|
||||
const [numberOfFolders, setNumberOfFolders] = useState(0)
|
||||
|
||||
function handleSave(e: FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault()
|
||||
@ -42,6 +44,17 @@ function ProjectCard({
|
||||
: date.toLocaleTimeString()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
async function getNumberOfParts() {
|
||||
const { kclFileCount, kclDirCount } = getPartsCount(
|
||||
await readProject(project.path)
|
||||
)
|
||||
setNumberOfParts(kclFileCount)
|
||||
setNumberOfFolders(kclDirCount)
|
||||
}
|
||||
getNumberOfParts()
|
||||
}, [project.path])
|
||||
|
||||
return (
|
||||
<li
|
||||
{...props}
|
||||
@ -76,7 +89,7 @@ function ProjectCard({
|
||||
</form>
|
||||
) : (
|
||||
<>
|
||||
<div className="p-1 flex flex-col gap-2">
|
||||
<div className="p-1 flex flex-col h-full gap-2">
|
||||
<Link
|
||||
to={`${paths.FILE}/${encodeURIComponent(project.path)}`}
|
||||
className="flex-1 text-liquid-100"
|
||||
@ -84,7 +97,14 @@ function ProjectCard({
|
||||
{project.name?.replace(FILE_EXT, '')}
|
||||
</Link>
|
||||
<span className="text-chalkboard-60 text-xs">
|
||||
Edited {getDisplayedTime(project.entrypoint_metadata.modifiedAt)}
|
||||
{numberOfParts} part{numberOfParts === 1 ? '' : 's'}{' '}
|
||||
{numberOfFolders > 0 &&
|
||||
`/ ${numberOfFolders} folder${
|
||||
numberOfFolders === 1 ? '' : 's'
|
||||
}`}
|
||||
</span>
|
||||
<span className="text-chalkboard-60 text-xs">
|
||||
Edited {getDisplayedTime(project.entrypointMetadata.modifiedAt)}
|
||||
</span>
|
||||
<div className="absolute bottom-2 right-2 flex gap-1 items-center opacity-0 group-hover:opacity-100 group-focus-within:opacity-100">
|
||||
<ActionButton
|
||||
|
||||
@ -15,7 +15,7 @@ const projectWellFormed = {
|
||||
path: '/some/path/Simple Box/main.kcl',
|
||||
},
|
||||
],
|
||||
entrypoint_metadata: {
|
||||
entrypointMetadata: {
|
||||
accessedAt: now,
|
||||
blksize: 32,
|
||||
blocks: 32,
|
||||
|
||||
@ -1,18 +1,21 @@
|
||||
import { Popover, Transition } from '@headlessui/react'
|
||||
import { ActionButton } from './ActionButton'
|
||||
import { faHome } from '@fortawesome/free-solid-svg-icons'
|
||||
import { ProjectWithEntryPointMetadata, paths } from '../Router'
|
||||
import { IndexLoaderData, paths } from '../Router'
|
||||
import { isTauri } from '../lib/isTauri'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { ExportButton } from './ExportButton'
|
||||
import { Fragment } from 'react'
|
||||
import { FileTree } from './FileTree'
|
||||
|
||||
const ProjectSidebarMenu = ({
|
||||
project,
|
||||
file,
|
||||
renderAsLink = false,
|
||||
}: {
|
||||
renderAsLink?: boolean
|
||||
project?: Partial<ProjectWithEntryPointMetadata>
|
||||
project?: IndexLoaderData['project']
|
||||
file?: IndexLoaderData['file']
|
||||
}) => {
|
||||
return renderAsLink ? (
|
||||
<Link
|
||||
@ -43,9 +46,18 @@ const ProjectSidebarMenu = ({
|
||||
alt="KittyCAD App"
|
||||
className="h-full w-auto"
|
||||
/>
|
||||
<span className="text-sm text-chalkboard-110 dark:text-chalkboard-20 whitespace-nowrap hidden lg:block">
|
||||
{isTauri() && project?.name ? project.name : 'KittyCAD Modeling App'}
|
||||
</span>
|
||||
<div className="flex flex-col items-start py-0.5">
|
||||
<span className="text-sm text-chalkboard-110 dark:text-chalkboard-20 whitespace-nowrap hidden lg:block">
|
||||
{isTauri() && file?.name
|
||||
? file.name.slice(file.name.lastIndexOf('/') + 1)
|
||||
: 'KittyCAD Modeling App'}
|
||||
</span>
|
||||
{isTauri() && project?.name && (
|
||||
<span className="text-xs text-chalkboard-70 dark:text-chalkboard-40 whitespace-nowrap hidden lg:block">
|
||||
{project.name}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</Popover.Button>
|
||||
<Transition
|
||||
enter="duration-200 ease-out"
|
||||
@ -68,54 +80,74 @@ const ProjectSidebarMenu = ({
|
||||
leaveTo="opacity-0 -translate-x-4"
|
||||
as={Fragment}
|
||||
>
|
||||
<Popover.Panel className="fixed inset-0 right-auto z-30 w-64 bg-chalkboard-10 dark:bg-chalkboard-100 border border-energy-100 dark:border-energy-100/50 shadow-md rounded-r-lg overflow-hidden">
|
||||
<div className="flex items-center gap-4 px-4 py-3 bg-energy-100">
|
||||
<img
|
||||
src="/kitt-8bit-winking.svg"
|
||||
alt="KittyCAD App"
|
||||
className="h-9 w-auto"
|
||||
/>
|
||||
<Popover.Panel
|
||||
className="fixed inset-0 right-auto z-30 w-64 h-screen max-h-screen grid grid-cols-1 bg-chalkboard-10 dark:bg-chalkboard-100 border border-energy-100 dark:border-energy-100/50 shadow-md rounded-r-lg"
|
||||
style={{ gridTemplateRows: 'auto 1fr auto' }}
|
||||
>
|
||||
{({ close }) => (
|
||||
<>
|
||||
<div className="flex items-center gap-4 px-4 py-3 bg-energy-10/25 dark:bg-energy-110">
|
||||
<img
|
||||
src="/kitt-8bit-winking.svg"
|
||||
alt="KittyCAD App"
|
||||
className="h-9 w-auto"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<p
|
||||
className="m-0 text-energy-10 text-mono"
|
||||
data-testid="projectName"
|
||||
>
|
||||
{project?.name ? project.name : 'KittyCAD Modeling App'}
|
||||
</p>
|
||||
{project?.entrypoint_metadata && (
|
||||
<p
|
||||
className="m-0 text-energy-40 text-xs"
|
||||
data-testid="createdAt"
|
||||
>
|
||||
Created{' '}
|
||||
{project?.entrypoint_metadata.createdAt.toLocaleDateString()}
|
||||
</p>
|
||||
<div>
|
||||
<p
|
||||
className="m-0 text-chalkboard-100 dark:text-energy-10 text-mono"
|
||||
data-testid="projectName"
|
||||
>
|
||||
{project?.name ? project.name : 'KittyCAD Modeling App'}
|
||||
</p>
|
||||
{project?.entrypointMetadata && (
|
||||
<p
|
||||
className="m-0 text-chalkboard-100 dark:text-energy-40 text-xs"
|
||||
data-testid="createdAt"
|
||||
>
|
||||
Created{' '}
|
||||
{project.entrypointMetadata.createdAt.toLocaleDateString()}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{isTauri() ? (
|
||||
<FileTree
|
||||
file={file}
|
||||
className="overflow-hidden border-0 border-y border-energy-40 dark:border-energy-70"
|
||||
closePanel={close}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex-1 overflow-hidden" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 flex flex-col gap-2">
|
||||
<ExportButton
|
||||
className={{
|
||||
button:
|
||||
'border-transparent dark:border-transparent dark:hover:border-energy-60',
|
||||
}}
|
||||
>
|
||||
Export Model
|
||||
</ExportButton>
|
||||
{isTauri() && (
|
||||
<ActionButton
|
||||
Element="link"
|
||||
to={paths.HOME}
|
||||
icon={{
|
||||
icon: faHome,
|
||||
}}
|
||||
className="border-transparent dark:border-transparent dark:hover:border-energy-60"
|
||||
>
|
||||
Go to Home
|
||||
</ActionButton>
|
||||
)}
|
||||
</div>
|
||||
<div className="p-4 flex flex-col gap-2 bg-energy-10/25 dark:bg-energy-110">
|
||||
<ExportButton
|
||||
className={{
|
||||
button:
|
||||
'border-transparent dark:border-transparent hover:border-energy-60',
|
||||
icon: 'text-energy-10 dark:text-energy-120',
|
||||
bg: 'bg-energy-120 dark:bg-energy-10',
|
||||
}}
|
||||
>
|
||||
Export Model
|
||||
</ExportButton>
|
||||
{isTauri() && (
|
||||
<ActionButton
|
||||
Element="link"
|
||||
to={paths.HOME}
|
||||
icon={{
|
||||
icon: faHome,
|
||||
iconClassName: 'text-energy-10 dark:text-energy-120',
|
||||
bgClassName: 'bg-energy-120 dark:bg-energy-10',
|
||||
}}
|
||||
className="border-transparent dark:border-transparent hover:border-energy-60"
|
||||
>
|
||||
Go to Home
|
||||
</ActionButton>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</Popover>
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { Dialog, Transition } from '@headlessui/react'
|
||||
import { Fragment, useState } from 'react'
|
||||
import { type InstanceProps, create } from 'react-modal-promise'
|
||||
import { Value } from '../lang/wasm'
|
||||
import {
|
||||
AvailableVars,
|
||||
@ -9,6 +10,28 @@ import {
|
||||
CreateNewVariable,
|
||||
} from './AvailableVarsHelpers'
|
||||
|
||||
type ModalResolve = {
|
||||
value: string
|
||||
sign: number
|
||||
valueNode: Value
|
||||
variableName?: string
|
||||
newVariableInsertIndex: number
|
||||
}
|
||||
|
||||
type ModalReject = boolean
|
||||
|
||||
type SetAngleLengthModalProps = InstanceProps<ModalResolve, ModalReject> & {
|
||||
value: number
|
||||
valueName: string
|
||||
shouldCreateVariable?: boolean
|
||||
}
|
||||
|
||||
export const createSetAngleLengthModal = create<
|
||||
SetAngleLengthModalProps,
|
||||
ModalResolve,
|
||||
ModalReject
|
||||
>
|
||||
|
||||
export const SetAngleLengthModal = ({
|
||||
isOpen,
|
||||
onResolve,
|
||||
@ -16,20 +39,7 @@ export const SetAngleLengthModal = ({
|
||||
value: initialValue,
|
||||
valueName,
|
||||
shouldCreateVariable: initialShouldCreateVariable = false,
|
||||
}: {
|
||||
isOpen: boolean
|
||||
onResolve: (a: {
|
||||
value: string
|
||||
sign: number
|
||||
valueNode: Value
|
||||
variableName?: string
|
||||
newVariableInsertIndex: number
|
||||
}) => void
|
||||
onReject: (a: any) => void
|
||||
value: number
|
||||
valueName: string
|
||||
shouldCreateVariable: boolean
|
||||
}) => {
|
||||
}: SetAngleLengthModalProps) => {
|
||||
const [sign, setSign] = useState(Math.sign(Number(initialValue)))
|
||||
const [value, setValue] = useState(String(initialValue * sign))
|
||||
const [shouldCreateVariable, setShouldCreateVariable] = useState(
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { Dialog, Transition } from '@headlessui/react'
|
||||
import { Fragment, useState } from 'react'
|
||||
import { type InstanceProps, create } from 'react-modal-promise'
|
||||
import { Value } from '../lang/wasm'
|
||||
import {
|
||||
AvailableVars,
|
||||
@ -9,6 +10,30 @@ import {
|
||||
CreateNewVariable,
|
||||
} from './AvailableVarsHelpers'
|
||||
|
||||
type ModalResolve = {
|
||||
value: string
|
||||
segName: string
|
||||
valueNode: Value
|
||||
variableName?: string
|
||||
newVariableInsertIndex: number
|
||||
sign: number
|
||||
}
|
||||
|
||||
type ModalReject = boolean
|
||||
|
||||
type GetInfoModalProps = InstanceProps<ModalResolve, ModalReject> & {
|
||||
segName: string
|
||||
isSegNameEditable: boolean
|
||||
value?: number
|
||||
initialVariableName: string
|
||||
}
|
||||
|
||||
export const createInfoModal = create<
|
||||
GetInfoModalProps,
|
||||
ModalResolve,
|
||||
ModalReject
|
||||
>
|
||||
|
||||
export const GetInfoModal = ({
|
||||
isOpen,
|
||||
onResolve,
|
||||
@ -17,25 +42,12 @@ export const GetInfoModal = ({
|
||||
isSegNameEditable,
|
||||
value: initialValue,
|
||||
initialVariableName,
|
||||
}: {
|
||||
isOpen: boolean
|
||||
onResolve: (a: {
|
||||
value: string
|
||||
segName: string
|
||||
valueNode: Value
|
||||
variableName?: string
|
||||
newVariableInsertIndex: number
|
||||
sign: number
|
||||
}) => void
|
||||
onReject: (a: any) => void
|
||||
segName: string
|
||||
isSegNameEditable: boolean
|
||||
value: number
|
||||
initialVariableName: string
|
||||
}) => {
|
||||
}: GetInfoModalProps) => {
|
||||
const [sign, setSign] = useState(Math.sign(Number(initialValue)))
|
||||
const [segName, setSegName] = useState(initialSegName)
|
||||
const [value, setValue] = useState(String(Math.abs(initialValue)))
|
||||
const [value, setValue] = useState(
|
||||
initialValue === undefined ? '' : String(Math.abs(initialValue))
|
||||
)
|
||||
const [shouldCreateVariable, setShouldCreateVariable] = useState(false)
|
||||
|
||||
const {
|
||||
|
||||
@ -4,19 +4,26 @@ import { useCalc, CreateNewVariable } from './AvailableVarsHelpers'
|
||||
import { ActionButton } from './ActionButton'
|
||||
import { faPlus } from '@fortawesome/free-solid-svg-icons'
|
||||
import { toast } from 'react-hot-toast'
|
||||
import { type InstanceProps, create } from 'react-modal-promise'
|
||||
|
||||
type ModalResolve = { variableName: string }
|
||||
type ModalReject = boolean
|
||||
type SetVarNameModalProps = InstanceProps<ModalResolve, ModalReject> & {
|
||||
valueName: string
|
||||
}
|
||||
|
||||
export const createSetVarNameModal = create<
|
||||
SetVarNameModalProps,
|
||||
ModalResolve,
|
||||
ModalReject
|
||||
>
|
||||
|
||||
export const SetVarNameModal = ({
|
||||
isOpen,
|
||||
onResolve,
|
||||
onReject,
|
||||
valueName,
|
||||
}: {
|
||||
isOpen: boolean
|
||||
onResolve: (a: { variableName?: string }) => void
|
||||
onReject: (a: any) => void
|
||||
value: number
|
||||
valueName: string
|
||||
}) => {
|
||||
}: SetVarNameModalProps) => {
|
||||
const { isNewVariableNameUnique, newVariableName, setNewVariableName } =
|
||||
useCalc({ value: '', initialVariableName: valueName })
|
||||
|
||||
|
||||
@ -14,10 +14,11 @@ import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||
import { CameraDragInteractionType_type } from '@kittycad/lib/dist/types/src/models'
|
||||
import { Models } from '@kittycad/lib'
|
||||
import { getNodeFromPath } from 'lang/queryAst'
|
||||
import { Program, VariableDeclarator, modifyAstForSketch } from 'lang/wasm'
|
||||
import { VariableDeclarator, recast, parse, CallExpression } from 'lang/wasm'
|
||||
import { engineCommandManager } from '../lang/std/engineConnection'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
import { kclManager, useKclContext } from 'lang/KclSinglton'
|
||||
import { changeSketchArguments } from 'lang/std/sketch'
|
||||
|
||||
export const Stream = ({ className = '' }) => {
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
@ -84,6 +85,12 @@ export const Stream = ({ className = '' }) => {
|
||||
}
|
||||
|
||||
if (state.matches('Sketch.Move Tool')) {
|
||||
if (
|
||||
state.matches('Sketch.Move Tool.No move') ||
|
||||
state.matches('Sketch.Move Tool.Move with execute')
|
||||
) {
|
||||
return
|
||||
}
|
||||
engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
@ -209,7 +216,14 @@ export const Stream = ({ className = '' }) => {
|
||||
}
|
||||
}
|
||||
|
||||
send({ type: 'Add point', data: { coords, axis: currentAxis } })
|
||||
send({
|
||||
type: 'Add point',
|
||||
data: {
|
||||
coords,
|
||||
axis: currentAxis,
|
||||
segmentId: entities_modified[0],
|
||||
},
|
||||
})
|
||||
} else if (state.matches('Sketch.Line Tool.Segment Added')) {
|
||||
const curve = await engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
@ -221,7 +235,10 @@ export const Stream = ({ className = '' }) => {
|
||||
})
|
||||
const coords: { x: number; y: number }[] =
|
||||
curve.data.data.control_points
|
||||
send({ type: 'Add point', data: { coords, axis: null } })
|
||||
send({
|
||||
type: 'Add point',
|
||||
data: { coords, axis: null, segmentId: entities_modified[0] },
|
||||
})
|
||||
}
|
||||
})
|
||||
} else if (
|
||||
@ -255,8 +272,6 @@ export const Stream = ({ className = '' }) => {
|
||||
context.sketchPathToNode,
|
||||
'VariableDeclarator'
|
||||
).node
|
||||
const variableName = varDec?.id?.name
|
||||
|
||||
// Get the current plane string for plane we are on.
|
||||
let currentPlaneString = ''
|
||||
if (context.sketchPlaneId === kclManager.getPlaneId('xy')) {
|
||||
@ -272,14 +287,73 @@ export const Stream = ({ className = '' }) => {
|
||||
// error.
|
||||
if (currentPlaneString === '') return
|
||||
|
||||
const updatedAst: Program = await modifyAstForSketch(
|
||||
engineCommandManager,
|
||||
kclManager.ast,
|
||||
variableName,
|
||||
currentPlaneString,
|
||||
context.sketchEnginePathId
|
||||
const pathInfo = await engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'path_get_info',
|
||||
path_id: context.sketchEnginePathId,
|
||||
},
|
||||
})
|
||||
const segmentsWithMappings = (
|
||||
pathInfo?.data?.data?.segments as { command_id: string }[]
|
||||
)
|
||||
kclManager.executeAstMock(updatedAst, true)
|
||||
.filter(({ command_id }) => {
|
||||
return command_id && engineCommandManager.artifactMap[command_id]
|
||||
})
|
||||
.map(({ command_id }) => command_id)
|
||||
const segment2dInfo = await Promise.all(
|
||||
segmentsWithMappings.map(async (segmentId) => {
|
||||
const response = await engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'curve_get_control_points',
|
||||
curve_id: segmentId,
|
||||
},
|
||||
})
|
||||
const controlPoints: [
|
||||
{ x: number; y: number },
|
||||
{ x: number; y: number }
|
||||
] = response.data.data.control_points
|
||||
return {
|
||||
controlPoints,
|
||||
segmentId,
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
let modifiedAst = { ...kclManager.ast }
|
||||
let code = kclManager.code
|
||||
for (const controlPoint of segment2dInfo) {
|
||||
const range =
|
||||
engineCommandManager.artifactMap[controlPoint.segmentId].range
|
||||
if (!range) continue
|
||||
const from = controlPoint.controlPoints[0]
|
||||
const to = controlPoint.controlPoints[1]
|
||||
const modded = changeSketchArguments(
|
||||
modifiedAst,
|
||||
kclManager.programMemory,
|
||||
range,
|
||||
[to.x, to.y],
|
||||
[from.x, from.y]
|
||||
)
|
||||
modifiedAst = modded.modifiedAst
|
||||
|
||||
// update artifact map ranges now that we have updated the ast.
|
||||
code = recast(modded.modifiedAst)
|
||||
const astWithCurrentRanges = parse(code)
|
||||
const updateNode = getNodeFromPath<CallExpression>(
|
||||
astWithCurrentRanges,
|
||||
modded.pathToNode
|
||||
).node
|
||||
engineCommandManager.artifactMap[controlPoint.segmentId].range = [
|
||||
updateNode.start,
|
||||
updateNode.end,
|
||||
]
|
||||
}
|
||||
|
||||
kclManager.executeAstMock(modifiedAst, true)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -11,9 +11,10 @@ import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||
import { useConvertToVariable } from 'hooks/useToolbarGuards'
|
||||
import { Themes } from 'lib/theme'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import { linter, lintGutter } from '@codemirror/lint'
|
||||
import { Selections, useStore } from 'useStore'
|
||||
import { useStore } from 'useStore'
|
||||
import { processCodeMirrorRanges } from 'lib/selections'
|
||||
import { LanguageServerClient } from 'editor/lsp'
|
||||
import kclLanguage from 'editor/lsp/language'
|
||||
import { isTauri } from 'lib/isTauri'
|
||||
@ -116,13 +117,11 @@ export const TextEditor = ({
|
||||
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, newCode).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')
|
||||
}
|
||||
)
|
||||
writeTextFile(pathParams.id, newCode).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]) })
|
||||
@ -132,64 +131,17 @@ export const TextEditor = ({
|
||||
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(
|
||||
engineCommandManager.sourceRangeMap || {}
|
||||
).filter(([_, sourceRange]) => {
|
||||
return isOverlap(sourceRange, range)
|
||||
})
|
||||
if (hasOverlap.length) {
|
||||
return {
|
||||
type,
|
||||
id: hasOverlap[0][0],
|
||||
}
|
||||
}
|
||||
})
|
||||
.filter(Boolean) as any
|
||||
|
||||
engineCommandManager.cusorsSelected({
|
||||
otherSelections: [],
|
||||
idBasedSelections,
|
||||
const eventInfo = processCodeMirrorRanges({
|
||||
codeMirrorRanges: viewUpdate.state.selection.ranges,
|
||||
selectionRanges,
|
||||
selectionRangeTypeMap,
|
||||
})
|
||||
if (!eventInfo) return
|
||||
|
||||
selectionRanges &&
|
||||
send({
|
||||
type: 'Set selection',
|
||||
data: {
|
||||
selectionType: 'mirrorCodeMirrorSelections',
|
||||
selection: {
|
||||
...selectionRanges,
|
||||
codeBasedSelections,
|
||||
},
|
||||
},
|
||||
})
|
||||
send(eventInfo.modelingEvent)
|
||||
eventInfo.engineEvents.forEach((event) =>
|
||||
engineCommandManager.sendSceneCommand(event)
|
||||
)
|
||||
}
|
||||
|
||||
const editorExtensions = useMemo(() => {
|
||||
@ -269,7 +221,7 @@ export const TextEditor = ({
|
||||
}
|
||||
|
||||
return extensions
|
||||
}, [kclLSP, textWrapping])
|
||||
}, [kclLSP, textWrapping, convertCallback])
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@ -1,102 +1,79 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { toolTips, useStore } from '../../useStore'
|
||||
import { Value, VariableDeclarator } from '../../lang/wasm'
|
||||
import { toolTips } from '../../useStore'
|
||||
import { Selections } from 'lib/selections'
|
||||
import { Program, Value, VariableDeclarator } from '../../lang/wasm'
|
||||
import {
|
||||
getNodePathFromSourceRange,
|
||||
getNodeFromPath,
|
||||
} from '../../lang/queryAst'
|
||||
import { isSketchVariablesLinked } from '../../lang/std/sketchConstraints'
|
||||
import {
|
||||
TransformInfo,
|
||||
transformSecondarySketchLinesTagFirst,
|
||||
getTransformInfos,
|
||||
PathToNodeMap,
|
||||
} from '../../lang/std/sketchcombos'
|
||||
import { ActionIcon } from 'components/ActionIcon'
|
||||
import { sketchButtonClassnames } from 'Toolbar'
|
||||
import { kclManager } from 'lang/KclSinglton'
|
||||
|
||||
/*
|
||||
export const EqualAngle = () => {
|
||||
const { guiMode, selectionRanges, setCursor } = useStore((s) => ({
|
||||
guiMode: s.guiMode,
|
||||
selectionRanges: s.selectionRanges,
|
||||
setCursor: s.setCursor,
|
||||
}))
|
||||
const [enableEqual, setEnableEqual] = useState(false)
|
||||
const [transformInfos, setTransformInfos] = useState<TransformInfo[]>()
|
||||
useEffect(() => {
|
||||
const paths = selectionRanges.codeBasedSelections.map(({ range }) =>
|
||||
getNodePathFromSourceRange(kclManager.ast, range)
|
||||
)
|
||||
const nodes = paths.map(
|
||||
(pathToNode) => getNodeFromPath<Value>(kclManager.ast, pathToNode).node
|
||||
)
|
||||
const varDecs = paths.map(
|
||||
(pathToNode) =>
|
||||
getNodeFromPath<VariableDeclarator>(
|
||||
kclManager.ast,
|
||||
pathToNode,
|
||||
'VariableDeclarator'
|
||||
)?.node
|
||||
)
|
||||
const primaryLine = varDecs[0]
|
||||
const secondaryVarDecs = varDecs.slice(1)
|
||||
const isOthersLinkedToPrimary = secondaryVarDecs.every((secondary) =>
|
||||
isSketchVariablesLinked(secondary, primaryLine, kclManager.ast)
|
||||
)
|
||||
const isAllTooltips = nodes.every(
|
||||
(node) =>
|
||||
node?.type === 'CallExpression' &&
|
||||
toolTips.includes(node.callee.name as any)
|
||||
)
|
||||
|
||||
const theTransforms = getTransformInfos(
|
||||
{
|
||||
...selectionRanges,
|
||||
codeBasedSelections: selectionRanges.codeBasedSelections.slice(1),
|
||||
},
|
||||
kclManager.ast,
|
||||
'equalAngle'
|
||||
)
|
||||
setTransformInfos(theTransforms)
|
||||
|
||||
const _enableEqual =
|
||||
!!secondaryVarDecs.length &&
|
||||
isAllTooltips &&
|
||||
isOthersLinkedToPrimary &&
|
||||
theTransforms.every(Boolean)
|
||||
setEnableEqual(_enableEqual)
|
||||
}, [guiMode, selectionRanges])
|
||||
if (guiMode.mode !== 'sketch') return null
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={async () => {
|
||||
if (!transformInfos) return
|
||||
const { modifiedAst, pathToNodeMap } =
|
||||
transformSecondarySketchLinesTagFirst({
|
||||
ast: kclManager.ast,
|
||||
selectionRanges,
|
||||
transformInfos,
|
||||
programMemory: kclManager.programMemory,
|
||||
})
|
||||
kclManager.updateAst(modifiedAst, true, {
|
||||
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||
})
|
||||
}}
|
||||
disabled={!enableEqual}
|
||||
title="Parallel (or equal angle)"
|
||||
className="group"
|
||||
>
|
||||
<ActionIcon
|
||||
icon="parallel"
|
||||
className="!p-0.5"
|
||||
bgClassName={sketchButtonClassnames.background}
|
||||
iconClassName={sketchButtonClassnames.icon}
|
||||
size="md"
|
||||
/>
|
||||
Parallel
|
||||
</button>
|
||||
export function equalAngleInfo({
|
||||
selectionRanges,
|
||||
}: {
|
||||
selectionRanges: Selections
|
||||
}) {
|
||||
const paths = selectionRanges.codeBasedSelections.map(({ range }) =>
|
||||
getNodePathFromSourceRange(kclManager.ast, range)
|
||||
)
|
||||
const nodes = paths.map(
|
||||
(pathToNode) => getNodeFromPath<Value>(kclManager.ast, pathToNode).node
|
||||
)
|
||||
const varDecs = paths.map(
|
||||
(pathToNode) =>
|
||||
getNodeFromPath<VariableDeclarator>(
|
||||
kclManager.ast,
|
||||
pathToNode,
|
||||
'VariableDeclarator'
|
||||
)?.node
|
||||
)
|
||||
const primaryLine = varDecs[0]
|
||||
const secondaryVarDecs = varDecs.slice(1)
|
||||
const isOthersLinkedToPrimary = secondaryVarDecs.every((secondary) =>
|
||||
isSketchVariablesLinked(secondary, primaryLine, kclManager.ast)
|
||||
)
|
||||
const isAllTooltips = nodes.every(
|
||||
(node) =>
|
||||
node?.type === 'CallExpression' &&
|
||||
toolTips.includes(node.callee.name as any)
|
||||
)
|
||||
|
||||
const transforms = getTransformInfos(
|
||||
{
|
||||
...selectionRanges,
|
||||
codeBasedSelections: selectionRanges.codeBasedSelections.slice(1),
|
||||
},
|
||||
kclManager.ast,
|
||||
'equalAngle'
|
||||
)
|
||||
|
||||
const enabled =
|
||||
!!secondaryVarDecs.length &&
|
||||
isAllTooltips &&
|
||||
isOthersLinkedToPrimary &&
|
||||
transforms.every(Boolean)
|
||||
return { enabled, transforms }
|
||||
}
|
||||
|
||||
export function applyConstraintEqualAngle({
|
||||
selectionRanges,
|
||||
}: {
|
||||
selectionRanges: Selections
|
||||
}): {
|
||||
modifiedAst: Program
|
||||
pathToNodeMap: PathToNodeMap
|
||||
} {
|
||||
const { transforms } = equalAngleInfo({ selectionRanges })
|
||||
const { modifiedAst, pathToNodeMap } = transformSecondarySketchLinesTagFirst({
|
||||
ast: kclManager.ast,
|
||||
selectionRanges,
|
||||
transformInfos: transforms,
|
||||
programMemory: kclManager.programMemory,
|
||||
})
|
||||
return { modifiedAst, pathToNodeMap }
|
||||
}
|
||||
*/
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Selections, toolTips, useStore } from '../../useStore'
|
||||
import { toolTips } from '../../useStore'
|
||||
import { Selections } from 'lib/selections'
|
||||
import { Program, Value, VariableDeclarator } from '../../lang/wasm'
|
||||
import {
|
||||
getNodePathFromSourceRange,
|
||||
@ -7,63 +7,12 @@ import {
|
||||
} from '../../lang/queryAst'
|
||||
import { isSketchVariablesLinked } from '../../lang/std/sketchConstraints'
|
||||
import {
|
||||
TransformInfo,
|
||||
transformSecondarySketchLinesTagFirst,
|
||||
getTransformInfos,
|
||||
PathToNodeMap,
|
||||
} from '../../lang/std/sketchcombos'
|
||||
import { ActionIcon } from 'components/ActionIcon'
|
||||
import { sketchButtonClassnames } from 'Toolbar'
|
||||
import { kclManager } from 'lang/KclSinglton'
|
||||
|
||||
/*
|
||||
export const EqualLength = () => {
|
||||
const { guiMode, selectionRanges, setCursor } = useStore((s) => ({
|
||||
guiMode: s.guiMode,
|
||||
selectionRanges: s.selectionRanges,
|
||||
setCursor: s.setCursor,
|
||||
}))
|
||||
const [enableEqual, setEnableEqual] = useState(false)
|
||||
const [transformInfos, setTransformInfos] = useState<TransformInfo[]>()
|
||||
useEffect(() => {
|
||||
const { enabled, transforms } = setEqualLengthInfo({ selectionRanges })
|
||||
|
||||
setTransformInfos(transforms)
|
||||
setEnableEqual(enabled)
|
||||
}, [guiMode, selectionRanges])
|
||||
if (guiMode.mode !== 'sketch') return null
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={() => {
|
||||
if (!transformInfos) return
|
||||
const { modifiedAst, pathToNodeMap } =
|
||||
transformSecondarySketchLinesTagFirst({
|
||||
ast: kclManager.ast,
|
||||
selectionRanges,
|
||||
transformInfos,
|
||||
programMemory: kclManager.programMemory,
|
||||
})
|
||||
kclManager.updateAst(modifiedAst, true, {
|
||||
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||
})
|
||||
}}
|
||||
disabled={!enableEqual}
|
||||
className="group"
|
||||
title="Equal Length"
|
||||
>
|
||||
<ActionIcon
|
||||
icon="equal"
|
||||
className="!p-0.5"
|
||||
bgClassName={sketchButtonClassnames.background}
|
||||
iconClassName={sketchButtonClassnames.icon}
|
||||
size="md"
|
||||
/>
|
||||
Equal Length
|
||||
</button>
|
||||
)
|
||||
}
|
||||
*/
|
||||
export function setEqualLengthInfo({
|
||||
selectionRanges,
|
||||
}: {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { toolTips, useStore } from '../../useStore'
|
||||
import { toolTips } from '../../useStore'
|
||||
import { Selections } from 'lib/selections'
|
||||
import { Program, ProgramMemory, Value } from '../../lang/wasm'
|
||||
import {
|
||||
getNodePathFromSourceRange,
|
||||
@ -7,66 +7,10 @@ import {
|
||||
} from '../../lang/queryAst'
|
||||
import {
|
||||
PathToNodeMap,
|
||||
TransformInfo,
|
||||
getTransformInfos,
|
||||
transformAstSketchLines,
|
||||
} from '../../lang/std/sketchcombos'
|
||||
import { ActionIcon } from 'components/ActionIcon'
|
||||
import { sketchButtonClassnames } from 'Toolbar'
|
||||
import { kclManager } from 'lang/KclSinglton'
|
||||
import { Selections } from 'useStore'
|
||||
|
||||
/*
|
||||
export const HorzVert = ({
|
||||
horOrVert,
|
||||
}: {
|
||||
horOrVert: 'vertical' | 'horizontal'
|
||||
}) => {
|
||||
const { guiMode, selectionRanges, setCursor } = useStore((s) => ({
|
||||
guiMode: s.guiMode,
|
||||
selectionRanges: s.selectionRanges,
|
||||
setCursor: s.setCursor,
|
||||
}))
|
||||
const [enableHorz, setEnableHorz] = useState(false)
|
||||
const [transformInfos, setTransformInfos] = useState<TransformInfo[]>()
|
||||
useEffect(() => {
|
||||
const { enabled, transforms } = horzVertInfo(selectionRanges, horOrVert)
|
||||
setTransformInfos(transforms)
|
||||
setEnableHorz(enabled)
|
||||
}, [guiMode, selectionRanges])
|
||||
if (guiMode.mode !== 'sketch') return null
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={() => {
|
||||
if (!transformInfos) return
|
||||
const { modifiedAst, pathToNodeMap } = transformAstSketchLines({
|
||||
ast: kclManager.ast,
|
||||
selectionRanges,
|
||||
transformInfos,
|
||||
programMemory: kclManager.programMemory,
|
||||
referenceSegName: '',
|
||||
})
|
||||
kclManager.updateAst(modifiedAst, true, {
|
||||
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||
})
|
||||
}}
|
||||
disabled={!enableHorz}
|
||||
className="group"
|
||||
title={horOrVert === 'horizontal' ? 'Horizontal' : 'Vertical'}
|
||||
>
|
||||
<ActionIcon
|
||||
icon={horOrVert === 'horizontal' ? 'horizontal' : 'vertical'}
|
||||
className="!p-0.5"
|
||||
bgClassName={sketchButtonClassnames.background}
|
||||
iconClassName={sketchButtonClassnames.icon}
|
||||
size="md"
|
||||
/>
|
||||
{horOrVert === 'horizontal' ? 'Horizontal' : 'Vertical'}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
*/
|
||||
|
||||
export function horzVertInfo(
|
||||
selectionRanges: Selections,
|
||||
@ -110,7 +54,4 @@ export function applyConstraintHorzVert(
|
||||
programMemory,
|
||||
referenceSegName: '',
|
||||
})
|
||||
// kclManager.updateAst(modifiedAst, true, {
|
||||
// callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||
// })
|
||||
}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { create } from 'react-modal-promise'
|
||||
import { toolTips, useStore } from '../../useStore'
|
||||
import { BinaryPart, Value, VariableDeclarator } from '../../lang/wasm'
|
||||
import { toolTips } from '../../useStore'
|
||||
import { Selections } from 'lib/selections'
|
||||
import { BinaryPart, Program, Value, VariableDeclarator } from '../../lang/wasm'
|
||||
import {
|
||||
getNodePathFromSourceRange,
|
||||
getNodeFromPath,
|
||||
@ -9,181 +8,170 @@ import {
|
||||
} from '../../lang/queryAst'
|
||||
import { isSketchVariablesLinked } from '../../lang/std/sketchConstraints'
|
||||
import {
|
||||
TransformInfo,
|
||||
transformSecondarySketchLinesTagFirst,
|
||||
getTransformInfos,
|
||||
PathToNodeMap,
|
||||
} from '../../lang/std/sketchcombos'
|
||||
import { GetInfoModal } from '../SetHorVertDistanceModal'
|
||||
import { GetInfoModal, createInfoModal } from '../SetHorVertDistanceModal'
|
||||
import { createVariableDeclaration } from '../../lang/modifyAst'
|
||||
import { removeDoubleNegatives } from '../AvailableVarsHelpers'
|
||||
import { kclManager } from 'lang/KclSinglton'
|
||||
|
||||
const getModalInfo = create(GetInfoModal as any)
|
||||
const getModalInfo = createInfoModal(GetInfoModal)
|
||||
|
||||
/*
|
||||
export const Intersect = () => {
|
||||
const { guiMode, selectionRanges, setCursor } = useStore((s) => ({
|
||||
guiMode: s.guiMode,
|
||||
selectionRanges: s.selectionRanges,
|
||||
setCursor: s.setCursor,
|
||||
}))
|
||||
const [enable, setEnable] = useState(false)
|
||||
const [transformInfos, setTransformInfos] = useState<TransformInfo[]>()
|
||||
const [forecdSelectionRanges, setForcedSelectionRanges] =
|
||||
useState<typeof selectionRanges>()
|
||||
useEffect(() => {
|
||||
if (selectionRanges.codeBasedSelections.length < 2) {
|
||||
setEnable(false)
|
||||
setForcedSelectionRanges({ ...selectionRanges })
|
||||
return
|
||||
export function intersectInfo({
|
||||
selectionRanges,
|
||||
}: {
|
||||
selectionRanges: Selections
|
||||
}) {
|
||||
if (selectionRanges.codeBasedSelections.length < 2) {
|
||||
return {
|
||||
enabled: false,
|
||||
transforms: [],
|
||||
forcedSelectionRanges: { ...selectionRanges },
|
||||
}
|
||||
}
|
||||
|
||||
const previousSegment =
|
||||
selectionRanges.codeBasedSelections.length > 1 &&
|
||||
isLinesParallelAndConstrained(
|
||||
kclManager.ast,
|
||||
kclManager.programMemory,
|
||||
selectionRanges.codeBasedSelections[0],
|
||||
selectionRanges.codeBasedSelections[1]
|
||||
)
|
||||
const shouldUsePreviousSegment =
|
||||
selectionRanges.codeBasedSelections?.[1]?.type !== 'line-end' &&
|
||||
previousSegment &&
|
||||
previousSegment.isParallelAndConstrained
|
||||
|
||||
const _forcedSelectionRanges: typeof selectionRanges = {
|
||||
...selectionRanges,
|
||||
codeBasedSelections: [
|
||||
selectionRanges.codeBasedSelections?.[0],
|
||||
shouldUsePreviousSegment
|
||||
? {
|
||||
range: previousSegment.sourceRange,
|
||||
type: 'line-end',
|
||||
}
|
||||
: selectionRanges.codeBasedSelections?.[1],
|
||||
],
|
||||
}
|
||||
setForcedSelectionRanges(_forcedSelectionRanges)
|
||||
|
||||
const paths = _forcedSelectionRanges.codeBasedSelections.map(({ range }) =>
|
||||
getNodePathFromSourceRange(kclManager.ast, range)
|
||||
)
|
||||
const nodes = paths.map(
|
||||
(pathToNode) => getNodeFromPath<Value>(kclManager.ast, pathToNode).node
|
||||
)
|
||||
const varDecs = paths.map(
|
||||
(pathToNode) =>
|
||||
getNodeFromPath<VariableDeclarator>(
|
||||
kclManager.ast,
|
||||
pathToNode,
|
||||
'VariableDeclarator'
|
||||
)?.node
|
||||
)
|
||||
const primaryLine = varDecs[0]
|
||||
const secondaryVarDecs = varDecs.slice(1)
|
||||
const isOthersLinkedToPrimary = secondaryVarDecs.every((secondary) =>
|
||||
isSketchVariablesLinked(secondary, primaryLine, kclManager.ast)
|
||||
)
|
||||
const isAllTooltips = nodes.every(
|
||||
(node) =>
|
||||
node?.type === 'CallExpression' &&
|
||||
[
|
||||
...toolTips,
|
||||
'startSketchAt', // TODO probably a better place for this to live
|
||||
].includes(node.callee.name as any)
|
||||
)
|
||||
|
||||
const theTransforms = getTransformInfos(
|
||||
{
|
||||
...selectionRanges,
|
||||
codeBasedSelections:
|
||||
_forcedSelectionRanges.codeBasedSelections.slice(1),
|
||||
},
|
||||
const previousSegment =
|
||||
selectionRanges.codeBasedSelections.length > 1 &&
|
||||
isLinesParallelAndConstrained(
|
||||
kclManager.ast,
|
||||
'intersect'
|
||||
kclManager.programMemory,
|
||||
selectionRanges.codeBasedSelections[0],
|
||||
selectionRanges.codeBasedSelections[1]
|
||||
)
|
||||
setTransformInfos(theTransforms)
|
||||
const shouldUsePreviousSegment =
|
||||
selectionRanges.codeBasedSelections?.[1]?.type !== 'line-end' &&
|
||||
previousSegment &&
|
||||
previousSegment.isParallelAndConstrained
|
||||
|
||||
const _enableEqual =
|
||||
secondaryVarDecs.length === 1 &&
|
||||
isAllTooltips &&
|
||||
isOthersLinkedToPrimary &&
|
||||
theTransforms.every(Boolean) &&
|
||||
_forcedSelectionRanges?.codeBasedSelections?.[1]?.type === 'line-end'
|
||||
setEnable(_enableEqual)
|
||||
}, [guiMode, selectionRanges])
|
||||
if (guiMode.mode !== 'sketch') return null
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={async () => {
|
||||
if (!(transformInfos && forecdSelectionRanges)) return
|
||||
const { modifiedAst, tagInfo, valueUsedInTransform, pathToNodeMap } =
|
||||
transformSecondarySketchLinesTagFirst({
|
||||
ast: JSON.parse(JSON.stringify(kclManager.ast)),
|
||||
selectionRanges: forecdSelectionRanges,
|
||||
transformInfos,
|
||||
programMemory: kclManager.programMemory,
|
||||
})
|
||||
const {
|
||||
segName,
|
||||
value,
|
||||
valueNode,
|
||||
variableName,
|
||||
newVariableInsertIndex,
|
||||
sign,
|
||||
}: {
|
||||
segName: string
|
||||
value: number
|
||||
valueNode: Value
|
||||
variableName?: string
|
||||
newVariableInsertIndex: number
|
||||
sign: number
|
||||
} = await getModalInfo({
|
||||
segName: tagInfo?.tag,
|
||||
isSegNameEditable: !tagInfo?.isTagExisting,
|
||||
value: valueUsedInTransform,
|
||||
initialVariableName: 'offset',
|
||||
} as any)
|
||||
if (segName === tagInfo?.tag && value === valueUsedInTransform) {
|
||||
kclManager.updateAst(modifiedAst, true, {
|
||||
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||
})
|
||||
} else {
|
||||
// transform again but forcing certain values
|
||||
const finalValue = removeDoubleNegatives(
|
||||
valueNode as BinaryPart,
|
||||
sign,
|
||||
variableName
|
||||
)
|
||||
const { modifiedAst: _modifiedAst, pathToNodeMap } =
|
||||
transformSecondarySketchLinesTagFirst({
|
||||
ast: kclManager.ast,
|
||||
selectionRanges: forecdSelectionRanges,
|
||||
transformInfos,
|
||||
programMemory: kclManager.programMemory,
|
||||
forceSegName: segName,
|
||||
forceValueUsedInTransform: finalValue,
|
||||
})
|
||||
if (variableName) {
|
||||
const newBody = [..._modifiedAst.body]
|
||||
newBody.splice(
|
||||
newVariableInsertIndex,
|
||||
0,
|
||||
createVariableDeclaration(variableName, valueNode)
|
||||
)
|
||||
_modifiedAst.body = newBody
|
||||
const _forcedSelectionRanges: typeof selectionRanges = {
|
||||
...selectionRanges,
|
||||
codeBasedSelections: [
|
||||
selectionRanges.codeBasedSelections?.[0],
|
||||
shouldUsePreviousSegment
|
||||
? {
|
||||
range: previousSegment.sourceRange,
|
||||
type: 'line-end',
|
||||
}
|
||||
kclManager.updateAst(_modifiedAst, true, {
|
||||
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||
})
|
||||
}
|
||||
}}
|
||||
disabled={!enable}
|
||||
title="Set Perpendicular Distance"
|
||||
>
|
||||
Set Perpendicular Distance
|
||||
</button>
|
||||
: selectionRanges.codeBasedSelections?.[1],
|
||||
],
|
||||
}
|
||||
|
||||
const paths = _forcedSelectionRanges.codeBasedSelections.map(({ range }) =>
|
||||
getNodePathFromSourceRange(kclManager.ast, range)
|
||||
)
|
||||
const nodes = paths.map(
|
||||
(pathToNode) => getNodeFromPath<Value>(kclManager.ast, pathToNode).node
|
||||
)
|
||||
const varDecs = paths.map(
|
||||
(pathToNode) =>
|
||||
getNodeFromPath<VariableDeclarator>(
|
||||
kclManager.ast,
|
||||
pathToNode,
|
||||
'VariableDeclarator'
|
||||
)?.node
|
||||
)
|
||||
const primaryLine = varDecs[0]
|
||||
const secondaryVarDecs = varDecs.slice(1)
|
||||
const isOthersLinkedToPrimary = secondaryVarDecs.every((secondary) =>
|
||||
isSketchVariablesLinked(secondary, primaryLine, kclManager.ast)
|
||||
)
|
||||
const isAllTooltips = nodes.every(
|
||||
(node) =>
|
||||
node?.type === 'CallExpression' &&
|
||||
[
|
||||
...toolTips,
|
||||
'startSketchAt', // TODO probably a better place for this to live
|
||||
].includes(node.callee.name as any)
|
||||
)
|
||||
|
||||
const theTransforms = getTransformInfos(
|
||||
{
|
||||
...selectionRanges,
|
||||
codeBasedSelections: _forcedSelectionRanges.codeBasedSelections.slice(1),
|
||||
},
|
||||
kclManager.ast,
|
||||
'intersect'
|
||||
)
|
||||
|
||||
const _enableEqual =
|
||||
secondaryVarDecs.length === 1 &&
|
||||
isAllTooltips &&
|
||||
isOthersLinkedToPrimary &&
|
||||
theTransforms.every(Boolean) &&
|
||||
_forcedSelectionRanges?.codeBasedSelections?.[1]?.type === 'line-end'
|
||||
|
||||
return {
|
||||
enabled: _enableEqual,
|
||||
transforms: theTransforms,
|
||||
forcedSelectionRanges: _forcedSelectionRanges,
|
||||
}
|
||||
}
|
||||
|
||||
export async function applyConstraintIntersect({
|
||||
selectionRanges,
|
||||
}: {
|
||||
selectionRanges: Selections
|
||||
}): Promise<{
|
||||
modifiedAst: Program
|
||||
pathToNodeMap: PathToNodeMap
|
||||
}> {
|
||||
const { transforms, forcedSelectionRanges } = intersectInfo({
|
||||
selectionRanges,
|
||||
})
|
||||
const { modifiedAst, tagInfo, valueUsedInTransform, pathToNodeMap } =
|
||||
transformSecondarySketchLinesTagFirst({
|
||||
ast: JSON.parse(JSON.stringify(kclManager.ast)),
|
||||
selectionRanges: forcedSelectionRanges,
|
||||
transformInfos: transforms,
|
||||
programMemory: kclManager.programMemory,
|
||||
})
|
||||
const {
|
||||
segName,
|
||||
value,
|
||||
valueNode,
|
||||
variableName,
|
||||
newVariableInsertIndex,
|
||||
sign,
|
||||
} = await getModalInfo({
|
||||
segName: tagInfo?.tag,
|
||||
isSegNameEditable: !tagInfo?.isTagExisting,
|
||||
value: valueUsedInTransform,
|
||||
initialVariableName: 'offset',
|
||||
})
|
||||
if (segName === tagInfo?.tag && Number(value) === valueUsedInTransform) {
|
||||
return {
|
||||
modifiedAst,
|
||||
pathToNodeMap,
|
||||
}
|
||||
}
|
||||
// transform again but forcing certain values
|
||||
const finalValue = removeDoubleNegatives(
|
||||
valueNode as BinaryPart,
|
||||
sign,
|
||||
variableName
|
||||
)
|
||||
const { modifiedAst: _modifiedAst, pathToNodeMap: _pathToNodeMap } =
|
||||
transformSecondarySketchLinesTagFirst({
|
||||
ast: kclManager.ast,
|
||||
selectionRanges: forcedSelectionRanges,
|
||||
transformInfos: transforms,
|
||||
programMemory: kclManager.programMemory,
|
||||
forceSegName: segName,
|
||||
forceValueUsedInTransform: finalValue,
|
||||
})
|
||||
if (variableName) {
|
||||
const newBody = [..._modifiedAst.body]
|
||||
newBody.splice(
|
||||
newVariableInsertIndex,
|
||||
0,
|
||||
createVariableDeclaration(variableName, valueNode)
|
||||
)
|
||||
_modifiedAst.body = newBody
|
||||
}
|
||||
return {
|
||||
modifiedAst: _modifiedAst,
|
||||
pathToNodeMap: _pathToNodeMap,
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
@ -1,75 +1,63 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { toolTips, useStore } from '../../useStore'
|
||||
import { Value } from '../../lang/wasm'
|
||||
import { toolTips } from '../../useStore'
|
||||
import { Selections } from 'lib/selections'
|
||||
import { Program, Value } from '../../lang/wasm'
|
||||
import {
|
||||
getNodePathFromSourceRange,
|
||||
getNodeFromPath,
|
||||
} from '../../lang/queryAst'
|
||||
import {
|
||||
TransformInfo,
|
||||
PathToNodeMap,
|
||||
getRemoveConstraintsTransforms,
|
||||
transformAstSketchLines,
|
||||
} from '../../lang/std/sketchcombos'
|
||||
import { kclManager } from 'lang/KclSinglton'
|
||||
|
||||
/*
|
||||
export const RemoveConstrainingValues = () => {
|
||||
const { guiMode, selectionRanges, setCursor } = useStore((s) => ({
|
||||
guiMode: s.guiMode,
|
||||
selectionRanges: s.selectionRanges,
|
||||
setCursor: s.setCursor,
|
||||
}))
|
||||
const [enableHorz, setEnableHorz] = useState(false)
|
||||
const [transformInfos, setTransformInfos] = useState<TransformInfo[]>()
|
||||
useEffect(() => {
|
||||
const paths = selectionRanges.codeBasedSelections.map(({ range }) =>
|
||||
getNodePathFromSourceRange(kclManager.ast, range)
|
||||
)
|
||||
const nodes = paths.map(
|
||||
(pathToNode) => getNodeFromPath<Value>(kclManager.ast, pathToNode).node
|
||||
)
|
||||
const isAllTooltips = nodes.every(
|
||||
(node) =>
|
||||
node?.type === 'CallExpression' &&
|
||||
toolTips.includes(node.callee.name as any)
|
||||
)
|
||||
|
||||
try {
|
||||
const theTransforms = getRemoveConstraintsTransforms(
|
||||
selectionRanges,
|
||||
kclManager.ast,
|
||||
'removeConstrainingValues'
|
||||
)
|
||||
setTransformInfos(theTransforms)
|
||||
|
||||
const _enableHorz = isAllTooltips && theTransforms.every(Boolean)
|
||||
setEnableHorz(_enableHorz)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}, [guiMode, selectionRanges])
|
||||
if (guiMode.mode !== 'sketch') return null
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={() => {
|
||||
if (!transformInfos) return
|
||||
const { modifiedAst, pathToNodeMap } = transformAstSketchLines({
|
||||
ast: kclManager.ast,
|
||||
selectionRanges,
|
||||
transformInfos,
|
||||
programMemory: kclManager.programMemory,
|
||||
referenceSegName: '',
|
||||
})
|
||||
kclManager.updateAst(modifiedAst, true, {
|
||||
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||
})
|
||||
}}
|
||||
disabled={!enableHorz}
|
||||
title="Remove Constraining Values"
|
||||
>
|
||||
Remove Constraining Values
|
||||
</button>
|
||||
export function removeConstrainingValuesInfo({
|
||||
selectionRanges,
|
||||
}: {
|
||||
selectionRanges: Selections
|
||||
}) {
|
||||
const paths = selectionRanges.codeBasedSelections.map(({ range }) =>
|
||||
getNodePathFromSourceRange(kclManager.ast, range)
|
||||
)
|
||||
const nodes = paths.map(
|
||||
(pathToNode) => getNodeFromPath<Value>(kclManager.ast, pathToNode).node
|
||||
)
|
||||
const isAllTooltips = nodes.every(
|
||||
(node) =>
|
||||
node?.type === 'CallExpression' &&
|
||||
toolTips.includes(node.callee.name as any)
|
||||
)
|
||||
|
||||
try {
|
||||
const transforms = getRemoveConstraintsTransforms(
|
||||
selectionRanges,
|
||||
kclManager.ast,
|
||||
'removeConstrainingValues'
|
||||
)
|
||||
|
||||
const enabled = isAllTooltips && transforms.every(Boolean)
|
||||
return { enabled, transforms }
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
return { enabled: false, transforms: [] }
|
||||
}
|
||||
}
|
||||
|
||||
export function applyRemoveConstrainingValues({
|
||||
selectionRanges,
|
||||
}: {
|
||||
selectionRanges: Selections
|
||||
}): {
|
||||
modifiedAst: Program
|
||||
pathToNodeMap: PathToNodeMap
|
||||
} {
|
||||
const { transforms } = removeConstrainingValuesInfo({ selectionRanges })
|
||||
return transformAstSketchLines({
|
||||
ast: kclManager.ast,
|
||||
selectionRanges,
|
||||
transformInfos: transforms,
|
||||
programMemory: kclManager.programMemory,
|
||||
referenceSegName: '',
|
||||
})
|
||||
}
|
||||
*/
|
||||
|
||||
@ -1,18 +1,19 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { create } from 'react-modal-promise'
|
||||
import { toolTips, useStore } from '../../useStore'
|
||||
import { Value } from '../../lang/wasm'
|
||||
import { toolTips } from '../../useStore'
|
||||
import { Selections } from 'lib/selections'
|
||||
import { BinaryPart, Program, Value } from '../../lang/wasm'
|
||||
import {
|
||||
getNodePathFromSourceRange,
|
||||
getNodeFromPath,
|
||||
} from '../../lang/queryAst'
|
||||
import {
|
||||
TransformInfo,
|
||||
getTransformInfos,
|
||||
transformAstSketchLines,
|
||||
ConstraintType,
|
||||
PathToNodeMap,
|
||||
} from '../../lang/std/sketchcombos'
|
||||
import { SetAngleLengthModal } from '../SetAngleLengthModal'
|
||||
import {
|
||||
SetAngleLengthModal,
|
||||
createSetAngleLengthModal,
|
||||
} from '../SetAngleLengthModal'
|
||||
import {
|
||||
createIdentifier,
|
||||
createVariableDeclaration,
|
||||
@ -20,128 +21,132 @@ import {
|
||||
import { removeDoubleNegatives } from '../AvailableVarsHelpers'
|
||||
import { kclManager } from 'lang/KclSinglton'
|
||||
|
||||
const getModalInfo = create(SetAngleLengthModal as any)
|
||||
const getModalInfo = createSetAngleLengthModal(SetAngleLengthModal)
|
||||
|
||||
type ButtonType = 'xAbs' | 'yAbs' | 'snapToYAxis' | 'snapToXAxis'
|
||||
type Constraint = 'xAbs' | 'yAbs' | 'snapToYAxis' | 'snapToXAxis'
|
||||
|
||||
const buttonLabels: Record<ButtonType, string> = {
|
||||
xAbs: 'Set distance from X Axis',
|
||||
yAbs: 'Set distance from Y Axis',
|
||||
snapToYAxis: 'Snap To Y Axis',
|
||||
snapToXAxis: 'Snap To X Axis',
|
||||
}
|
||||
|
||||
/*
|
||||
export const SetAbsDistance = ({ buttonType }: { buttonType: ButtonType }) => {
|
||||
const { guiMode, selectionRanges, setCursor } = useStore((s) => ({
|
||||
guiMode: s.guiMode,
|
||||
selectionRanges: s.selectionRanges,
|
||||
setCursor: s.setCursor,
|
||||
}))
|
||||
const disType: ConstraintType =
|
||||
buttonType === 'xAbs' || buttonType === 'yAbs'
|
||||
? buttonType
|
||||
: buttonType === 'snapToYAxis'
|
||||
export function absDistanceInfo({
|
||||
selectionRanges,
|
||||
constraint,
|
||||
}: {
|
||||
selectionRanges: Selections
|
||||
constraint: Constraint
|
||||
}) {
|
||||
const disType =
|
||||
constraint === 'xAbs' || constraint === 'yAbs'
|
||||
? constraint
|
||||
: constraint === 'snapToYAxis'
|
||||
? 'xAbs'
|
||||
: 'yAbs'
|
||||
const [enableAngLen, setEnableAngLen] = useState(false)
|
||||
const [transformInfos, setTransformInfos] = useState<TransformInfo[]>()
|
||||
useEffect(() => {
|
||||
const paths = selectionRanges.codeBasedSelections.map(({ range }) =>
|
||||
getNodePathFromSourceRange(kclManager.ast, range)
|
||||
)
|
||||
const nodes = paths.map(
|
||||
(pathToNode) =>
|
||||
getNodeFromPath<Value>(kclManager.ast, pathToNode, 'CallExpression')
|
||||
.node
|
||||
)
|
||||
const isAllTooltips = nodes.every(
|
||||
(node) =>
|
||||
node?.type === 'CallExpression' &&
|
||||
toolTips.includes(node.callee.name as any)
|
||||
)
|
||||
|
||||
const theTransforms = getTransformInfos(
|
||||
selectionRanges,
|
||||
kclManager.ast,
|
||||
disType
|
||||
)
|
||||
setTransformInfos(theTransforms)
|
||||
|
||||
const enableY =
|
||||
disType === 'yAbs' &&
|
||||
selectionRanges.otherSelections.length === 1 &&
|
||||
selectionRanges.otherSelections[0] === 'x-axis' // select the x axis to set the distance from it i.e. y
|
||||
const enableX =
|
||||
disType === 'xAbs' &&
|
||||
selectionRanges.otherSelections.length === 1 &&
|
||||
selectionRanges.otherSelections[0] === 'y-axis' // select the y axis to set the distance from it i.e. x
|
||||
|
||||
const _enableHorz =
|
||||
isAllTooltips &&
|
||||
theTransforms.every(Boolean) &&
|
||||
selectionRanges.codeBasedSelections.length === 1 &&
|
||||
(enableX || enableY)
|
||||
setEnableAngLen(_enableHorz)
|
||||
}, [guiMode, selectionRanges])
|
||||
if (guiMode.mode !== 'sketch') return null
|
||||
|
||||
const isAlign = buttonType === 'snapToYAxis' || buttonType === 'snapToXAxis'
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={async () => {
|
||||
if (!transformInfos) return
|
||||
const { valueUsedInTransform } = transformAstSketchLines({
|
||||
ast: JSON.parse(JSON.stringify(kclManager.ast)),
|
||||
selectionRanges: selectionRanges,
|
||||
transformInfos,
|
||||
programMemory: kclManager.programMemory,
|
||||
referenceSegName: '',
|
||||
})
|
||||
try {
|
||||
let forceVal = valueUsedInTransform || 0
|
||||
const { valueNode, variableName, newVariableInsertIndex, sign } =
|
||||
await (!isAlign &&
|
||||
getModalInfo({
|
||||
value: forceVal,
|
||||
valueName: disType === 'yAbs' ? 'yDis' : 'xDis',
|
||||
} as any))
|
||||
let finalValue = isAlign
|
||||
? createIdentifier('_0')
|
||||
: removeDoubleNegatives(valueNode, sign, variableName)
|
||||
|
||||
const { modifiedAst: _modifiedAst, pathToNodeMap } =
|
||||
transformAstSketchLines({
|
||||
ast: JSON.parse(JSON.stringify(kclManager.ast)),
|
||||
selectionRanges: selectionRanges,
|
||||
transformInfos,
|
||||
programMemory: kclManager.programMemory,
|
||||
referenceSegName: '',
|
||||
forceValueUsedInTransform: finalValue,
|
||||
})
|
||||
if (variableName) {
|
||||
const newBody = [..._modifiedAst.body]
|
||||
newBody.splice(
|
||||
newVariableInsertIndex,
|
||||
0,
|
||||
createVariableDeclaration(variableName, valueNode)
|
||||
)
|
||||
_modifiedAst.body = newBody
|
||||
}
|
||||
|
||||
kclManager.updateAst(_modifiedAst, true, {
|
||||
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||
})
|
||||
} catch (e) {
|
||||
console.log('error', e)
|
||||
}
|
||||
}}
|
||||
disabled={!enableAngLen}
|
||||
title={buttonLabels[buttonType]}
|
||||
>
|
||||
{buttonLabels[buttonType]}
|
||||
</button>
|
||||
const paths = selectionRanges.codeBasedSelections.map(({ range }) =>
|
||||
getNodePathFromSourceRange(kclManager.ast, range)
|
||||
)
|
||||
const nodes = paths.map(
|
||||
(pathToNode) =>
|
||||
getNodeFromPath<Value>(kclManager.ast, pathToNode, 'CallExpression').node
|
||||
)
|
||||
const isAllTooltips = nodes.every(
|
||||
(node) =>
|
||||
node?.type === 'CallExpression' &&
|
||||
toolTips.includes(node.callee.name as any)
|
||||
)
|
||||
|
||||
const transforms = getTransformInfos(selectionRanges, kclManager.ast, disType)
|
||||
|
||||
const enableY =
|
||||
disType === 'yAbs' &&
|
||||
selectionRanges.otherSelections.length === 1 &&
|
||||
selectionRanges.otherSelections[0] === 'x-axis' // select the x axis to set the distance from it i.e. y
|
||||
const enableX =
|
||||
disType === 'xAbs' &&
|
||||
selectionRanges.otherSelections.length === 1 &&
|
||||
selectionRanges.otherSelections[0] === 'y-axis' // select the y axis to set the distance from it i.e. x
|
||||
|
||||
const enabled =
|
||||
isAllTooltips &&
|
||||
transforms.every(Boolean) &&
|
||||
selectionRanges.codeBasedSelections.length === 1 &&
|
||||
(enableX || enableY)
|
||||
|
||||
return { enabled, transforms }
|
||||
}
|
||||
|
||||
export async function applyConstraintAbsDistance({
|
||||
selectionRanges,
|
||||
constraint,
|
||||
}: {
|
||||
selectionRanges: Selections
|
||||
constraint: 'xAbs' | 'yAbs'
|
||||
}): Promise<{
|
||||
modifiedAst: Program
|
||||
pathToNodeMap: PathToNodeMap
|
||||
}> {
|
||||
const transformInfos = absDistanceInfo({
|
||||
selectionRanges,
|
||||
constraint,
|
||||
}).transforms
|
||||
const { valueUsedInTransform } = transformAstSketchLines({
|
||||
ast: JSON.parse(JSON.stringify(kclManager.ast)),
|
||||
selectionRanges: selectionRanges,
|
||||
transformInfos,
|
||||
programMemory: kclManager.programMemory,
|
||||
referenceSegName: '',
|
||||
})
|
||||
let forceVal = valueUsedInTransform || 0
|
||||
const { valueNode, variableName, newVariableInsertIndex, sign } =
|
||||
await getModalInfo({
|
||||
value: forceVal,
|
||||
valueName: constraint === 'yAbs' ? 'yDis' : 'xDis',
|
||||
})
|
||||
let finalValue = removeDoubleNegatives(
|
||||
valueNode as BinaryPart,
|
||||
sign,
|
||||
variableName
|
||||
)
|
||||
|
||||
const { modifiedAst: _modifiedAst, pathToNodeMap } = transformAstSketchLines({
|
||||
ast: JSON.parse(JSON.stringify(kclManager.ast)),
|
||||
selectionRanges: selectionRanges,
|
||||
transformInfos,
|
||||
programMemory: kclManager.programMemory,
|
||||
referenceSegName: '',
|
||||
forceValueUsedInTransform: finalValue,
|
||||
})
|
||||
if (variableName) {
|
||||
const newBody = [..._modifiedAst.body]
|
||||
newBody.splice(
|
||||
newVariableInsertIndex,
|
||||
0,
|
||||
createVariableDeclaration(variableName, valueNode)
|
||||
)
|
||||
_modifiedAst.body = newBody
|
||||
}
|
||||
return { modifiedAst: _modifiedAst, pathToNodeMap }
|
||||
}
|
||||
|
||||
export function applyConstraintAxisAlign({
|
||||
selectionRanges,
|
||||
constraint,
|
||||
}: {
|
||||
selectionRanges: Selections
|
||||
constraint: 'snapToYAxis' | 'snapToXAxis'
|
||||
}): {
|
||||
modifiedAst: Program
|
||||
pathToNodeMap: PathToNodeMap
|
||||
} {
|
||||
const transformInfos = absDistanceInfo({
|
||||
selectionRanges,
|
||||
constraint,
|
||||
}).transforms
|
||||
|
||||
let finalValue = createIdentifier('_0')
|
||||
|
||||
return transformAstSketchLines({
|
||||
ast: JSON.parse(JSON.stringify(kclManager.ast)),
|
||||
selectionRanges: selectionRanges,
|
||||
transformInfos,
|
||||
programMemory: kclManager.programMemory,
|
||||
referenceSegName: '',
|
||||
forceValueUsedInTransform: finalValue,
|
||||
})
|
||||
}
|
||||
*/
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { create } from 'react-modal-promise'
|
||||
import { Selections, toolTips, useStore } from '../../useStore'
|
||||
import { toolTips } from '../../useStore'
|
||||
import { Selections } from 'lib/selections'
|
||||
import { BinaryPart, Program, Value, VariableDeclarator } from '../../lang/wasm'
|
||||
import {
|
||||
getNodePathFromSourceRange,
|
||||
@ -8,107 +7,16 @@ import {
|
||||
} from '../../lang/queryAst'
|
||||
import { isSketchVariablesLinked } from '../../lang/std/sketchConstraints'
|
||||
import {
|
||||
TransformInfo,
|
||||
transformSecondarySketchLinesTagFirst,
|
||||
getTransformInfos,
|
||||
PathToNodeMap,
|
||||
} from '../../lang/std/sketchcombos'
|
||||
import { GetInfoModal } from '../SetHorVertDistanceModal'
|
||||
import { GetInfoModal, createInfoModal } from '../SetHorVertDistanceModal'
|
||||
import { createVariableDeclaration } from '../../lang/modifyAst'
|
||||
import { removeDoubleNegatives } from '../AvailableVarsHelpers'
|
||||
import { kclManager } from 'lang/KclSinglton'
|
||||
|
||||
const getModalInfo = create(GetInfoModal as any)
|
||||
|
||||
/*
|
||||
export const SetAngleBetween = () => {
|
||||
const { guiMode, selectionRanges, setCursor } = useStore((s) => ({
|
||||
guiMode: s.guiMode,
|
||||
selectionRanges: s.selectionRanges,
|
||||
setCursor: s.setCursor,
|
||||
}))
|
||||
const [enable, setEnable] = useState(false)
|
||||
const [transformInfos, setTransformInfos] = useState<TransformInfo[]>()
|
||||
useEffect(() => {
|
||||
const { enabled, transforms } = angleBetweenInfo({ selectionRanges })
|
||||
setTransformInfos(transforms)
|
||||
setEnable(enabled)
|
||||
}, [guiMode, selectionRanges])
|
||||
if (guiMode.mode !== 'sketch') return null
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={async () => {
|
||||
if (!transformInfos) return
|
||||
const { modifiedAst, tagInfo, valueUsedInTransform, pathToNodeMap } =
|
||||
transformSecondarySketchLinesTagFirst({
|
||||
ast: JSON.parse(JSON.stringify(kclManager.ast)),
|
||||
selectionRanges,
|
||||
transformInfos,
|
||||
programMemory: kclManager.programMemory,
|
||||
})
|
||||
const {
|
||||
segName,
|
||||
value,
|
||||
valueNode,
|
||||
variableName,
|
||||
newVariableInsertIndex,
|
||||
sign,
|
||||
}: {
|
||||
segName: string
|
||||
value: number
|
||||
valueNode: Value
|
||||
variableName?: string
|
||||
newVariableInsertIndex: number
|
||||
sign: number
|
||||
} = await getModalInfo({
|
||||
segName: tagInfo?.tag,
|
||||
isSegNameEditable: !tagInfo?.isTagExisting,
|
||||
value: valueUsedInTransform,
|
||||
initialVariableName: 'angle',
|
||||
} as any)
|
||||
if (segName === tagInfo?.tag && value === valueUsedInTransform) {
|
||||
kclManager.updateAst(modifiedAst, true, {
|
||||
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||
})
|
||||
} else {
|
||||
const finalValue = removeDoubleNegatives(
|
||||
valueNode as BinaryPart,
|
||||
sign,
|
||||
variableName
|
||||
)
|
||||
// transform again but forcing certain values
|
||||
const { modifiedAst: _modifiedAst, pathToNodeMap } =
|
||||
transformSecondarySketchLinesTagFirst({
|
||||
ast: kclManager.ast,
|
||||
selectionRanges,
|
||||
transformInfos,
|
||||
programMemory: kclManager.programMemory,
|
||||
forceSegName: segName,
|
||||
forceValueUsedInTransform: finalValue,
|
||||
})
|
||||
if (variableName) {
|
||||
const newBody = [..._modifiedAst.body]
|
||||
newBody.splice(
|
||||
newVariableInsertIndex,
|
||||
0,
|
||||
createVariableDeclaration(variableName, valueNode)
|
||||
)
|
||||
_modifiedAst.body = newBody
|
||||
}
|
||||
kclManager.updateAst(_modifiedAst, true, {
|
||||
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||
})
|
||||
}
|
||||
}}
|
||||
disabled={!enable}
|
||||
title="Set Angle Between"
|
||||
>
|
||||
Set Angle Between
|
||||
</button>
|
||||
)
|
||||
}
|
||||
*/
|
||||
const getModalInfo = createInfoModal(GetInfoModal)
|
||||
|
||||
export function angleBetweenInfo({
|
||||
selectionRanges,
|
||||
@ -183,28 +91,17 @@ export async function applyConstraintAngleBetween({
|
||||
variableName,
|
||||
newVariableInsertIndex,
|
||||
sign,
|
||||
}: {
|
||||
segName: string
|
||||
value: number
|
||||
valueNode: Value
|
||||
variableName?: string
|
||||
newVariableInsertIndex: number
|
||||
sign: number
|
||||
} = await getModalInfo({
|
||||
segName: tagInfo?.tag,
|
||||
isSegNameEditable: !tagInfo?.isTagExisting,
|
||||
value: valueUsedInTransform,
|
||||
initialVariableName: 'angle',
|
||||
} as any)
|
||||
if (segName === tagInfo?.tag && value === valueUsedInTransform) {
|
||||
if (segName === tagInfo?.tag && Number(value) === valueUsedInTransform) {
|
||||
return {
|
||||
modifiedAst,
|
||||
pathToNodeMap,
|
||||
}
|
||||
// kclManager.updateAst(modifiedAst, true, {
|
||||
// TODO handle cursor
|
||||
// callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||
// })
|
||||
}
|
||||
|
||||
const finalValue = removeDoubleNegatives(
|
||||
@ -235,8 +132,4 @@ export async function applyConstraintAngleBetween({
|
||||
modifiedAst: _modifiedAst,
|
||||
pathToNodeMap: _pathToNodeMap,
|
||||
}
|
||||
// kclManager.updateAst(_modifiedAst, true, {
|
||||
// TODO handle cursor
|
||||
// callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||
// })
|
||||
}
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { create } from 'react-modal-promise'
|
||||
import { toolTips, useStore } from '../../useStore'
|
||||
import { toolTips } from '../../useStore'
|
||||
import { BinaryPart, Program, Value, VariableDeclarator } from '../../lang/wasm'
|
||||
import {
|
||||
getNodePathFromSourceRange,
|
||||
@ -8,139 +6,17 @@ import {
|
||||
} from '../../lang/queryAst'
|
||||
import { isSketchVariablesLinked } from '../../lang/std/sketchConstraints'
|
||||
import {
|
||||
TransformInfo,
|
||||
transformSecondarySketchLinesTagFirst,
|
||||
getTransformInfos,
|
||||
ConstraintType,
|
||||
PathToNodeMap,
|
||||
} from '../../lang/std/sketchcombos'
|
||||
import { GetInfoModal } from '../SetHorVertDistanceModal'
|
||||
import { GetInfoModal, createInfoModal } from '../SetHorVertDistanceModal'
|
||||
import { createLiteral, createVariableDeclaration } from '../../lang/modifyAst'
|
||||
import { removeDoubleNegatives } from '../AvailableVarsHelpers'
|
||||
import { kclManager } from 'lang/KclSinglton'
|
||||
import { Selections } from 'useStore'
|
||||
import { Selections } from 'lib/selections'
|
||||
|
||||
const getModalInfo = create(GetInfoModal as any)
|
||||
|
||||
type ButtonType =
|
||||
| 'setHorzDistance'
|
||||
| 'setVertDistance'
|
||||
| 'alignEndsHorizontally'
|
||||
| 'alignEndsVertically'
|
||||
|
||||
const buttonLabels: Record<ButtonType, string> = {
|
||||
setHorzDistance: 'Set Horizontal Distance',
|
||||
setVertDistance: 'Set Vertical Distance',
|
||||
alignEndsHorizontally: 'Align Ends Horizontally',
|
||||
alignEndsVertically: 'Align Ends Vertically',
|
||||
}
|
||||
|
||||
/*
|
||||
export const SetHorzVertDistance = ({
|
||||
buttonType,
|
||||
}: {
|
||||
buttonType: ButtonType
|
||||
}) => {
|
||||
const { guiMode, selectionRanges, setCursor } = useStore((s) => ({
|
||||
guiMode: s.guiMode,
|
||||
selectionRanges: s.selectionRanges,
|
||||
setCursor: s.setCursor,
|
||||
}))
|
||||
const constraint: ConstraintType =
|
||||
buttonType === 'setHorzDistance' || buttonType === 'setVertDistance'
|
||||
? buttonType
|
||||
: buttonType === 'alignEndsHorizontally'
|
||||
? 'setVertDistance'
|
||||
: 'setHorzDistance'
|
||||
const [enable, setEnable] = useState(false)
|
||||
const [transformInfos, setTransformInfos] = useState<TransformInfo[]>()
|
||||
useEffect(() => {
|
||||
const { transforms, enabled } = horzVertDistanceInfo({
|
||||
selectionRanges,
|
||||
constraint,
|
||||
})
|
||||
setTransformInfos(transforms)
|
||||
setEnable(enabled)
|
||||
}, [guiMode, selectionRanges])
|
||||
if (guiMode.mode !== 'sketch') return null
|
||||
|
||||
const isAlign =
|
||||
buttonType === 'alignEndsHorizontally' ||
|
||||
buttonType === 'alignEndsVertically'
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={async () => {
|
||||
if (!transformInfos) return
|
||||
const { modifiedAst, tagInfo, valueUsedInTransform, pathToNodeMap } =
|
||||
transformSecondarySketchLinesTagFirst({
|
||||
ast: JSON.parse(JSON.stringify(kclManager.ast)),
|
||||
selectionRanges,
|
||||
transformInfos,
|
||||
programMemory: kclManager.programMemory,
|
||||
})
|
||||
const {
|
||||
segName,
|
||||
value,
|
||||
valueNode,
|
||||
variableName,
|
||||
newVariableInsertIndex,
|
||||
sign,
|
||||
}: {
|
||||
segName: string
|
||||
value: number
|
||||
valueNode: Value
|
||||
variableName?: string
|
||||
newVariableInsertIndex: number
|
||||
sign: number
|
||||
} = await (!isAlign &&
|
||||
getModalInfo({
|
||||
segName: tagInfo?.tag,
|
||||
isSegNameEditable: !tagInfo?.isTagExisting,
|
||||
value: valueUsedInTransform,
|
||||
initialVariableName:
|
||||
constraint === 'setHorzDistance' ? 'xDis' : 'yDis',
|
||||
} as any))
|
||||
if (segName === tagInfo?.tag && value === valueUsedInTransform) {
|
||||
kclManager.updateAst(modifiedAst, true, {
|
||||
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||
})
|
||||
} else {
|
||||
let finalValue = isAlign
|
||||
? createLiteral(0)
|
||||
: removeDoubleNegatives(valueNode as BinaryPart, sign, variableName)
|
||||
// transform again but forcing certain values
|
||||
const { modifiedAst: _modifiedAst, pathToNodeMap } =
|
||||
transformSecondarySketchLinesTagFirst({
|
||||
ast: kclManager.ast,
|
||||
selectionRanges,
|
||||
transformInfos,
|
||||
programMemory: kclManager.programMemory,
|
||||
forceSegName: segName,
|
||||
forceValueUsedInTransform: finalValue,
|
||||
})
|
||||
if (variableName) {
|
||||
const newBody = [..._modifiedAst.body]
|
||||
newBody.splice(
|
||||
newVariableInsertIndex,
|
||||
0,
|
||||
createVariableDeclaration(variableName, valueNode)
|
||||
)
|
||||
_modifiedAst.body = newBody
|
||||
}
|
||||
kclManager.updateAst(_modifiedAst, true, {
|
||||
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||
})
|
||||
}
|
||||
}}
|
||||
disabled={!enable}
|
||||
title={buttonLabels[buttonType]}
|
||||
>
|
||||
{buttonLabels[buttonType]}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
*/
|
||||
const getModalInfo = createInfoModal(GetInfoModal)
|
||||
|
||||
export function horzVertDistanceInfo({
|
||||
selectionRanges,
|
||||
@ -201,7 +77,7 @@ export async function applyConstraintHorzVertDistance({
|
||||
}: {
|
||||
selectionRanges: Selections
|
||||
constraint: 'setHorzDistance' | 'setVertDistance'
|
||||
isAlign?: boolean
|
||||
isAlign?: false
|
||||
}): Promise<{
|
||||
modifiedAst: Program
|
||||
pathToNodeMap: PathToNodeMap
|
||||
@ -224,29 +100,17 @@ export async function applyConstraintHorzVertDistance({
|
||||
variableName,
|
||||
newVariableInsertIndex,
|
||||
sign,
|
||||
}: {
|
||||
segName: string
|
||||
value: number
|
||||
valueNode: Value
|
||||
variableName?: string
|
||||
newVariableInsertIndex: number
|
||||
sign: number
|
||||
} = await (!isAlign &&
|
||||
getModalInfo({
|
||||
segName: tagInfo?.tag,
|
||||
isSegNameEditable: !tagInfo?.isTagExisting,
|
||||
value: valueUsedInTransform,
|
||||
initialVariableName: constraint === 'setHorzDistance' ? 'xDis' : 'yDis',
|
||||
} as any))
|
||||
if (segName === tagInfo?.tag && value === valueUsedInTransform) {
|
||||
} = await getModalInfo({
|
||||
segName: tagInfo?.tag,
|
||||
isSegNameEditable: !tagInfo?.isTagExisting,
|
||||
value: valueUsedInTransform,
|
||||
initialVariableName: constraint === 'setHorzDistance' ? 'xDis' : 'yDis',
|
||||
} as any)
|
||||
if (segName === tagInfo?.tag && Number(value) === valueUsedInTransform) {
|
||||
return {
|
||||
modifiedAst,
|
||||
pathToNodeMap,
|
||||
}
|
||||
// TODO handle cursor stuff
|
||||
// kclManager.updateAst(modifiedAst, true, {
|
||||
// callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||
// })
|
||||
} else {
|
||||
let finalValue = isAlign
|
||||
? createLiteral(0)
|
||||
@ -274,10 +138,6 @@ export async function applyConstraintHorzVertDistance({
|
||||
modifiedAst: _modifiedAst,
|
||||
pathToNodeMap,
|
||||
}
|
||||
// TODO handle cursor stuff
|
||||
// kclManager.updateAst(_modifiedAst, true, {
|
||||
// callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||
// })
|
||||
}
|
||||
}
|
||||
|
||||
@ -307,8 +167,4 @@ export function applyConstraintHorzVertAlign({
|
||||
modifiedAst: modifiedAst,
|
||||
pathToNodeMap,
|
||||
}
|
||||
// TODO handle cursor stuff
|
||||
// kclManager.updateAst(_modifiedAst, true, {
|
||||
// callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||
// })
|
||||
}
|
||||
|
||||
@ -1,18 +1,19 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { create } from 'react-modal-promise'
|
||||
import { Selections, toolTips, useStore } from '../../useStore'
|
||||
import { Program, Value } from '../../lang/wasm'
|
||||
import { toolTips } from '../../useStore'
|
||||
import { Selections } from 'lib/selections'
|
||||
import { BinaryPart, Program, Value } from '../../lang/wasm'
|
||||
import {
|
||||
getNodePathFromSourceRange,
|
||||
getNodeFromPath,
|
||||
} from '../../lang/queryAst'
|
||||
import {
|
||||
PathToNodeMap,
|
||||
TransformInfo,
|
||||
getTransformInfos,
|
||||
transformAstSketchLines,
|
||||
} from '../../lang/std/sketchcombos'
|
||||
import { SetAngleLengthModal } from '../SetAngleLengthModal'
|
||||
import {
|
||||
SetAngleLengthModal,
|
||||
createSetAngleLengthModal,
|
||||
} from '../SetAngleLengthModal'
|
||||
import {
|
||||
createBinaryExpressionWithUnary,
|
||||
createIdentifier,
|
||||
@ -22,128 +23,7 @@ import { removeDoubleNegatives } from '../AvailableVarsHelpers'
|
||||
import { normaliseAngle } from '../../lib/utils'
|
||||
import { kclManager } from 'lang/KclSinglton'
|
||||
|
||||
const getModalInfo = create(SetAngleLengthModal as any)
|
||||
|
||||
type ButtonType = 'setAngle' | 'setLength'
|
||||
|
||||
const buttonLabels: Record<ButtonType, string> = {
|
||||
setAngle: 'Set Angle',
|
||||
setLength: 'Set Length',
|
||||
}
|
||||
|
||||
/*
|
||||
export const SetAngleLength = ({
|
||||
angleOrLength,
|
||||
}: {
|
||||
angleOrLength: ButtonType
|
||||
}) => {
|
||||
const { guiMode, selectionRanges, setCursor } = useStore((s) => ({
|
||||
guiMode: s.guiMode,
|
||||
selectionRanges: s.selectionRanges,
|
||||
setCursor: s.setCursor,
|
||||
}))
|
||||
const [enableAngLen, setEnableAngLen] = useState(false)
|
||||
const [transformInfos, setTransformInfos] = useState<TransformInfo[]>()
|
||||
useEffect(() => {
|
||||
const { enabled, transforms } = setAngleLengthInfo({
|
||||
selectionRanges,
|
||||
angleOrLength,
|
||||
})
|
||||
|
||||
setTransformInfos(transforms)
|
||||
setEnableAngLen(enabled)
|
||||
}, [guiMode, selectionRanges])
|
||||
if (guiMode.mode !== 'sketch') return null
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={async () => {
|
||||
if (!transformInfos) return
|
||||
const { valueUsedInTransform } = transformAstSketchLines({
|
||||
ast: JSON.parse(JSON.stringify(kclManager.ast)),
|
||||
selectionRanges,
|
||||
transformInfos,
|
||||
programMemory: kclManager.programMemory,
|
||||
referenceSegName: '',
|
||||
})
|
||||
try {
|
||||
const isReferencingYAxis =
|
||||
selectionRanges.otherSelections.length === 1 &&
|
||||
selectionRanges.otherSelections[0] === 'y-axis'
|
||||
const isReferencingYAxisAngle =
|
||||
isReferencingYAxis && angleOrLength === 'setAngle'
|
||||
|
||||
const isReferencingXAxis =
|
||||
selectionRanges.otherSelections.length === 1 &&
|
||||
selectionRanges.otherSelections[0] === 'x-axis'
|
||||
const isReferencingXAxisAngle =
|
||||
isReferencingXAxis && angleOrLength === 'setAngle'
|
||||
|
||||
let forceVal = valueUsedInTransform || 0
|
||||
let calcIdentifier = createIdentifier('_0')
|
||||
if (isReferencingYAxisAngle) {
|
||||
calcIdentifier = createIdentifier(forceVal < 0 ? '_270' : '_90')
|
||||
forceVal = normaliseAngle(forceVal + (forceVal < 0 ? 90 : -90))
|
||||
} else if (isReferencingXAxisAngle) {
|
||||
calcIdentifier = createIdentifier(
|
||||
Math.abs(forceVal) > 90 ? '_180' : '_0'
|
||||
)
|
||||
forceVal =
|
||||
Math.abs(forceVal) > 90
|
||||
? normaliseAngle(forceVal - 180)
|
||||
: forceVal
|
||||
}
|
||||
const { valueNode, variableName, newVariableInsertIndex, sign } =
|
||||
await getModalInfo({
|
||||
value: forceVal,
|
||||
valueName: angleOrLength === 'setAngle' ? 'angle' : 'length',
|
||||
shouldCreateVariable: true,
|
||||
} as any)
|
||||
let finalValue = removeDoubleNegatives(valueNode, sign, variableName)
|
||||
if (
|
||||
isReferencingYAxisAngle ||
|
||||
(isReferencingXAxisAngle && calcIdentifier.name !== '_0')
|
||||
) {
|
||||
finalValue = createBinaryExpressionWithUnary([
|
||||
calcIdentifier,
|
||||
finalValue,
|
||||
])
|
||||
}
|
||||
|
||||
const { modifiedAst: _modifiedAst, pathToNodeMap } =
|
||||
transformAstSketchLines({
|
||||
ast: JSON.parse(JSON.stringify(kclManager.ast)),
|
||||
selectionRanges,
|
||||
transformInfos,
|
||||
programMemory: kclManager.programMemory,
|
||||
referenceSegName: '',
|
||||
forceValueUsedInTransform: finalValue,
|
||||
})
|
||||
if (variableName) {
|
||||
const newBody = [..._modifiedAst.body]
|
||||
newBody.splice(
|
||||
newVariableInsertIndex,
|
||||
0,
|
||||
createVariableDeclaration(variableName, valueNode)
|
||||
)
|
||||
_modifiedAst.body = newBody
|
||||
}
|
||||
|
||||
kclManager.updateAst(_modifiedAst, true, {
|
||||
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||
})
|
||||
} catch (e) {
|
||||
console.log('erorr', e)
|
||||
}
|
||||
}}
|
||||
disabled={!enableAngLen}
|
||||
title={buttonLabels[angleOrLength]}
|
||||
>
|
||||
{buttonLabels[angleOrLength]}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
*/
|
||||
const getModalInfo = createSetAngleLengthModal(SetAngleLengthModal)
|
||||
|
||||
export function setAngleLengthInfo({
|
||||
selectionRanges,
|
||||
@ -220,8 +100,13 @@ export async function applyConstraintAngleLength({
|
||||
value: forceVal,
|
||||
valueName: angleOrLength === 'setAngle' ? 'angle' : 'length',
|
||||
shouldCreateVariable: true,
|
||||
} as any)
|
||||
let finalValue = removeDoubleNegatives(valueNode, sign, variableName)
|
||||
})
|
||||
|
||||
let finalValue = removeDoubleNegatives(
|
||||
valueNode as BinaryPart,
|
||||
sign,
|
||||
variableName
|
||||
)
|
||||
if (
|
||||
isReferencingYAxisAngle ||
|
||||
(isReferencingXAxisAngle && calcIdentifier.name !== '_0')
|
||||
@ -251,9 +136,6 @@ export async function applyConstraintAngleLength({
|
||||
modifiedAst: _modifiedAst,
|
||||
pathToNodeMap,
|
||||
}
|
||||
// kclManager.updateAst(_modifiedAst, true, {
|
||||
// callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||
// })
|
||||
} catch (e) {
|
||||
console.log('erorr', e)
|
||||
throw e
|
||||
|
||||
229
src/components/Tooltip.module.css
Normal file
@ -0,0 +1,229 @@
|
||||
/* Adapted from https://github.com/argyleink/gui-challenges/blob/main/tooltips/tool-tip.css */
|
||||
|
||||
.tooltip {
|
||||
/* internal CSS vars */
|
||||
--_delay: 200ms;
|
||||
--_p-inline: 1ch;
|
||||
--_p-block: 4px;
|
||||
--_triangle-size: 7px;
|
||||
/* --_bg: hsl(0 0% 20%); */
|
||||
--_bg: var(--chalkboard-10);
|
||||
--_shadow-alpha: 20%;
|
||||
|
||||
/* Used to power spacing and layout for RTL languages */
|
||||
--isRTL: -1;
|
||||
|
||||
/* Using conic gradients to get a clear tip triangle */
|
||||
--_bottom-tip: conic-gradient(
|
||||
from -30deg at bottom,
|
||||
#0000,
|
||||
#000 1deg 60deg,
|
||||
#0000 61deg
|
||||
)
|
||||
bottom / 100% 50% no-repeat;
|
||||
--_top-tip: conic-gradient(
|
||||
from 150deg at top,
|
||||
#0000,
|
||||
#000 1deg 60deg,
|
||||
#0000 61deg
|
||||
)
|
||||
top / 100% 50% no-repeat;
|
||||
--_right-tip: conic-gradient(
|
||||
from -120deg at right,
|
||||
#0000,
|
||||
#000 1deg 60deg,
|
||||
#0000 61deg
|
||||
)
|
||||
right / 50% 100% no-repeat;
|
||||
--_left-tip: conic-gradient(
|
||||
from 60deg at left,
|
||||
#0000,
|
||||
#000 1deg 60deg,
|
||||
#0000 61deg
|
||||
)
|
||||
left / 50% 100% no-repeat;
|
||||
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
|
||||
/* The parts that will be transitioned */
|
||||
opacity: 0;
|
||||
transform: translate(var(--_x, 0), var(--_y, 0));
|
||||
transition: transform 0.15s ease-out, opacity 0.11s ease-out;
|
||||
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
inline-size: max-content;
|
||||
max-inline-size: 25ch;
|
||||
text-align: start;
|
||||
font-family: var(--mono-font-family);
|
||||
text-transform: none;
|
||||
font-size: 0.9rem;
|
||||
font-weight: normal;
|
||||
line-height: initial;
|
||||
letter-spacing: 0;
|
||||
padding: var(--_p-block) var(--_p-inline);
|
||||
margin: 0;
|
||||
border-radius: 3px;
|
||||
background: var(--_bg);
|
||||
@apply text-chalkboard-110;
|
||||
will-change: filter;
|
||||
filter: drop-shadow(0 1px 3px hsl(0 0% 0% / var(--_shadow-alpha)))
|
||||
drop-shadow(0 6px 12px hsl(0 0% 0% / var(--_shadow-alpha)));
|
||||
}
|
||||
|
||||
:global(.dark) .tooltip {
|
||||
--_bg: var(--chalkboard-110);
|
||||
@apply text-chalkboard-10;
|
||||
}
|
||||
|
||||
/* TODO we don't support a light theme yet */
|
||||
/* @media (prefers-color-scheme: light) {
|
||||
.tooltip {
|
||||
--_bg: white;
|
||||
--_shadow-alpha: 15%;
|
||||
}
|
||||
} */
|
||||
|
||||
.tooltip:dir(rtl) {
|
||||
--isRTL: 1;
|
||||
}
|
||||
|
||||
/* :has and :is are pretty fresh CSS pseudo-selectors, may not see full support */
|
||||
:has(> .tooltip) {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:is(:hover, :focus-visible, :active) > .tooltip {
|
||||
opacity: 1;
|
||||
transition-delay: var(--_delay);
|
||||
}
|
||||
|
||||
:is(:focus, :focus-visible, :focus-within) > .tooltip {
|
||||
--_delay: 0 !important;
|
||||
}
|
||||
|
||||
/* prepend some prose for screen readers only */
|
||||
.tooltip::before {
|
||||
content: '; Has tooltip: ';
|
||||
clip: rect(1px, 1px, 1px, 1px);
|
||||
clip-path: inset(50%);
|
||||
height: 1px;
|
||||
width: 1px;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
/* tooltip shape is a pseudo element so we can cast a shadow */
|
||||
.tooltip::after {
|
||||
content: '';
|
||||
background: var(--_bg);
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
inset: 0;
|
||||
mask: var(--_tip);
|
||||
}
|
||||
|
||||
.tooltip.top,
|
||||
.tooltip.blockStart,
|
||||
.tooltip.bottom,
|
||||
.tooltip.blockEnd {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* TOP || BLOCK-START */
|
||||
.tooltip.top,
|
||||
.tooltip.blockStart {
|
||||
inset-inline-start: 50%;
|
||||
inset-block-end: calc(100% + var(--_p-block) + var(--_triangle-size));
|
||||
--_x: calc(50% * var(--isRTL));
|
||||
}
|
||||
|
||||
.tooltip.top::after,
|
||||
.tooltip.tooltip.blockStart::after {
|
||||
--_tip: var(--_bottom-tip);
|
||||
inset-block-end: calc(var(--_triangle-size) * -1);
|
||||
border-block-end: var(--_triangle-size) solid transparent;
|
||||
}
|
||||
|
||||
/* RIGHT || INLINE-END */
|
||||
.tooltip.right,
|
||||
.tooltip.inlineEnd {
|
||||
inset-inline-start: calc(100% + var(--_p-inline) + var(--_triangle-size));
|
||||
inset-block-end: 50%;
|
||||
--_y: 50%;
|
||||
}
|
||||
|
||||
.tooltip.right::after,
|
||||
.tooltip.tooltip.inlineEnd::after {
|
||||
--_tip: var(--_left-tip);
|
||||
inset-inline-start: calc(var(--_triangle-size) * -1);
|
||||
border-inline-start: var(--_triangle-size) solid transparent;
|
||||
}
|
||||
|
||||
.tooltip.right:dir(rtl)::after,
|
||||
.tooltip.inlineEnd:dir(rtl)::after {
|
||||
--_tip: var(--_right-tip);
|
||||
}
|
||||
|
||||
/* BOTTOM || BLOCK-END */
|
||||
.tooltip.bottom,
|
||||
.tooltip.blockEnd {
|
||||
inset-inline-start: 50%;
|
||||
inset-block-start: calc(100% + var(--_p-block) + var(--_triangle-size));
|
||||
--_x: calc(50% * var(--isRTL));
|
||||
}
|
||||
|
||||
.tooltip.bottom::after,
|
||||
.tooltip.tooltip.blockEnd::after {
|
||||
--_tip: var(--_top-tip);
|
||||
inset-block-start: calc(var(--_triangle-size) * -1);
|
||||
border-block-start: var(--_triangle-size) solid transparent;
|
||||
}
|
||||
|
||||
/* LEFT || INLINE-START */
|
||||
.tooltip.left,
|
||||
.tooltip.inlineStart {
|
||||
inset-inline-end: calc(100% + var(--_p-inline) + var(--_triangle-size));
|
||||
inset-block-end: 50%;
|
||||
--_y: 50%;
|
||||
}
|
||||
|
||||
.tooltip.left::after,
|
||||
.tooltip.tooltip.inlineStart::after {
|
||||
--_tip: var(--_right-tip);
|
||||
inset-inline-end: calc(var(--_triangle-size) * -1);
|
||||
border-inline-end: var(--_triangle-size) solid transparent;
|
||||
}
|
||||
|
||||
.tooltip.left:dir(rtl)::after,
|
||||
.tooltip.inlineStart:dir(rtl)::after {
|
||||
--_tip: var(--_left-tip);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
/* TOP || BLOCK-START */
|
||||
:has(> :is(.tooltip.top, .tooltip.blockStart)):not(:hover, :active) .tooltip {
|
||||
--_y: 3px;
|
||||
}
|
||||
|
||||
/* RIGHT || INLINE-END */
|
||||
:has(> :is(.tooltip.right, .tooltip.inlineEnd)):not(:hover, :active)
|
||||
.tooltip {
|
||||
--_x: calc(var(--isRTL) * -3px * -1);
|
||||
}
|
||||
|
||||
/* BOTTOM || BLOCK-END */
|
||||
:has(> :is(.tooltip.bottom, .tooltip.blockEnd)):not(:hover, :active)
|
||||
.tooltip {
|
||||
--_y: -3px;
|
||||
}
|
||||
|
||||
/* BOTTOM || BLOCK-END */
|
||||
:has(> :is(.tooltip.left, .tooltip.inlineStart)):not(:hover, :active)
|
||||
.tooltip {
|
||||
--_x: calc(var(--isRTL) * 3px * -1);
|
||||
}
|
||||
}
|
||||
37
src/components/Tooltip.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
// We do use all the classes in this file currently, but we
|
||||
// index into them with styles[position], which CSS Modules doesn't pick up.
|
||||
// eslint-disable-next-line css-modules/no-unused-class
|
||||
import styles from './Tooltip.module.css'
|
||||
|
||||
interface TooltipProps extends React.PropsWithChildren {
|
||||
position?:
|
||||
| 'top'
|
||||
| 'bottom'
|
||||
| 'left'
|
||||
| 'right'
|
||||
| 'blockStart'
|
||||
| 'blockEnd'
|
||||
| 'inlineStart'
|
||||
| 'inlineEnd'
|
||||
className?: string
|
||||
delay?: number
|
||||
}
|
||||
|
||||
export default function Tooltip({
|
||||
children,
|
||||
position = 'top',
|
||||
className,
|
||||
delay = 200,
|
||||
}: TooltipProps) {
|
||||
return (
|
||||
<div
|
||||
// @ts-ignore while awaiting merge of this PR for support of "inert" https://github.com/DefinitelyTyped/DefinitelyTyped/pull/60822
|
||||
inert="true"
|
||||
role="tooltip"
|
||||
className={styles.tooltip + ' ' + styles[position] + ' ' + className}
|
||||
style={{ '--_delay': delay + 'ms' } as React.CSSProperties}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -9,6 +9,7 @@ import { LanguageServerClient } from '.'
|
||||
import { kclPlugin } from './plugin'
|
||||
import type * as LSP from 'vscode-languageserver-protocol'
|
||||
import { parser as jsParser } from '@lezer/javascript'
|
||||
import { EditorState } from '@uiw/react-codemirror'
|
||||
|
||||
const data = defineLanguageFacet({})
|
||||
|
||||
@ -22,7 +23,25 @@ export default function kclLanguage(options: LanguageOptions): LanguageSupport {
|
||||
// For now let's use the javascript parser.
|
||||
// It works really well and has good syntax highlighting.
|
||||
// We can use our lsp for the rest.
|
||||
const lang = new Language(data, jsParser, [], 'kcl')
|
||||
const lang = new Language(
|
||||
data,
|
||||
jsParser,
|
||||
[
|
||||
EditorState.languageData.of(() => [
|
||||
{
|
||||
// https://codemirror.net/docs/ref/#commands.CommentTokens
|
||||
commentTokens: {
|
||||
line: '//',
|
||||
block: {
|
||||
open: '/*',
|
||||
close: '*/',
|
||||
},
|
||||
},
|
||||
},
|
||||
]),
|
||||
],
|
||||
'kcl'
|
||||
)
|
||||
|
||||
// Create our supporting extension.
|
||||
const kclLsp = kclPlugin({
|
||||
|
||||
@ -2,13 +2,22 @@ import { useEffect } from 'react'
|
||||
import { useStore } from 'useStore'
|
||||
import { engineCommandManager } from '../lang/std/engineConnection'
|
||||
import { useModelingContext } from './useModelingContext'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { SourceRange } from 'lang/wasm'
|
||||
import { getEventForSelectWithPoint } from 'lib/selections'
|
||||
|
||||
export function useEngineConnectionSubscriptions() {
|
||||
const { setHighlightRange, highlightRange } = useStore((s) => ({
|
||||
setHighlightRange: s.setHighlightRange,
|
||||
highlightRange: s.highlightRange,
|
||||
}))
|
||||
const { send } = useModelingContext()
|
||||
const { send, context } = useModelingContext()
|
||||
|
||||
interface RangeAndId {
|
||||
id: string
|
||||
range: SourceRange
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!engineCommandManager) return
|
||||
|
||||
@ -17,7 +26,7 @@ export function useEngineConnectionSubscriptions() {
|
||||
callback: ({ data }) => {
|
||||
if (data?.entity_id) {
|
||||
const sourceRange =
|
||||
engineCommandManager.sourceRangeMap[data.entity_id]
|
||||
engineCommandManager.artifactMap?.[data.entity_id]?.range
|
||||
setHighlightRange(sourceRange)
|
||||
} else if (
|
||||
!highlightRange ||
|
||||
@ -29,27 +38,21 @@ export function useEngineConnectionSubscriptions() {
|
||||
})
|
||||
const unSubClick = engineCommandManager.subscribeTo({
|
||||
event: 'select_with_point',
|
||||
callback: ({ data }) => {
|
||||
if (!data?.entity_id) {
|
||||
send({
|
||||
type: 'Set selection',
|
||||
data: { selectionType: 'singleCodeCursor' },
|
||||
})
|
||||
return
|
||||
}
|
||||
const sourceRange = engineCommandManager.sourceRangeMap[data.entity_id]
|
||||
send({
|
||||
type: 'Set selection',
|
||||
data: {
|
||||
selectionType: 'singleCodeCursor',
|
||||
selection: { range: sourceRange, type: 'default' },
|
||||
},
|
||||
callback: async (engineEvent) => {
|
||||
const event = await getEventForSelectWithPoint(engineEvent, {
|
||||
sketchEnginePathId: context.sketchEnginePathId,
|
||||
})
|
||||
send(event)
|
||||
},
|
||||
})
|
||||
return () => {
|
||||
unSubHover()
|
||||
unSubClick()
|
||||
}
|
||||
}, [engineCommandManager, setHighlightRange, highlightRange])
|
||||
}, [
|
||||
engineCommandManager,
|
||||
setHighlightRange,
|
||||
highlightRange,
|
||||
context.sketchEnginePathId,
|
||||
])
|
||||
}
|
||||
|
||||
6
src/hooks/useFileContext.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { FileContext } from 'components/FileMachineProvider'
|
||||
import { useContext } from 'react'
|
||||
|
||||
export const useFileContext = () => {
|
||||
return useContext(FileContext)
|
||||
}
|
||||
@ -1,12 +1,14 @@
|
||||
import { SetVarNameModal } from 'components/SetVarNameModal'
|
||||
import {
|
||||
SetVarNameModal,
|
||||
createSetVarNameModal,
|
||||
} from 'components/SetVarNameModal'
|
||||
import { kclManager } from 'lang/KclSinglton'
|
||||
import { moveValueIntoNewVariable } from 'lang/modifyAst'
|
||||
import { isNodeSafeToReplace } from 'lang/queryAst'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { create } from 'react-modal-promise'
|
||||
import { useModelingContext } from './useModelingContext'
|
||||
|
||||
const getModalInfo = create(SetVarNameModal as any)
|
||||
const getModalInfo = createSetVarNameModal(SetVarNameModal)
|
||||
|
||||
export function useConvertToVariable() {
|
||||
const { context } = useModelingContext()
|
||||
@ -28,7 +30,7 @@ export function useConvertToVariable() {
|
||||
try {
|
||||
const { variableName } = await getModalInfo({
|
||||
valueName: 'var',
|
||||
} as any)
|
||||
})
|
||||
|
||||
const { modifiedAst: _modifiedAst } = moveValueIntoNewVariable(
|
||||
kclManager.ast,
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { Selections, executeAst, executeCode } from 'useStore'
|
||||
import { executeAst, executeCode } from 'useStore'
|
||||
import { Selections } from 'lib/selections'
|
||||
import { KCLError } from './errors'
|
||||
import {
|
||||
EngineCommandManager,
|
||||
@ -16,6 +17,8 @@ import {
|
||||
import { bracket } from 'lib/exampleKcl'
|
||||
import { createContext, useContext, useEffect, useState } from 'react'
|
||||
import { getNodeFromPath } from './queryAst'
|
||||
import { IndexLoaderData } from 'Router'
|
||||
import { useLoaderData } from 'react-router-dom'
|
||||
|
||||
const PERSIST_CODE_TOKEN = 'persistCode'
|
||||
|
||||
@ -27,7 +30,7 @@ class KclManager {
|
||||
end: 0,
|
||||
nonCodeMeta: {
|
||||
nonCodeNodes: {},
|
||||
start: null,
|
||||
start: [],
|
||||
},
|
||||
}
|
||||
private _programMemory: ProgramMemory = {
|
||||
@ -185,6 +188,7 @@ class KclManager {
|
||||
async executeCode(code?: string) {
|
||||
await initPromise
|
||||
await this?.engineCommandManager?.waitForReady
|
||||
if (!this?.engineCommandManager?.planesInitialized()) return
|
||||
const result = await executeCode({
|
||||
engineCommandManager,
|
||||
code: code || this._code,
|
||||
@ -217,7 +221,7 @@ class KclManager {
|
||||
end: 0,
|
||||
nonCodeMeta: {
|
||||
nonCodeNodes: {},
|
||||
start: null,
|
||||
start: [],
|
||||
},
|
||||
}
|
||||
this._programMemory = {
|
||||
@ -313,7 +317,10 @@ export function KclContextProvider({
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const [code, setCode] = useState(kclManager.code)
|
||||
// If we try to use this component anywhere but under the paths.FILE route it will fail
|
||||
// Because useLoaderData assumes we are on within it's context.
|
||||
const { code: loadedCode } = useLoaderData() as IndexLoaderData
|
||||
const [code, setCode] = useState(loadedCode || kclManager.code)
|
||||
const [programMemory, setProgramMemory] = useState(kclManager.programMemory)
|
||||
const [ast, setAst] = useState(kclManager.ast)
|
||||
const [isExecuting, setIsExecuting] = useState(false)
|
||||
|
||||
@ -141,42 +141,6 @@ const newVar = myVar + 1
|
||||
})
|
||||
|
||||
describe('testing function declaration', () => {
|
||||
test('fn funcN = () => {}', () => {
|
||||
const { body } = parse('fn funcN = () => {}')
|
||||
delete (body[0] as any).declarations[0].init.body.nonCodeMeta
|
||||
expect(body).toEqual([
|
||||
{
|
||||
type: 'VariableDeclaration',
|
||||
start: 0,
|
||||
end: 19,
|
||||
kind: 'fn',
|
||||
declarations: [
|
||||
{
|
||||
type: 'VariableDeclarator',
|
||||
start: 3,
|
||||
end: 19,
|
||||
id: {
|
||||
type: 'Identifier',
|
||||
start: 3,
|
||||
end: 8,
|
||||
name: 'funcN',
|
||||
},
|
||||
init: {
|
||||
type: 'FunctionExpression',
|
||||
start: 11,
|
||||
end: 19,
|
||||
params: [],
|
||||
body: {
|
||||
start: 17,
|
||||
end: 19,
|
||||
body: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
})
|
||||
test('fn funcN = (a, b) => {return a + b}', () => {
|
||||
const { body } = parse(
|
||||
['fn funcN = (a, b) => {', ' return a + b', '}'].join('\n')
|
||||
@ -1513,22 +1477,23 @@ const key = 'c'`
|
||||
const nonCodeMetaInstance = {
|
||||
type: 'NonCodeNode',
|
||||
start: code.indexOf('\n// this is a comment'),
|
||||
end: code.indexOf('const key'),
|
||||
end: code.indexOf('const key') - 1,
|
||||
value: {
|
||||
type: 'blockComment',
|
||||
style: 'line',
|
||||
value: 'this is a comment',
|
||||
},
|
||||
}
|
||||
const { nonCodeMeta } = parse(code)
|
||||
expect(nonCodeMeta.nonCodeNodes[0]).toEqual(nonCodeMetaInstance)
|
||||
expect(nonCodeMeta.nonCodeNodes[0][0]).toEqual(nonCodeMetaInstance)
|
||||
|
||||
// extra whitespace won't change it's position (0) or value (NB the start end would have changed though)
|
||||
const codeWithExtraStartWhitespace = '\n\n\n' + code
|
||||
const { nonCodeMeta: nonCodeMeta2 } = parse(codeWithExtraStartWhitespace)
|
||||
expect(nonCodeMeta2.nonCodeNodes[0].value).toStrictEqual(
|
||||
expect(nonCodeMeta2.nonCodeNodes[0][0].value).toStrictEqual(
|
||||
nonCodeMetaInstance.value
|
||||
)
|
||||
expect(nonCodeMeta2.nonCodeNodes[0].start).not.toBe(
|
||||
expect(nonCodeMeta2.nonCodeNodes[0][0].start).not.toBe(
|
||||
nonCodeMetaInstance.start
|
||||
)
|
||||
})
|
||||
@ -1546,12 +1511,13 @@ const key = 'c'`
|
||||
const indexOfSecondLineToExpression = 2
|
||||
const sketchNonCodeMeta = (body as any)[0].declarations[0].init.nonCodeMeta
|
||||
.nonCodeNodes
|
||||
expect(sketchNonCodeMeta[indexOfSecondLineToExpression]).toEqual({
|
||||
expect(sketchNonCodeMeta[indexOfSecondLineToExpression][0]).toEqual({
|
||||
type: 'NonCodeNode',
|
||||
start: 106,
|
||||
end: 166,
|
||||
end: 163,
|
||||
value: {
|
||||
type: 'blockComment',
|
||||
type: 'inlineComment',
|
||||
style: 'block',
|
||||
value: 'this is\n a comment\n spanning a few lines',
|
||||
},
|
||||
})
|
||||
@ -1568,14 +1534,15 @@ const key = 'c'`
|
||||
|
||||
const { body } = parse(code)
|
||||
const sketchNonCodeMeta = (body[0] as any).declarations[0].init.nonCodeMeta
|
||||
.nonCodeNodes
|
||||
expect(sketchNonCodeMeta[3]).toEqual({
|
||||
.nonCodeNodes[3][0]
|
||||
expect(sketchNonCodeMeta).toEqual({
|
||||
type: 'NonCodeNode',
|
||||
start: 125,
|
||||
end: 141,
|
||||
end: 138,
|
||||
value: {
|
||||
type: 'blockComment',
|
||||
value: 'a comment',
|
||||
style: 'line',
|
||||
},
|
||||
})
|
||||
})
|
||||
@ -1693,11 +1660,7 @@ describe('parsing errors', () => {
|
||||
}
|
||||
const theError = _theError as any
|
||||
expect(theError).toEqual(
|
||||
new KCLError(
|
||||
'unexpected',
|
||||
'Unexpected token Token { token_type: Brace, start: 29, end: 30, value: "}" }',
|
||||
[[29, 30]]
|
||||
)
|
||||
new KCLError('syntax', 'Unexpected token', [[27, 28]])
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@ -104,7 +104,7 @@ describe('Testing addSketchTo', () => {
|
||||
body: [],
|
||||
start: 0,
|
||||
end: 0,
|
||||
nonCodeMeta: { nonCodeNodes: {}, start: null },
|
||||
nonCodeMeta: { nonCodeNodes: {}, start: [] },
|
||||
},
|
||||
'yz'
|
||||
)
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { Selection, ToolTip } from '../useStore'
|
||||
import { ToolTip } from '../useStore'
|
||||
import { Selection } from 'lib/selections'
|
||||
import {
|
||||
Program,
|
||||
CallExpression,
|
||||
@ -540,7 +541,7 @@ export function createPipeExpression(
|
||||
start: 0,
|
||||
end: 0,
|
||||
body,
|
||||
nonCodeMeta: { nonCodeNodes: {}, start: null },
|
||||
nonCodeMeta: { nonCodeNodes: {}, start: [] },
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { Selection, ToolTip } from '../useStore'
|
||||
import { ToolTip } from '../useStore'
|
||||
import { Selection } from 'lib/selections'
|
||||
import {
|
||||
BinaryExpression,
|
||||
Program,
|
||||
|
||||
@ -272,21 +272,20 @@ const mySk1 = startSketchAt([0, 0])
|
||||
`
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(`// comment at start
|
||||
expect(recasted).toBe(`/* comment at start */
|
||||
|
||||
const mySk1 = startSketchAt([0, 0])
|
||||
|> lineTo([1, 1], %)
|
||||
// comment here
|
||||
|> lineTo({ to: [0, 1], tag: 'myTag' }, %)
|
||||
|> lineTo([1, 1], %)
|
||||
/* and
|
||||
here
|
||||
|
||||
a comment between pipe expression statements */
|
||||
|> lineTo([1, 1], %) /* and
|
||||
here */
|
||||
// a comment between pipe expression statements
|
||||
|> rx(90, %)
|
||||
// and another with just white space between others below
|
||||
|> ry(45, %)
|
||||
|> rx(45, %)
|
||||
// one more for good measure
|
||||
/* one more for good measure */
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,16 +1,9 @@
|
||||
import {
|
||||
ProgramMemory,
|
||||
SourceRange,
|
||||
Program,
|
||||
VariableDeclarator,
|
||||
} from 'lang/wasm'
|
||||
import { Selections } from 'useStore'
|
||||
import { SourceRange } from 'lang/wasm'
|
||||
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'
|
||||
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
|
||||
import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
|
||||
|
||||
let lastMessage = ''
|
||||
@ -27,6 +20,7 @@ interface ResultCommand extends CommandInfo {
|
||||
type: 'result'
|
||||
data: any
|
||||
raw: WebSocketResponse
|
||||
headVertexId?: string
|
||||
}
|
||||
interface FailedCommand extends CommandInfo {
|
||||
type: 'failed'
|
||||
@ -41,9 +35,6 @@ interface PendingCommand extends CommandInfo {
|
||||
export interface ArtifactMap {
|
||||
[key: string]: ResultCommand | PendingCommand | FailedCommand
|
||||
}
|
||||
export interface SourceRangeMap {
|
||||
[key: string]: SourceRange
|
||||
}
|
||||
|
||||
interface NewTrackArgs {
|
||||
conn: EngineConnection
|
||||
@ -594,7 +585,6 @@ interface Subscription<T extends ModelTypes> {
|
||||
|
||||
export class EngineCommandManager {
|
||||
artifactMap: ArtifactMap = {}
|
||||
sourceRangeMap: SourceRangeMap = {}
|
||||
outSequence = 1
|
||||
inSequence = 1
|
||||
engineConnection?: EngineConnection
|
||||
@ -765,7 +755,6 @@ export class EngineCommandManager {
|
||||
streamWidth: number
|
||||
streamHeight: number
|
||||
}) {
|
||||
console.log('handleResize', streamWidth, streamHeight)
|
||||
if (!this.engineConnection?.isReady()) {
|
||||
return
|
||||
}
|
||||
@ -856,7 +845,6 @@ export class EngineCommandManager {
|
||||
}
|
||||
startNewSession() {
|
||||
this.artifactMap = {}
|
||||
this.sourceRangeMap = {}
|
||||
}
|
||||
subscribeTo<T extends ModelTypes>({
|
||||
event,
|
||||
@ -922,30 +910,6 @@ export class EngineCommandManager {
|
||||
this.engineConnection?.send(deletCmd)
|
||||
})
|
||||
}
|
||||
cusorsSelected(selections: {
|
||||
otherSelections: Selections['otherSelections']
|
||||
idBasedSelections: { type: string; id: string }[]
|
||||
}) {
|
||||
if (!this.engineConnection?.isReady()) {
|
||||
console.log('engine connection isnt ready')
|
||||
return
|
||||
}
|
||||
this.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'select_clear',
|
||||
},
|
||||
cmd_id: uuidv4(),
|
||||
})
|
||||
this.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'select_add',
|
||||
entities: selections.idBasedSelections.map((s) => s.id),
|
||||
},
|
||||
cmd_id: uuidv4(),
|
||||
})
|
||||
}
|
||||
sendSceneCommand(command: EngineCommand): Promise<any> {
|
||||
if (this.engineConnection === undefined) {
|
||||
return Promise.resolve()
|
||||
@ -1006,7 +970,6 @@ export class EngineCommandManager {
|
||||
if (this.engineConnection === undefined) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
this.sourceRangeMap[id] = range
|
||||
|
||||
if (!this.engineConnection?.isReady()) {
|
||||
return Promise.resolve()
|
||||
@ -1082,109 +1045,19 @@ export class EngineCommandManager {
|
||||
}
|
||||
return command.promise
|
||||
}
|
||||
async waitForAllCommands(
|
||||
ast?: Program,
|
||||
programMemory?: ProgramMemory
|
||||
): Promise<{
|
||||
async waitForAllCommands(): Promise<{
|
||||
artifactMap: ArtifactMap
|
||||
sourceRangeMap: SourceRangeMap
|
||||
}> {
|
||||
const pendingCommands = Object.values(this.artifactMap).filter(
|
||||
({ type }) => type === 'pending'
|
||||
) as PendingCommand[]
|
||||
const proms = pendingCommands.map(({ promise }) => promise)
|
||||
await Promise.all(proms)
|
||||
if (ast && programMemory) {
|
||||
await this.fixIdMappings(ast, programMemory)
|
||||
}
|
||||
|
||||
return {
|
||||
artifactMap: this.artifactMap,
|
||||
sourceRangeMap: this.sourceRangeMap,
|
||||
}
|
||||
}
|
||||
private async fixIdMappings(ast: Program, programMemory: ProgramMemory) {
|
||||
if (this.engineConnection === undefined) {
|
||||
return
|
||||
}
|
||||
/* This is a temporary solution since the cmd_ids that are sent through when
|
||||
sending 'extend_path' ids are not used as the segment ids.
|
||||
|
||||
We have a way to back fill them with 'path_get_info', however this relies on one
|
||||
the sketchGroup array and the segements array returned from the server to be in
|
||||
the same length and order. plus it's super hacky, we first use the path_id to get
|
||||
the source range of the pipe expression then use the name of the variable to get
|
||||
the sketchGroup from programMemory.
|
||||
|
||||
I feel queezy about relying on all these steps to always line up.
|
||||
We have also had to pollute this EngineCommandManager class with knowledge of both the ast and programMemory
|
||||
We should get the cmd_ids to match with the segment ids and delete this method.
|
||||
*/
|
||||
const pathInfoProms = []
|
||||
for (const [id, artifact] of Object.entries(this.artifactMap)) {
|
||||
if (artifact.commandType === 'start_path') {
|
||||
pathInfoProms.push(
|
||||
this.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'path_get_info',
|
||||
path_id: id,
|
||||
},
|
||||
}).then(({ data }) => ({
|
||||
originalId: id,
|
||||
segments: data?.data?.segments,
|
||||
}))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const pathInfos = await Promise.all(pathInfoProms)
|
||||
pathInfos.forEach(({ originalId, segments }) => {
|
||||
const originalArtifact = this.artifactMap[originalId]
|
||||
if (!originalArtifact || originalArtifact.type === 'pending') {
|
||||
return
|
||||
}
|
||||
const pipeExpPath = getNodePathFromSourceRange(
|
||||
ast,
|
||||
originalArtifact.range
|
||||
)
|
||||
const pipeExp = getNodeFromPath<VariableDeclarator>(
|
||||
ast,
|
||||
pipeExpPath,
|
||||
'VariableDeclarator'
|
||||
).node
|
||||
if (pipeExp.type !== 'VariableDeclarator') {
|
||||
return
|
||||
}
|
||||
const variableName = pipeExp.id.name
|
||||
const memoryItem = programMemory.root[variableName]
|
||||
if (!memoryItem) {
|
||||
return
|
||||
} else if (memoryItem.type !== 'SketchGroup') {
|
||||
return
|
||||
}
|
||||
|
||||
const relevantSegments = segments.filter(
|
||||
({ command_id }: { command_id: string | null }) => command_id
|
||||
)
|
||||
if (memoryItem.value.length !== relevantSegments.length) {
|
||||
return
|
||||
}
|
||||
for (let i = 0; i < relevantSegments.length; i++) {
|
||||
const engineSegment = relevantSegments[i]
|
||||
const memorySegment = memoryItem.value[i]
|
||||
const oldId = memorySegment.__geoMeta.id
|
||||
const artifact = this.artifactMap[oldId]
|
||||
delete this.artifactMap[oldId]
|
||||
delete this.sourceRangeMap[oldId]
|
||||
if (artifact) {
|
||||
this.artifactMap[engineSegment.command_id] = artifact
|
||||
this.sourceRangeMap[engineSegment.command_id] = artifact.range
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
private async initPlanes() {
|
||||
const [xy, yz, xz] = [
|
||||
await this.createPlane({
|
||||
@ -1221,6 +1094,13 @@ export class EngineCommandManager {
|
||||
},
|
||||
})
|
||||
}
|
||||
planesInitialized(): boolean {
|
||||
return (
|
||||
this.defaultPlanes.xy !== '' &&
|
||||
this.defaultPlanes.yz !== '' &&
|
||||
this.defaultPlanes.xz !== ''
|
||||
)
|
||||
}
|
||||
|
||||
onPlaneSelectCallback = (id: string) => {}
|
||||
onPlaneSelected(callback: (id: string) => void) {
|
||||
|
||||
@ -100,7 +100,7 @@ describe('testing changeSketchArguments', () => {
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> ${line}
|
||||
|> lineTo([0.46, -5.82], %)
|
||||
// |> rx(45, %)
|
||||
// |> rx(45, %)
|
||||
show(mySketch001)
|
||||
`
|
||||
const code = genCode(lineToChange)
|
||||
|
||||
@ -209,7 +209,11 @@ export const line: SketchLineHelper = {
|
||||
pipe.body[callIndex] = callExp
|
||||
return {
|
||||
modifiedAst: _node,
|
||||
pathToNode,
|
||||
pathToNode: [
|
||||
...pathToNode,
|
||||
['body', 'PipeExpression'],
|
||||
[callIndex, 'CallExpression'],
|
||||
],
|
||||
valueUsedInTransform,
|
||||
}
|
||||
}
|
||||
@ -220,6 +224,14 @@ export const line: SketchLineHelper = {
|
||||
])
|
||||
if (pipe.type === 'PipeExpression') {
|
||||
pipe.body = [...pipe.body, callExp]
|
||||
return {
|
||||
modifiedAst: _node,
|
||||
pathToNode: [
|
||||
...pathToNode,
|
||||
['body', 'PipeExpression'],
|
||||
[pipe.body.length - 1, 'CallExpression'],
|
||||
],
|
||||
}
|
||||
} else {
|
||||
varDec.init = createPipeExpression([varDec.init, callExp])
|
||||
}
|
||||
@ -909,7 +921,7 @@ export function changeSketchArguments(
|
||||
sourceRange: SourceRange,
|
||||
args: [number, number],
|
||||
from: [number, number]
|
||||
): { modifiedAst: Program } {
|
||||
): { modifiedAst: Program; pathToNode: PathToNode } {
|
||||
const _node = { ...node }
|
||||
const thePath = getNodePathFromSourceRange(_node, sourceRange)
|
||||
const { node: callExpression, shallowPath } = getNodeFromPath<CallExpression>(
|
||||
@ -929,7 +941,7 @@ export function changeSketchArguments(
|
||||
})
|
||||
}
|
||||
|
||||
throw new Error('not a sketch line helper')
|
||||
throw new Error(`not a sketch line helper: ${callExpression?.callee?.name}`)
|
||||
}
|
||||
|
||||
interface CreateLineFnCallArgs {
|
||||
@ -959,6 +971,7 @@ export function addNewSketchLn({
|
||||
pathToNode,
|
||||
}: Omit<CreateLineFnCallArgs, 'from'>): {
|
||||
modifiedAst: Program
|
||||
pathToNode: PathToNode
|
||||
} {
|
||||
const node = JSON.parse(JSON.stringify(_node))
|
||||
const { add, updateArgs } = sketchLineHelperMap?.[fnName] || {}
|
||||
|
||||
@ -5,7 +5,7 @@ import {
|
||||
transformAstSketchLines,
|
||||
} from './sketchcombos'
|
||||
import { getSketchSegmentFromSourceRange } from './sketchConstraints'
|
||||
import { Selection } from '../../useStore'
|
||||
import { Selection } from 'lib/selections'
|
||||
import { enginelessExecutor } from '../../lib/testHelpers'
|
||||
|
||||
beforeAll(() => initPromise)
|
||||
|
||||
@ -7,7 +7,8 @@ import {
|
||||
ConstraintType,
|
||||
getConstraintLevelFromSourceRange,
|
||||
} from './sketchcombos'
|
||||
import { Selections, ToolTip } from '../../useStore'
|
||||
import { ToolTip } from '../../useStore'
|
||||
import { Selections } from 'lib/selections'
|
||||
import { enginelessExecutor } from '../../lib/testHelpers'
|
||||
|
||||
beforeAll(() => initPromise)
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { TransformCallback } from './stdTypes'
|
||||
import { Selections, toolTips, ToolTip, Selection } from '../../useStore'
|
||||
import { toolTips, ToolTip } from '../../useStore'
|
||||
import { Selections, Selection } from 'lib/selections'
|
||||
import {
|
||||
CallExpression,
|
||||
Program,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Selections, StoreState } from '../useStore'
|
||||
import { Selections } from 'lib/selections'
|
||||
import { Program, PathToNode } from './wasm'
|
||||
import { getNodeFromPath } from './queryAst'
|
||||
import { ArtifactMap } from './std/engineConnection'
|
||||
|
||||
@ -7,11 +7,7 @@ import init, {
|
||||
} from '../wasm-lib/pkg/wasm_lib'
|
||||
import { KCLError } from './errors'
|
||||
import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError'
|
||||
import {
|
||||
EngineCommandManager,
|
||||
ArtifactMap,
|
||||
SourceRangeMap,
|
||||
} from './std/engineConnection'
|
||||
import { EngineCommandManager } from './std/engineConnection'
|
||||
import { ProgramReturn } from '../wasm-lib/kcl/bindings/ProgramReturn'
|
||||
import { MemoryItem } from '../wasm-lib/kcl/bindings/MemoryItem'
|
||||
import type { Program } from '../wasm-lib/kcl/bindings/Program'
|
||||
@ -119,13 +115,7 @@ export const executor = async (
|
||||
node: Program,
|
||||
programMemory: ProgramMemory = { root: {}, return: null },
|
||||
engineCommandManager: EngineCommandManager,
|
||||
planes: DefaultPlanes,
|
||||
// work around while the gemotry is still be stored on the frontend
|
||||
// will be removed when the stream UI is added.
|
||||
tempMapCallback: (a: {
|
||||
artifactMap: ArtifactMap
|
||||
sourceRangeMap: SourceRangeMap
|
||||
}) => void = () => {}
|
||||
planes: DefaultPlanes
|
||||
): Promise<ProgramMemory> => {
|
||||
engineCommandManager.startNewSession()
|
||||
const _programMemory = await _executor(
|
||||
@ -134,9 +124,7 @@ export const executor = async (
|
||||
engineCommandManager,
|
||||
planes
|
||||
)
|
||||
const { artifactMap, sourceRangeMap } =
|
||||
await engineCommandManager.waitForAllCommands(node, _programMemory)
|
||||
tempMapCallback({ artifactMap, sourceRangeMap })
|
||||
await engineCommandManager.waitForAllCommands()
|
||||
|
||||
engineCommandManager.endSession()
|
||||
return _programMemory
|
||||
|
||||
@ -11,14 +11,14 @@ const wallMountL = 8
|
||||
const bracket = startSketchOn('XY')
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> line([0, wallMountL], %)
|
||||
|> tangentalArc({
|
||||
|> tangentialArc({
|
||||
radius: filletR,
|
||||
offset: 90
|
||||
}, %)
|
||||
|> line([-shelfMountL, 0], %)
|
||||
|> line([0, -thickness], %)
|
||||
|> line([shelfMountL, 0], %)
|
||||
|> tangentalArc({
|
||||
|> tangentialArc({
|
||||
radius: filletR - thickness,
|
||||
offset: -90
|
||||
}, %)
|
||||
|
||||
326
src/lib/selections.ts
Normal file
@ -0,0 +1,326 @@
|
||||
import { Models } from '@kittycad/lib'
|
||||
import { engineCommandManager } from 'lang/std/engineConnection'
|
||||
import { SourceRange } from 'lang/wasm'
|
||||
import { ModelingMachineEvent } from 'machines/modelingMachine'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { EditorSelection } from '@codemirror/state'
|
||||
import { kclManager } from 'lang/KclSinglton'
|
||||
import { SelectionRange } from '@uiw/react-codemirror'
|
||||
import { isOverlap } from 'lib/utils'
|
||||
|
||||
/*
|
||||
How selections work is complex due to the nature that we rely on the engine
|
||||
to tell what has been selected after we send a click command. But than the
|
||||
app needs these selections to be based on cursors, therefore the app must
|
||||
be in control of selections. On top of that because we need to set cursor
|
||||
positions in code-mirror for selections, both from app logic, and still
|
||||
allow the user to add multiple cursors like a normal editor, it's best to
|
||||
let code mirror control cursor positions and assosiate those source ranges
|
||||
with entity ids from code-mirror events later.
|
||||
|
||||
So it's a lot of back and forth. conceptually the back and forth is:
|
||||
|
||||
1) we send a click command to the engine
|
||||
2) the engine sends back ids of entities that were clicked
|
||||
3) we associate that source ranges with those ids
|
||||
4) we set the codemirror selection based on those source ranges (taking
|
||||
into account if the user is holding shift to add to current selections
|
||||
or not). we also create and remember a SelectionRangeTypeMap
|
||||
5) Code mirror fires a an event that cursors have changed, we loop through
|
||||
these ranges and associate them with entity ids again with the ArtifactMap,
|
||||
but also we can pick up selection types using the SelectionRangeTypeMap
|
||||
6) we clear all previous selections in the engine and set the new ones
|
||||
|
||||
The above is less likely to get stale but below is some more details,
|
||||
because this wonders all over the code-base, I've tried to centeralise it
|
||||
by putting relevant utils in this file. All of the functions below are
|
||||
pure with the exception of getEventForSelectWithPoint which makes a call
|
||||
to the engine, but it's a query call (not mutation) so I'm okay with this.
|
||||
Actual side effects that change cursors or tell the engine what's selected
|
||||
are still done throughout the in their relevant parts in the codebase.
|
||||
|
||||
In detail:
|
||||
|
||||
1) Click commands are mostly sent in stream.tsx search for
|
||||
"select_with_point"
|
||||
2) The handler for when the engine sends back entitiy ids calls
|
||||
getEventForSelectWithPoint, it fires an XState event to update our
|
||||
selections is xstate context
|
||||
3 and 4) The XState handler for the above uses handleSelectionBatch and
|
||||
handleSelectionWithShift to update the selections in xstate context as
|
||||
well as returning our SelectionRangeTypeMap and a codeMirror specific
|
||||
event to be dispatched.
|
||||
5) The codeMirror handler for changes to the cursor uses
|
||||
processCodeMirrorRanges to associate the ranges back with their original
|
||||
types and the entity ids (the id can vary depending on the type, as
|
||||
there's only one source range for a given segment, but depending on if
|
||||
the user selected the segment directly or the vertex, the id will be
|
||||
different)
|
||||
6) We take all of the ids and create events for the engine with
|
||||
resetAndSetEngineEntitySelectionCmds
|
||||
|
||||
An important note is that if a user changes the cursor directly themselves
|
||||
then they skip directly to step 5, And these selections get a type of
|
||||
"default".
|
||||
|
||||
There are a few more nuances than this, but best to find them in the code.
|
||||
*/
|
||||
|
||||
export type Axis = 'y-axis' | 'x-axis' | 'z-axis'
|
||||
|
||||
export type Selection = {
|
||||
type:
|
||||
| 'default'
|
||||
| 'line-end'
|
||||
| 'line-mid'
|
||||
| 'face'
|
||||
| 'point'
|
||||
| 'edge'
|
||||
| 'line'
|
||||
| 'arc'
|
||||
| 'all'
|
||||
range: SourceRange
|
||||
}
|
||||
export type Selections = {
|
||||
otherSelections: Axis[]
|
||||
codeBasedSelections: Selection[]
|
||||
}
|
||||
|
||||
export interface SelectionRangeTypeMap {
|
||||
[key: number]: Selection['type']
|
||||
}
|
||||
|
||||
interface RangeAndId {
|
||||
id: string
|
||||
range: SourceRange
|
||||
}
|
||||
|
||||
export async function getEventForSelectWithPoint(
|
||||
{
|
||||
data,
|
||||
}: Extract<
|
||||
Models['OkModelingCmdResponse_type'],
|
||||
{ type: 'select_with_point' }
|
||||
>,
|
||||
{ sketchEnginePathId }: { sketchEnginePathId: string }
|
||||
): Promise<ModelingMachineEvent> {
|
||||
if (!data?.entity_id) {
|
||||
return {
|
||||
type: 'Set selection',
|
||||
data: { selectionType: 'singleCodeCursor' },
|
||||
}
|
||||
}
|
||||
const sourceRange = engineCommandManager.artifactMap[data.entity_id]?.range
|
||||
if (engineCommandManager.artifactMap[data.entity_id]) {
|
||||
return {
|
||||
type: 'Set selection',
|
||||
data: {
|
||||
selectionType: 'singleCodeCursor',
|
||||
selection: { range: sourceRange, type: 'default' },
|
||||
},
|
||||
}
|
||||
}
|
||||
// selected a vertex
|
||||
const res = await engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'path_get_curve_uuids_for_vertices',
|
||||
vertex_ids: [data.entity_id],
|
||||
path_id: sketchEnginePathId,
|
||||
},
|
||||
})
|
||||
const curveIds = res?.data?.data?.curve_ids
|
||||
const ranges: RangeAndId[] = curveIds
|
||||
.map(
|
||||
(id: string): RangeAndId => ({
|
||||
id,
|
||||
range: engineCommandManager.artifactMap[id].range,
|
||||
})
|
||||
)
|
||||
.sort((a: RangeAndId, b: RangeAndId) => a.range[0] - b.range[0])
|
||||
// default to the head of the curve selected
|
||||
const _sourceRange = ranges?.[0].range
|
||||
const artifact = engineCommandManager.artifactMap[ranges?.[0]?.id]
|
||||
if (artifact.type === 'result') {
|
||||
artifact.headVertexId = data.entity_id
|
||||
}
|
||||
return {
|
||||
type: 'Set selection',
|
||||
data: {
|
||||
selectionType: 'singleCodeCursor',
|
||||
// line-end is used to indicate that headVertexId should be sent to the engine as "selected"
|
||||
// not the whole curve
|
||||
selection: { range: _sourceRange, type: 'line-end' },
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function handleSelectionBatch({
|
||||
selections,
|
||||
}: {
|
||||
selections: Selections
|
||||
}): {
|
||||
selectionRangeTypeMap: SelectionRangeTypeMap
|
||||
codeMirrorSelection?: EditorSelection
|
||||
} {
|
||||
const ranges: ReturnType<typeof EditorSelection.cursor>[] = []
|
||||
const selectionRangeTypeMap: SelectionRangeTypeMap = {}
|
||||
selections.codeBasedSelections.forEach(({ range, type }) => {
|
||||
if (range?.[1]) {
|
||||
ranges.push(EditorSelection.cursor(range[1]))
|
||||
selectionRangeTypeMap[range[1]] = type
|
||||
}
|
||||
})
|
||||
if (ranges.length)
|
||||
return {
|
||||
selectionRangeTypeMap,
|
||||
codeMirrorSelection: EditorSelection.create(
|
||||
ranges,
|
||||
selections.codeBasedSelections.length - 1
|
||||
),
|
||||
}
|
||||
|
||||
return {
|
||||
selectionRangeTypeMap,
|
||||
}
|
||||
}
|
||||
|
||||
export function handleSelectionWithShift({
|
||||
codeSelection,
|
||||
currestSelections,
|
||||
isShiftDown,
|
||||
}: {
|
||||
codeSelection?: Selection
|
||||
currestSelections: Selections
|
||||
isShiftDown: boolean
|
||||
}): {
|
||||
selectionRangeTypeMap: SelectionRangeTypeMap
|
||||
codeMirrorSelection?: EditorSelection
|
||||
} {
|
||||
const code = kclManager.code
|
||||
if (!codeSelection)
|
||||
return handleSelectionBatch({
|
||||
selections: {
|
||||
otherSelections: currestSelections.otherSelections,
|
||||
codeBasedSelections: [
|
||||
{
|
||||
range: [0, code.length ? code.length - 1 : 0],
|
||||
type: 'default',
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
const selections: Selections = {
|
||||
...currestSelections,
|
||||
codeBasedSelections: isShiftDown
|
||||
? [...currestSelections.codeBasedSelections, codeSelection]
|
||||
: [codeSelection],
|
||||
}
|
||||
return handleSelectionBatch({ selections })
|
||||
}
|
||||
|
||||
type SelectionToEngine = { type: Selection['type']; id: string }
|
||||
|
||||
export function processCodeMirrorRanges({
|
||||
codeMirrorRanges,
|
||||
selectionRanges,
|
||||
selectionRangeTypeMap,
|
||||
}: {
|
||||
codeMirrorRanges: readonly SelectionRange[]
|
||||
selectionRanges: Selections
|
||||
selectionRangeTypeMap: SelectionRangeTypeMap
|
||||
}): null | {
|
||||
modelingEvent: ModelingMachineEvent
|
||||
engineEvents: Models['WebSocketRequest_type'][]
|
||||
} {
|
||||
const isChange =
|
||||
codeMirrorRanges.length !== selectionRanges.codeBasedSelections.length ||
|
||||
codeMirrorRanges.some(({ from, to }, i) => {
|
||||
return (
|
||||
from !== selectionRanges.codeBasedSelections[i].range[0] ||
|
||||
to !== selectionRanges.codeBasedSelections[i].range[1]
|
||||
)
|
||||
})
|
||||
|
||||
if (!isChange) return null
|
||||
const codeBasedSelections: Selections['codeBasedSelections'] =
|
||||
codeMirrorRanges.map(({ from, to }) => {
|
||||
if (selectionRangeTypeMap[to]) {
|
||||
return {
|
||||
type: selectionRangeTypeMap[to],
|
||||
range: [from, to],
|
||||
}
|
||||
}
|
||||
return {
|
||||
type: 'default',
|
||||
range: [from, to],
|
||||
}
|
||||
})
|
||||
const idBasedSelections: SelectionToEngine[] = codeBasedSelections
|
||||
.map(({ type, range }): null | SelectionToEngine => {
|
||||
// TODO #868: loops over all artifacts will become inefficient at a large scale
|
||||
const entriesWithOverlap = Object.entries(
|
||||
engineCommandManager.artifactMap || {}
|
||||
).filter(([_, artifact]) => {
|
||||
return artifact.range && isOverlap(artifact.range, range)
|
||||
? artifact
|
||||
: false
|
||||
})
|
||||
if (entriesWithOverlap.length) {
|
||||
const [id, artifact] = entriesWithOverlap?.[0]
|
||||
return {
|
||||
type,
|
||||
id:
|
||||
type === 'line-end' &&
|
||||
artifact.type === 'result' &&
|
||||
artifact.headVertexId
|
||||
? artifact.headVertexId
|
||||
: id,
|
||||
}
|
||||
}
|
||||
return null
|
||||
})
|
||||
.filter(Boolean) as any
|
||||
|
||||
if (!selectionRanges) return null
|
||||
return {
|
||||
modelingEvent: {
|
||||
type: 'Set selection',
|
||||
data: {
|
||||
selectionType: 'mirrorCodeMirrorSelections',
|
||||
selection: {
|
||||
...selectionRanges,
|
||||
codeBasedSelections,
|
||||
},
|
||||
},
|
||||
},
|
||||
engineEvents: resetAndSetEngineEntitySelectionCmds(idBasedSelections),
|
||||
}
|
||||
}
|
||||
|
||||
export function resetAndSetEngineEntitySelectionCmds(
|
||||
selections: SelectionToEngine[]
|
||||
): Models['WebSocketRequest_type'][] {
|
||||
if (!engineCommandManager.engineConnection?.isReady()) {
|
||||
console.log('engine connection isnt ready')
|
||||
return []
|
||||
}
|
||||
return [
|
||||
{
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'select_clear',
|
||||
},
|
||||
cmd_id: uuidv4(),
|
||||
},
|
||||
{
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'select_add',
|
||||
entities: selections.map(({ id }) => id),
|
||||
},
|
||||
cmd_id: uuidv4(),
|
||||
},
|
||||
]
|
||||
}
|
||||
@ -43,15 +43,12 @@ export function getSortFunction(sortBy: string) {
|
||||
a: ProjectWithEntryPointMetadata,
|
||||
b: ProjectWithEntryPointMetadata
|
||||
) => {
|
||||
if (
|
||||
a.entrypoint_metadata?.modifiedAt &&
|
||||
b.entrypoint_metadata?.modifiedAt
|
||||
) {
|
||||
if (a.entrypointMetadata?.modifiedAt && b.entrypointMetadata?.modifiedAt) {
|
||||
return !sortBy || sortBy.includes('desc')
|
||||
? b.entrypoint_metadata.modifiedAt.getTime() -
|
||||
a.entrypoint_metadata.modifiedAt.getTime()
|
||||
: a.entrypoint_metadata.modifiedAt.getTime() -
|
||||
b.entrypoint_metadata.modifiedAt.getTime()
|
||||
? b.entrypointMetadata.modifiedAt.getTime() -
|
||||
a.entrypointMetadata.modifiedAt.getTime()
|
||||
: a.entrypointMetadata.modifiedAt.getTime() -
|
||||
b.entrypointMetadata.modifiedAt.getTime()
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
@ -1,10 +1,14 @@
|
||||
import { FileEntry } from '@tauri-apps/api/fs'
|
||||
import {
|
||||
MAX_PADDING,
|
||||
deepFileFilter,
|
||||
getNextProjectIndex,
|
||||
getPartsCount,
|
||||
interpolateProjectNameWithIndex,
|
||||
isRelevantFileOrDir,
|
||||
} from './tauriFS'
|
||||
|
||||
describe('Test file utility functions', () => {
|
||||
describe('Test project name utility functions', () => {
|
||||
it('interpolates a project name without an index', () => {
|
||||
expect(interpolateProjectNameWithIndex('test', 1)).toBe('test')
|
||||
})
|
||||
@ -46,3 +50,101 @@ describe('Test file utility functions', () => {
|
||||
expect(getNextProjectIndex('new-project-$n', testFiles)).toBe(8)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Test file tree utility functions', () => {
|
||||
const baseFiles: FileEntry[] = [
|
||||
{
|
||||
name: 'show-me.kcl',
|
||||
path: '/projects/show-me.kcl',
|
||||
},
|
||||
{
|
||||
name: 'hide-me.jpg',
|
||||
path: '/projects/hide-me.jpg',
|
||||
},
|
||||
{
|
||||
name: '.gitignore',
|
||||
path: '/projects/.gitignore',
|
||||
},
|
||||
]
|
||||
|
||||
const filteredBaseFiles: FileEntry[] = [
|
||||
{
|
||||
name: 'show-me.kcl',
|
||||
path: '/projects/show-me.kcl',
|
||||
},
|
||||
]
|
||||
|
||||
it('Only includes files relevant to the project in a flat directory', () => {
|
||||
expect(deepFileFilter(baseFiles, isRelevantFileOrDir)).toEqual(
|
||||
filteredBaseFiles
|
||||
)
|
||||
})
|
||||
|
||||
const nestedFiles: FileEntry[] = [
|
||||
...baseFiles,
|
||||
{
|
||||
name: 'show-me',
|
||||
path: '/projects/show-me',
|
||||
children: [
|
||||
{
|
||||
name: 'show-me-nested',
|
||||
path: '/projects/show-me/show-me-nested',
|
||||
children: baseFiles,
|
||||
},
|
||||
{
|
||||
name: 'hide-me',
|
||||
path: '/projects/show-me/hide-me',
|
||||
children: baseFiles.filter((file) => file.name !== 'show-me.kcl'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'hide-me',
|
||||
path: '/projects/hide-me',
|
||||
children: baseFiles.filter((file) => file.name !== 'show-me.kcl'),
|
||||
},
|
||||
]
|
||||
|
||||
const filteredNestedFiles: FileEntry[] = [
|
||||
...filteredBaseFiles,
|
||||
{
|
||||
name: 'show-me',
|
||||
path: '/projects/show-me',
|
||||
children: [
|
||||
{
|
||||
name: 'show-me-nested',
|
||||
path: '/projects/show-me/show-me-nested',
|
||||
children: filteredBaseFiles,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
it('Only includes directories that include files relevant to the project in a nested directory', () => {
|
||||
expect(deepFileFilter(nestedFiles, isRelevantFileOrDir)).toEqual(
|
||||
filteredNestedFiles
|
||||
)
|
||||
})
|
||||
|
||||
const withHiddenDir: FileEntry[] = [
|
||||
...baseFiles,
|
||||
{
|
||||
name: '.hide-me',
|
||||
path: '/projects/.hide-me',
|
||||
children: baseFiles,
|
||||
},
|
||||
]
|
||||
|
||||
it(`Hides folders that begin with a ".", even if they contain relevant files`, () => {
|
||||
expect(deepFileFilter(withHiddenDir, isRelevantFileOrDir)).toEqual(
|
||||
filteredBaseFiles
|
||||
)
|
||||
})
|
||||
|
||||
it(`Properly counts the number of relevant files and directories in a project`, () => {
|
||||
expect(getPartsCount(nestedFiles)).toEqual({
|
||||
kclFileCount: 2,
|
||||
kclDirCount: 2,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -15,6 +15,7 @@ export const FILE_EXT = '.kcl'
|
||||
export const PROJECT_ENTRYPOINT = 'main' + FILE_EXT
|
||||
const INDEX_IDENTIFIER = '$n' // $nn.. will pad the number with 0s
|
||||
export const MAX_PADDING = 7
|
||||
const RELEVANT_FILE_TYPES = ['kcl']
|
||||
|
||||
// Initializes the project directory and returns the path
|
||||
export async function initializeProjectDirectory(directory: string) {
|
||||
@ -69,7 +70,7 @@ export async function getProjectsInDir(projectDir: string) {
|
||||
|
||||
const projectsWithMetadata = await Promise.all(
|
||||
readProjects.map(async (p) => ({
|
||||
entrypoint_metadata: await metadata(p.path + '/' + PROJECT_ENTRYPOINT),
|
||||
entrypointMetadata: await metadata(p.path + '/' + PROJECT_ENTRYPOINT),
|
||||
...p,
|
||||
}))
|
||||
)
|
||||
@ -77,6 +78,135 @@ export async function getProjectsInDir(projectDir: string) {
|
||||
return projectsWithMetadata
|
||||
}
|
||||
|
||||
export const isHidden = (fileOrDir: FileEntry) =>
|
||||
!!fileOrDir.name?.startsWith('.')
|
||||
|
||||
export const isDir = (fileOrDir: FileEntry) =>
|
||||
'children' in fileOrDir && fileOrDir.children !== undefined
|
||||
|
||||
export function deepFileFilter(
|
||||
entries: FileEntry[],
|
||||
filterFn: (f: FileEntry) => boolean
|
||||
): FileEntry[] {
|
||||
const filteredEntries: FileEntry[] = []
|
||||
for (const fileOrDir of entries) {
|
||||
if ('children' in fileOrDir && fileOrDir.children !== undefined) {
|
||||
const filteredChildren = deepFileFilter(fileOrDir.children, filterFn)
|
||||
if (filterFn(fileOrDir)) {
|
||||
filteredEntries.push({
|
||||
...fileOrDir,
|
||||
children: filteredChildren,
|
||||
})
|
||||
}
|
||||
} else if (filterFn(fileOrDir)) {
|
||||
filteredEntries.push(fileOrDir)
|
||||
}
|
||||
}
|
||||
return filteredEntries
|
||||
}
|
||||
|
||||
export function deepFileFilterFlat(
|
||||
entries: FileEntry[],
|
||||
filterFn: (f: FileEntry) => boolean
|
||||
): FileEntry[] {
|
||||
const filteredEntries: FileEntry[] = []
|
||||
for (const fileOrDir of entries) {
|
||||
if ('children' in fileOrDir && fileOrDir.children !== undefined) {
|
||||
const filteredChildren = deepFileFilterFlat(fileOrDir.children, filterFn)
|
||||
if (filterFn(fileOrDir)) {
|
||||
filteredEntries.push({
|
||||
...fileOrDir,
|
||||
children: filteredChildren,
|
||||
})
|
||||
}
|
||||
filteredEntries.push(...filteredChildren)
|
||||
} else if (filterFn(fileOrDir)) {
|
||||
filteredEntries.push(fileOrDir)
|
||||
}
|
||||
}
|
||||
return filteredEntries
|
||||
}
|
||||
|
||||
// Read the contents of a project directory
|
||||
// and return all relevant files and sub-directories recursively
|
||||
export async function readProject(projectDir: string) {
|
||||
const readFiles = await readDir(projectDir, {
|
||||
recursive: true,
|
||||
})
|
||||
|
||||
return deepFileFilter(readFiles, isRelevantFileOrDir)
|
||||
}
|
||||
|
||||
// Given a read project, return the number of .kcl files,
|
||||
// both in the root directory and in sub-directories,
|
||||
// and folders that contain at least one .kcl file
|
||||
export function getPartsCount(project: FileEntry[]) {
|
||||
const flatProject = deepFileFilterFlat(project, isRelevantFileOrDir)
|
||||
|
||||
const kclFileCount = flatProject.filter((f) =>
|
||||
f.name?.endsWith(FILE_EXT)
|
||||
).length
|
||||
const kclDirCount = flatProject.filter((f) => f.children !== undefined).length
|
||||
|
||||
return {
|
||||
kclFileCount,
|
||||
kclDirCount,
|
||||
}
|
||||
}
|
||||
|
||||
// Determines if a file or directory is relevant to the project
|
||||
// i.e. not a hidden file or directory, and is a relevant file type
|
||||
// or contains at least one relevant file (even if it's nested)
|
||||
// or is a completely empty directory
|
||||
export function isRelevantFileOrDir(fileOrDir: FileEntry) {
|
||||
let isRelevantDir = false
|
||||
if ('children' in fileOrDir && fileOrDir.children !== undefined) {
|
||||
isRelevantDir =
|
||||
!isHidden(fileOrDir) &&
|
||||
(fileOrDir.children.some(isRelevantFileOrDir) ||
|
||||
fileOrDir.children.length === 0)
|
||||
}
|
||||
const isRelevantFile =
|
||||
!isHidden(fileOrDir) &&
|
||||
RELEVANT_FILE_TYPES.some((ext) => fileOrDir.name?.endsWith(ext))
|
||||
|
||||
return (
|
||||
(isDir(fileOrDir) && isRelevantDir) || (!isDir(fileOrDir) && isRelevantFile)
|
||||
)
|
||||
}
|
||||
|
||||
// Deeply sort the files and directories in a project like VS Code does:
|
||||
// The main.kcl file is always first, then files, then directories
|
||||
// Files and directories are sorted alphabetically
|
||||
export function sortProject(project: FileEntry[]): FileEntry[] {
|
||||
const sortedProject = project.sort((a, b) => {
|
||||
if (a.name === PROJECT_ENTRYPOINT) {
|
||||
return -1
|
||||
} else if (b.name === PROJECT_ENTRYPOINT) {
|
||||
return 1
|
||||
} else if (a.children === undefined && b.children !== undefined) {
|
||||
return -1
|
||||
} else if (a.children !== undefined && b.children === undefined) {
|
||||
return 1
|
||||
} else if (a.name && b.name) {
|
||||
return a.name.localeCompare(b.name)
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
})
|
||||
|
||||
return sortedProject.map((fileOrDir: FileEntry) => {
|
||||
if ('children' in fileOrDir && fileOrDir.children !== undefined) {
|
||||
return {
|
||||
...fileOrDir,
|
||||
children: sortProject(fileOrDir.children),
|
||||
}
|
||||
} else {
|
||||
return fileOrDir
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Creates a new file in the default directory with the default project name
|
||||
// Returns the path to the new file
|
||||
export async function createNewProject(
|
||||
@ -104,7 +234,7 @@ export async function createNewProject(
|
||||
return {
|
||||
name: path.slice(path.lastIndexOf('/') + 1),
|
||||
path: path,
|
||||
entrypoint_metadata: m,
|
||||
entrypointMetadata: m,
|
||||
children: [
|
||||
{
|
||||
name: PROJECT_ENTRYPOINT,
|
||||
|
||||
@ -93,6 +93,6 @@ export async function executor(
|
||||
yz: uuidv4(),
|
||||
xz: uuidv4(),
|
||||
})
|
||||
await engineCommandManager.waitForAllCommands(ast, programMemory)
|
||||
await engineCommandManager.waitForAllCommands()
|
||||
return programMemory
|
||||
}
|
||||
|
||||
178
src/machines/fileMachine.ts
Normal file
@ -0,0 +1,178 @@
|
||||
import { assign, createMachine } from 'xstate'
|
||||
import { ProjectWithEntryPointMetadata } from 'Router'
|
||||
import { FileEntry } from '@tauri-apps/api/fs'
|
||||
|
||||
export const FILE_PERSIST_KEY = 'Last opened KCL files'
|
||||
export const DEFAULT_FILE_NAME = 'Untitled'
|
||||
|
||||
export const fileMachine = createMachine(
|
||||
{
|
||||
/** @xstate-layout N4IgpgJg5mDOIC5QAkD2BbMACdBDAxgBYCWAdmAHTK6xampYAOATqgFZj4AusAxAMLMwuLthbtOXANoAGALqJQjVLGJdiqUopAAPRAHYAbPooAWABwBGUwE5zAJgeGArM-MAaEAE9EN0wGYKGX97GX1nGVNDS0MbfwBfeM80TBwCEnIqGiZWDm4+ACUwUlxU8TzpeW1lVXVNbT0EcJNg02d-fzt7fU77Tx8EQ0iKCPtnfUsjGRtLGXtE5IxsPCIySmpacsk+QWFRHIluWQUkEBq1DS1TxqN7ChjzOxtXf0t7a37EcwsRibH-ZzRezA8wLEApZbpNZZTa5ba8AAiYAANmB9lsjlVTuc6ldQDdDOYKP5bm0os5TDJDJ8mlEzPpzIZHA4bO9umCIWlVpkNgcKnwAPKMYp8yTHaoqC71a6IEmBUz6BkWZzWDq2Uw0qzOIJAwz+PXWfSmeZJcFLLkZSi7ERkKCi7i8CCaShkABuqAA1pR8EIRGAALQYyonJSS3ENRDA2wUeyvd6dPVhGw0-RhGOp8IA8xGFkc80rS0Ua3qUh2oO8MDMVjMCiMZEiABmqGY6AoPr2AaD4uxYcuEYQoQpQWNNjsMnMgLGKbT3TC7TcOfsNjzqQL0KKJXQtvtXEdzoobs9lCEm87cMxIbOvel+MQqtMQRmS5ks31sZpAUsZkcIX+cQZJIrpC3KUBupTbuWlbVrW9ZcE2LYUCepRnocwYSrUfYyggbzvBQ+jMq49imLYwTUt4iCft+5i-u0-7UfoQEWtCSKoiWZbnruTqZIeXoUBAKJoihFTdqGGE3rod7UdqsQTI8hiGAqrIauRA7RvYeoqhO1jtAqjFrpkLFohBHEVlWzYwY2zatvxrFCWKWKiVKeISdh4yBJE-jGs4fhhA4zg0kRNgxhplhaW0nn4XpUKZEUuAQMZqF8FxLqkO6vG+hAgYcbAIlXmJzmNERdy0RYNiKgpthxDSEU6q8MSTJYjWGFFIEULF8WljuSX7jxx7CJlQY5ZYl44pht4IP61gyPc8njt0lIuH51UKrVVITEyMy2C1hbtQl-KmdBdaWQhGVZYluWjeJjSTf402shMEyuEyljPAFL0UNmMiuN86lWHMiSmvQ-HwKcnL6WA6FOf2k3mESMRDA4RpUm4U4qf6gSEt0QIvvqfjOCaiyrtF6zZPQXWQ+GWFlUEsbmNMf1TV9NLeXDcqRIySnNaaYPEzC5M9vl-b+IyFCjupryPF9jKWP5Kks-cbMWLERHRNt0LFntkgU2NLk4dqsz43YsTK++Kk2C+MbTOOcxzOMrhqzFxTgZ1Qba1dd6BUE1jGsLMxxK9KlDNqm3tMLUQvqYlgO5QhlsTubsFXesTTUuPTfHExshDS0RftRftGgEnTZtHbX9Zr+QJ-2S4Y3qnmTC+4tMyp1EfeOnmeQqdOhyXQrFOXXCV1hCkmLDOnBJYvRRDSsyRzGjiKj0lKdAkANAA */
|
||||
id: 'File machine',
|
||||
|
||||
initial: 'Reading files',
|
||||
|
||||
context: {
|
||||
project: {} as ProjectWithEntryPointMetadata,
|
||||
selectedDirectory: {} as FileEntry,
|
||||
},
|
||||
|
||||
on: {
|
||||
assign: {
|
||||
actions: assign((_, event) => ({
|
||||
...event.data,
|
||||
})),
|
||||
target: '.Reading files',
|
||||
},
|
||||
},
|
||||
states: {
|
||||
'Has no files': {
|
||||
on: {
|
||||
'Create file': {
|
||||
target: 'Creating file',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
'Has files': {
|
||||
on: {
|
||||
'Rename file': {
|
||||
target: 'Renaming file',
|
||||
},
|
||||
|
||||
'Create file': {
|
||||
target: 'Creating file',
|
||||
},
|
||||
|
||||
'Delete file': {
|
||||
target: 'Deleting file',
|
||||
},
|
||||
|
||||
'Open file': {
|
||||
target: 'Opening file',
|
||||
},
|
||||
|
||||
'Set selected directory': {
|
||||
target: 'Has files',
|
||||
actions: ['setSelectedDirectory'],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
'Creating file': {
|
||||
invoke: {
|
||||
id: 'create-file',
|
||||
src: 'createFile',
|
||||
onDone: [
|
||||
{
|
||||
target: 'Reading files',
|
||||
actions: ['toastSuccess'],
|
||||
},
|
||||
],
|
||||
onError: [
|
||||
{
|
||||
target: 'Reading files',
|
||||
actions: ['toastError'],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
'Renaming file': {
|
||||
invoke: {
|
||||
id: 'rename-file',
|
||||
src: 'renameFile',
|
||||
onDone: [
|
||||
{
|
||||
target: '#File machine.Reading files',
|
||||
actions: ['toastSuccess'],
|
||||
},
|
||||
],
|
||||
onError: [
|
||||
{
|
||||
target: '#File machine.Reading files',
|
||||
actions: ['toastError'],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
'Deleting file': {
|
||||
invoke: {
|
||||
id: 'delete-file',
|
||||
src: 'deleteFile',
|
||||
onDone: [
|
||||
{
|
||||
actions: ['toastSuccess'],
|
||||
target: '#File machine.Reading files',
|
||||
},
|
||||
],
|
||||
onError: {
|
||||
actions: ['toastError'],
|
||||
target: '#File machine.Has files',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
'Reading files': {
|
||||
invoke: {
|
||||
id: 'read-files',
|
||||
src: 'readFiles',
|
||||
onDone: [
|
||||
{
|
||||
cond: 'Has at least 1 file',
|
||||
target: 'Has files',
|
||||
actions: ['setFiles'],
|
||||
},
|
||||
{
|
||||
target: 'Has no files',
|
||||
actions: ['setFiles'],
|
||||
},
|
||||
],
|
||||
onError: [
|
||||
{
|
||||
target: 'Has no files',
|
||||
actions: ['toastError'],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
'Opening file': {
|
||||
entry: ['navigateToFile'],
|
||||
},
|
||||
},
|
||||
|
||||
schema: {
|
||||
events: {} as
|
||||
| { type: 'Open file'; data: { name: string } }
|
||||
| {
|
||||
type: 'Rename file'
|
||||
data: { oldName: string; newName: string; isDir: boolean }
|
||||
}
|
||||
| { type: 'Create file'; data: { name: string; makeDir: boolean } }
|
||||
| { type: 'Delete file'; data: FileEntry }
|
||||
| { type: 'Set selected directory'; data: FileEntry }
|
||||
| { type: 'navigate'; data: { name: string } }
|
||||
| {
|
||||
type: 'done.invoke.read-files'
|
||||
data: ProjectWithEntryPointMetadata
|
||||
}
|
||||
| { type: 'assign'; data: { [key: string]: any } },
|
||||
},
|
||||
|
||||
predictableActionArguments: true,
|
||||
preserveActionOrder: true,
|
||||
tsTypes: {} as import('./fileMachine.typegen').Typegen0,
|
||||
},
|
||||
{
|
||||
actions: {
|
||||
setFiles: assign((_, event) => {
|
||||
return { project: event.data }
|
||||
}),
|
||||
setSelectedDirectory: assign((_, event) => {
|
||||
return { selectedDirectory: event.data }
|
||||
}),
|
||||
},
|
||||
}
|
||||
)
|
||||
96
src/machines/fileMachine.typegen.ts
Normal file
@ -0,0 +1,96 @@
|
||||
// This file was automatically generated. Edits will be overwritten
|
||||
|
||||
export interface Typegen0 {
|
||||
'@@xstate/typegen': true
|
||||
internalEvents: {
|
||||
'done.invoke.create-file': {
|
||||
type: 'done.invoke.create-file'
|
||||
data: unknown
|
||||
__tip: 'See the XState TS docs to learn how to strongly type this.'
|
||||
}
|
||||
'done.invoke.delete-file': {
|
||||
type: 'done.invoke.delete-file'
|
||||
data: unknown
|
||||
__tip: 'See the XState TS docs to learn how to strongly type this.'
|
||||
}
|
||||
'done.invoke.read-files': {
|
||||
type: 'done.invoke.read-files'
|
||||
data: unknown
|
||||
__tip: 'See the XState TS docs to learn how to strongly type this.'
|
||||
}
|
||||
'done.invoke.rename-file': {
|
||||
type: 'done.invoke.rename-file'
|
||||
data: unknown
|
||||
__tip: 'See the XState TS docs to learn how to strongly type this.'
|
||||
}
|
||||
'error.platform.create-file': {
|
||||
type: 'error.platform.create-file'
|
||||
data: unknown
|
||||
}
|
||||
'error.platform.delete-file': {
|
||||
type: 'error.platform.delete-file'
|
||||
data: unknown
|
||||
}
|
||||
'error.platform.read-files': {
|
||||
type: 'error.platform.read-files'
|
||||
data: unknown
|
||||
}
|
||||
'error.platform.rename-file': {
|
||||
type: 'error.platform.rename-file'
|
||||
data: unknown
|
||||
}
|
||||
'xstate.init': { type: 'xstate.init' }
|
||||
}
|
||||
invokeSrcNameMap: {
|
||||
createFile: 'done.invoke.create-file'
|
||||
deleteFile: 'done.invoke.delete-file'
|
||||
readFiles: 'done.invoke.read-files'
|
||||
renameFile: 'done.invoke.rename-file'
|
||||
}
|
||||
missingImplementations: {
|
||||
actions: 'navigateToFile' | 'toastError' | 'toastSuccess'
|
||||
delays: never
|
||||
guards: 'Has at least 1 file'
|
||||
services: 'createFile' | 'deleteFile' | 'readFiles' | 'renameFile'
|
||||
}
|
||||
eventsCausingActions: {
|
||||
navigateToFile: 'Open file'
|
||||
setFiles: 'done.invoke.read-files'
|
||||
setSelectedDirectory: 'Set selected directory'
|
||||
toastError:
|
||||
| 'error.platform.create-file'
|
||||
| 'error.platform.delete-file'
|
||||
| 'error.platform.read-files'
|
||||
| 'error.platform.rename-file'
|
||||
toastSuccess:
|
||||
| 'done.invoke.create-file'
|
||||
| 'done.invoke.delete-file'
|
||||
| 'done.invoke.rename-file'
|
||||
}
|
||||
eventsCausingDelays: {}
|
||||
eventsCausingGuards: {
|
||||
'Has at least 1 file': 'done.invoke.read-files'
|
||||
}
|
||||
eventsCausingServices: {
|
||||
createFile: 'Create file'
|
||||
deleteFile: 'Delete file'
|
||||
readFiles:
|
||||
| 'assign'
|
||||
| 'done.invoke.create-file'
|
||||
| 'done.invoke.delete-file'
|
||||
| 'done.invoke.rename-file'
|
||||
| 'error.platform.create-file'
|
||||
| 'error.platform.rename-file'
|
||||
| 'xstate.init'
|
||||
renameFile: 'Rename file'
|
||||
}
|
||||
matchesStates:
|
||||
| 'Creating file'
|
||||
| 'Deleting file'
|
||||
| 'Has files'
|
||||
| 'Has no files'
|
||||
| 'Opening file'
|
||||
| 'Reading files'
|
||||
| 'Renaming file'
|
||||
tags: never
|
||||
}
|
||||
@ -1,7 +1,12 @@
|
||||
import { PathToNode } from 'lang/wasm'
|
||||
import { engineCommandManager } from 'lang/std/engineConnection'
|
||||
import { isReducedMotion } from 'lang/util'
|
||||
import { Axis, Selection, SelectionRangeTypeMap, Selections } from 'useStore'
|
||||
import {
|
||||
Axis,
|
||||
Selection,
|
||||
SelectionRangeTypeMap,
|
||||
Selections,
|
||||
} from 'lib/selections'
|
||||
import { assign, createMachine } from 'xstate'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { isCursorInSketchCommandRange } from 'lang/util'
|
||||
@ -28,6 +33,16 @@ import {
|
||||
import { extrudeSketch } from 'lang/modifyAst'
|
||||
import { getNodeFromPath } from '../lang/queryAst'
|
||||
import { CallExpression, PipeExpression } from '../lang/wasm'
|
||||
import { getConstraintLevelFromSourceRange } from 'lang/std/sketchcombos'
|
||||
import {
|
||||
applyConstraintEqualAngle,
|
||||
equalAngleInfo,
|
||||
} from 'components/Toolbar/EqualAngle'
|
||||
import {
|
||||
applyRemoveConstrainingValues,
|
||||
removeConstrainingValuesInfo,
|
||||
} from 'components/Toolbar/RemoveConstrainingValues'
|
||||
import { intersectInfo } from 'components/Toolbar/Intersect'
|
||||
|
||||
export const MODELING_PERSIST_KEY = 'MODELING_PERSIST_KEY'
|
||||
|
||||
@ -49,9 +64,67 @@ export type SetSelections =
|
||||
selection: Selections
|
||||
}
|
||||
|
||||
export type ModelingMachineEvent =
|
||||
| { type: 'Deselect all' }
|
||||
| { type: 'Deselect edge'; data: Selection & { type: 'edge' } }
|
||||
| { type: 'Deselect axis'; data: Axis }
|
||||
| {
|
||||
type: 'Deselect segment'
|
||||
data: Selection & { type: 'line' | 'arc' }
|
||||
}
|
||||
| { type: 'Deselect face'; data: Selection & { type: 'face' } }
|
||||
| {
|
||||
type: 'Deselect point'
|
||||
data: Selection & { type: 'point' | 'line-end' | 'line-mid' }
|
||||
}
|
||||
| { type: 'Enter sketch' }
|
||||
| { type: 'Select all'; data: Selection & { type: 'all ' } }
|
||||
| { type: 'Select edge'; data: Selection & { type: 'edge' } }
|
||||
| { type: 'Select axis'; data: Axis }
|
||||
| { type: 'Select segment'; data: Selection & { type: 'line' | 'arc' } }
|
||||
| { type: 'Select face'; data: Selection & { type: 'face' } }
|
||||
| { type: 'Select default plane'; data: { planeId: string } }
|
||||
| { type: 'Set selection'; data: SetSelections }
|
||||
| {
|
||||
type: 'Select point'
|
||||
data: Selection & { type: 'point' | 'line-end' | 'line-mid' }
|
||||
}
|
||||
| { type: 'Sketch no face' }
|
||||
| { type: 'Toggle gui mode' }
|
||||
| { type: 'Cancel' }
|
||||
| { type: 'CancelSketch' }
|
||||
| {
|
||||
type: 'Add point'
|
||||
data: {
|
||||
coords: { x: number; y: number }[]
|
||||
axis: 'xy' | 'xz' | 'yz' | '-xy' | '-xz' | '-yz' | null
|
||||
segmentId?: string
|
||||
}
|
||||
}
|
||||
| { type: 'Equip tool' }
|
||||
| { type: 'Equip move tool' }
|
||||
| { type: 'Set radius' }
|
||||
| { type: 'Complete line' }
|
||||
| { type: 'Set distance' }
|
||||
| { type: 'Equip new tool' }
|
||||
| { type: 'update_code'; data: string }
|
||||
| { type: 'Make segment horizontal' }
|
||||
| { type: 'Make segment vertical' }
|
||||
| { type: 'Constrain horizontal distance' }
|
||||
| { type: 'Constrain vertical distance' }
|
||||
| { type: 'Constrain angle' }
|
||||
| { type: 'Constrain perpendicular distance' }
|
||||
| { type: 'Constrain horizontally align' }
|
||||
| { type: 'Constrain vertically align' }
|
||||
| { type: 'Constrain length' }
|
||||
| { type: 'Constrain equal length' }
|
||||
| { type: 'Constrain parallel' }
|
||||
| { type: 'Constrain remove constraints' }
|
||||
| { type: 'extrude intent' }
|
||||
|
||||
export const modelingMachine = createMachine(
|
||||
{
|
||||
/** @xstate-layout N4IgpgJg5mDOIC5QFkD2EwBsCWA7KAxAMICGuAxlgNoAMAuoqAA6qzYAu2qujIAHogDMAVgDsAOmEiAjLMGCAHIIAsAJlHKANCACeiaQDZBEsctGrVATmmiaNQQF8H2tBhz5x2CJjAEAymDsAASwWGDknNy0DEggLGyRPLECCMrCCuL20qrCRpYagvnaegjZBtLiBqLploIGCtZVyk4u6Fh4UJ7evgAicGERQSx47NG88RxcSaApdcKSNKIKi8ppwsrLwsX60oriqgaWBgbKImpWqi0gru0eXj4EfaE+g5AwY7ETibyzx5lLu1sygM61ERV0+ho0mU4gURlOwihlis2SuN3cnXuvX6L2CJD42FgH2YrEm3B+QnM4mqdhoyPUILqBm2CAaFTS5houQUCMczmubQxXQeAVxQ1QI2JcVJ32SiGUXPEhjBtWklgaokMzIhqVUNHEG0WgjVqkEUN2aMFHWFvlF4WCbzAUq+UwpqRoGQ2pwacPW4JKJxhgYU0kWu3Wymklrc1qx-gGeIJRPo4xlrrlqUEqkqdMKOSRNEOLPWGTVhkswlU0O5fNaMbu3XjYoAZiRKM60+SMwqMgY7Go6mbalCWaIjLCjtJ0n36tUFNHbpjGwBRXDsMAAJxCAGtAuQABYdhLpmY7SPiSzAyOXwGFUQs01BtXVEEV9VSZr89Gxldrzc7vdD2kGISWPLtTwQepBGpEN8lDGhVBDLYdTUfVZzvYFQ3MS4vytBsHieBMglbdsU0+Ttpn4Sk0KzBR1EKdING1EozQqYxajyNQ6MOBchTjO1BhITBMCPMlKJSSNoIsEEllQrlhGQko9RhZEDFUL1vWEUMcLrRcbUeHF7SCISRLI0CxLdRR2ROJQwQQ2RmMQENJD1OxjR5aE+1rAV6yXB4wD4dgNwAVwwIIRjANdRNlCDlGRJU6kfapK0vdYWSnBQJEsfJMtODYrM-XS+MbAKgtCsBwr-KLgNTMDxPlawJ0DKtXNkLl0o2NjNULPMPQ2HSfL0vxd3YA9iDIShMGGwDopPKjSg0fUaDURFFoVbJ7x1S9oOSmQcl2CtRF461ptG-dxFOg8AElGwE4JhiiszpTqt1pB9C8NA8plynVFlyn1EMzVfdTrAU46PEu87IZukUiNCKAAFtItGJ6XXA+a3vVD6vV2Y41IQlkzAMakLCnD0K0jK9wc6SGLpG67G0IsUHpRkDnosjM3oUi91SsRYjmUywWRUDJRFEY0PXzdQrGpunALls6YexZ4jPhpHHrZtH6tKHkJHkLJ1AUGpsofQxxBEWpxayCXFFl2noZXABHYLsCYIJ2FQVBTM1ijLK5Njp20wtPMEUc+yVRC4vgpZlqjXDfIVg9E-3JWCGXZ3XaCBHUAANwqj2vdm9HZg9NjpPUZbIykFkKYNTD1nsGycjt+modb1OAmCFWIimIvtbVMwczF4wtXqRyEDHfVsiNwtahyTU46Kk7W+T1PkBIXcQjARHkaCPON04cghL716kKVdYjErEWFEy9LoXZKsQT7I3jWEFv5Ydh5183tXd-3VANzYAAF7cHYMfVGvtOZggyOkewqkjBqQrOlOBlQqjyCrEoacR145DRXp-XwRBuCwCCiQPAQR-6AJAWuISQQICEjARQJ0ECXpQOqJULm+QVArFvjqcsMIDg2UwRsdIhVBpCntu3RshDcDEI3KQ3Ae9NyHxoXQ4hE0mE+xYRBGwlh9QpQOp5awdR0piAqOLLS5g0HmlEd+CGeDJEPGkbI+Rxl8A+BPpzOEKk+wOT6nYHk6VNgGkOLkUJOQ1jvzOqvKRRCSFkJ8Pgdgh5mEc20UbDIN4GjHAKopfQjQLzLAVCIrkdE344PEfYwCqcnFxIURQ4BoCTI6GMjgKAuAPHaKWMTJQCFzBZIOEbdK6kYSRg0EsRCdhLyWEiUnfBxBYlyLIfvZRwlmlCWwG0jpGNljQQVMCKohZzBiHSiIHasg1DIhEFpOEMy25VJiTI2pQQwDOxoQkqASStkpDejYA0voqg5HKFOLQvC0gwK0sOPs1g3K3PEAAGTwBVAAKp7TAacM5u2znnd2qKvlCEUDA8ZCk1TpE8qOJQSouQ9SnMIfIGhYUItwMi1F4gAAKEo1xBAAIIQAwBAAgPKIDiklCkmK80zTAnNg0dIUg1JqFWMWKESoThQkyn6I2UgGWIqCCir2F1t7q2CIKyAAreXCo1rVVJ4rEIVEOJlLyZpCz1EJqaA0Bx1JPx+dJLVTKdUsoCDvTlxr+WEIRkwHw64gjuA0ZasVsxgQSBnsPMFyJcmpBvkqXKozlpWDij65leqA2Gu5byk16cXZuyZQAdxxYXUVc1ZgV32G9MeCEjhHGFm9JU2Uq5yGlTYvCNMV6MoLZgcQV1cAcAIHihA6k1T-CuZqXIVc-r2EyPUMQmokJ2AMPmv1eqJ1TqoDVciWj5qmmypkS5cClAghBSUSsxNCglJSosRYu7ynL3liO-dY6AByqAgjspGLAU1QqWYzoZJYC8G0wSvuRMWdSSoim6N0a-TKsKuVVtIcEepVCwGYFofQ9RlVmyoAIBAbgYBPC4BzqgXc4gYDsAALR4caZgZjeAyOQaWPMRCyJNTGmBccdKS7YRmmMG5fs0zP12PllhnD5CAENOoYR1RDDKCkfI5uDcADxDhpIOwMjG4EaMcCKx5T+GhKcdwNx+txdECG3mIdaWjQjaXnSlUDIqroTmHDOsTD2GOCKIPtgI+aniOMK0xRqjNG6MMaY8x5ZYXrNcdQDx3RkhzDytonCYwJjGqIUyvIWwiJnyBcU8l8LRG1FRbSwQHTemDNGYAaZxLVXUu2fS-Z7W6hMtiHUupXLRhNolGBTtG+4tjBwjpLUCrwWyBQB8NFyjTK4v0eo4lxbPgbN2c0ValIfXSynGylBKcVZAmVgvB6ZayxR4enm3iNxFV6uNY3PpzAhnjNtfM9tsAu3uv7bjY5lUkhgTqnbR6qcgTdhXtkDYeHxojiPajZFD5+4VuxbwPFzb5n3lJIBxl4m6hKwKQ0Ju1Q6V1QVGWh6E4Fh8wbBR-jjHr2Ny6fe8177ZmWMs8Jz1t0fXibbSrPkBUsc01qj2Cqxu8gn5KFuUEXAgGSK2iIhgVswVMD3U+0ymd2Q4qSArG9a8PJgShxQlOGDUI6hG2nuYbytjOgkCC5wfAW9cRTHjF3BMvcBec2DtSXRY4eTHEjND3hy7qR9g2Oqe1l55yyc6AecI24Oge-tF7-Xoz9jLo5AcXpJi1J-NWOaOosdZYp-IGn933dEjTpPeZYHpRKaVGNBXDQ6pwcdSkOwlQtLr3LBwvyZXGB4CxCd7GhtQg6SSBkHIRQKh1D3sQMx5zyxzRSCkLog4i8xE-h8FPhzqRi-b8OYcTU8ILdjcHBHXRdI3ohlOI7wdycj-90ONmHNq09lQnUCyZydzRQTqLSY4KcWFfBd-V6S5fYJkG3eQQoKoGuPUXPKQBAkMB3AdBOWmH9XVTAKAjMaSEwS5WlaoLMCwFfWdM0SoHtK+RCDBGWJPZOeFbVPA8QHoKjAgiCYwag3IcWG1VVBkRDaCZ9f6WlKwBXJgnA1gllYDINUtCALg89NDc2ThXKBAk4UbRASsIMFDG+EEdYCwPdNgotXeYNJQxtcOODCmfZIcRDXQrkOPAw84YwllQ9dgCwxzV+I3U0UgqQBnSg65fYC9FQFQU4OkD9JeOTKJXAllADIDDldgcfJvafWdE4YmU4H6U0aeOEewyoFDOkLIRiWFNAbFPAzwhAErLKQFMwBNOoaQGuagy8coVaeeKoGTKIodeTV3JTShdjGrDTF7LrCoi9JaGwARDBAfTzfrXIIrUJKEEEFHDrCLWrTTNLEYhUHaQcKEHNbIZaExDYWA0AjiCwI4RPTo5ghTBbZ7LTDY35PUIwIcZaEQJQQJRYTIEWBEewOoCsZnNHJJW409A7RzcPD6d8dBCFXIKnEQTINtZKaoPUAaJ3ZOJXFXNsMACos46kEQVVawCsQsSnFCd4woHkCItqKsWWF3HDdPOvOaLWN0XKC8A5dzG1PgkxTKHmJYAofKJ-SvfcVPGk33dGekzmBnSQE4anQ2OKQodk7MWQCxREFVOiJwJwIAA */
|
||||
/** @xstate-layout N4IgpgJg5mDOIC5QFkD2EwBsCWA7KAxAMICGuAxlgNoAMAuoqAA6qzYAu2qujIAHogDMAVgDsAOmEiAjLMGCAHIIAsAJlHKANCACeiaQDZBEsctGrVATmmiaNQQF8H2tBhz5x2CJjAEAymDsAASwWGDknNy0DEggLGyRPLECCMrCCuL20qrCRpYagvnaegjZBtLiBqLploIGCtZVyk4u6Fh4UJ7evgAicGERQSx47NG88RxcSaApdcKSNKIKi8ppwsrLwsX60oriqgaWBgbKImpWqi0gru0eXj4EfaE+g5AwY7ETibyzx5lLu1sygM61ERV0+ho0mU4gURlOwihlis2SuN3cnXuvX6L2CJD42FgH2YrEm3B+QnM4mqdhoyPUILqBm2CAaFTS5houQUCMczmubQxXQeAVxQ1QI2JcVJ32SiGUXPEhjBtWklgaokMzIhqVUNHEG0WgjVqkEUN2aMFHWFvlF4WCbzAUq+UwpqRoGQ2pwacPW4JKJxhgYU0kWu3Wymklrc1qx-gGeIJRPo4xlrrlqUEqkqdMKOSRNEOLPWGTVhkswlU0O5fNaMbu3XjYoAZiRKM60+SMwqMgY7Go6mbalCWaIjLCjtJ0n36tUFNHbpjGwBRXDsMAAJxCAGtAuQABYdhLpmY7SPiSzAyOXwGFUQs01BtXVEEV9VSZr89Gxldrzc7vdD2kGISWPLtTwQepBGpEN8lDGhVBDLYdTUfVZzvYFQ3MS4vytBsHieBMglbdsU0+Ttpn4Sk0KzBR1EKdING1EozQqYxajyNQ6MOBchTjO1BhITBMCPMlKJSSNoIsEEllQrlhGQko9RhZEDFUL1vWEUMcLrRcbUeHF7SCISRLI0CxLdRR2ROJQwQQ2RmMQENJD1OxjR5aE+1rAV6yXB4wD4dgNwAVwwIIRjANdRNlCDlGRJU6kfapK0vdYWSnBQJEsfJMtODYrM-XS+MbAKgtCsBwr-KLgNTMDxPlawJ0DKtXNkLl0o2NjNULPMPQ2HSfL0vxd3YA9iDIShMGGwDopPKjSg0fUaDURFFoVbJ7x1S9oOSmQcl2CtRF461ptG-dxFOg8AElGwE4JhiiszpTqt1pB9C8NA8plynVFlyn1EMzVfdTrAU46PEu87IZukUiNCKAAFtItGJ6XXA+a3vVD6vV2Y41IQlkzAMakLCnD0K0jK9wc6SGLpG67G0IsUHpRkDnosjM3oUi91SsRYjmUywWRUDJRFEY0PXzdQrGpunALls6YexZ4jPhpHHrZtH6tKHkJHkLJ1AUGpsofQxxBEWpxayCXFFl2noZXABHYLsCYIJ2FQVBTM1ijLK5Njp20wtPMEUc+yVRC4vgpZlqjXDfIVg9E-3JWCGXZ3XaCBHUAANwqj2vdm9HZg9NjpPUZbIykFkKYNTD1nsGycjt+modb1OAmCFWIimIvtbVMwczF4wtXqRyEDHfVsiNwtahyTU46Kk7W+T1PkBIXcQjARHkaCPON04cghL716kKVdYjErEWFEy9LoXZKsQT7I3jWEFv5Ydh5183tXd-3VANzYAAF7cHYMfVGvtOZggyOkewqkjBqQrOlOBlQqjyCrEoacR145DRXp-XwRBuCwCCiQPAQR-6AJAWuISQQICEjARQJ0ECXpQOqJIawHl0hMizOlfI8xsrqTBHZD084cFCntu3RshDcDEI3KQ3Ae9NyHxoXQ4hE0mE+xYRBGwlh9SIlNEoRQvMFDpRfgacohhIyS0jO-M6q8pFEJIWQsgUAfAn05nCFS8g6j2AOPSIWOo9SZSVBWBSthMpiykLYpO+DiCOLkWQnw+B2CHmYRzbRRsMig2hCI0MwhLymyxrYPI5g5zZEKoNcReDJEPGkbI+RQxNxMEinQ8gwVMAkC3KohhpFNHpIxjSTIGweRV0LMtRSiBTSXkqNyMW8kBzRLboBVOdSnEKIocA0BJkdDGRwFAXA7jtFLGJkoBC5gGhqXqBM0o6kYSRg0EsRCdhLyWEWfY2p8SGn72UcJHZQlsD7MORjZY0EFTAiqIWcwYh0oiB2rINQyIRBaThG82JqyEkKLAM7GhSSoApKBSkN6NgDS+iqDkCxaUdTVhgVpYcfZrBuVRTUghnyyFME6SZagaSYrAsRDzVYZZVjqACSULU8wpxmGROsLMhQDBMuWQ4mRayggbjANnPOQRyCsrXMmPpPLCVi2zOkNQ3oRBIhMVShSlgEpG0RO+coYs3kABk8AVQACqe0wGnDObt1X509QSoQigYGPIUmqdInlRxKCVFyHqU58liwqd+CGK8XW4HdZ68QAAFCUa4ggAEEIAYAgAQQtEBxSSm5XNEuwJzYNHSFINSJqtA6kRBUSxUJMp+ltd5ZNNNU2uqCB6r2F1t7q2CGWyApai0Vo1rVfpsxEIVEOJlLyZpCz1EJqaA0Bx1JPyJdJZ1g7h2YFHTvPNk6S2EIRkwHw64gjuA0fO-VQhgQSBnsPNIDQciExvkqXK9zlpWDike9NQ7M0BHPROotU704uzdumgA7u7ANVbi5CArvsN6Y8EJHCOMLN6ISbxaV2PWpNeF+3yzTRmkdV1cAcAIIGhA6k1T-CRZqXIVc-r2EyFcsWRLESFlAzR09dGGNUBquRLR80pnWrpKaOBSgQQtpKJWYmhQuRGz5rYMcwnwMjoAHKoCCDmkYsBp3lpZkxhk1rrDqDBCleTxZ1JKgVPkukWRGJvLQBqk98Yu4Jl7mh7WTIsq1DNAcFQ7nNolA0PMU4fZZ7uXFuRhOtMfMifEBlyq4nguWXc9SLMBxp66OhMgv4hwZwKmNKcS83nc6Zey3gcTknzIvoQCLcVxpLzmHqNYZEd8rCSDyIUaEFMZZiOXvLbLJ6ssNZy+wRjqg9XVqEOkDIFzTTmGq15O+ujKhwiQqXXYFpJspvlvmxDpDggbKoWAzAtD6HqMqs2VABAIDcDAJ4XAOdUC7nEDAdgABaW7WzMBA7wK96zdF+FvkpjfTkMX9DZWtUsdUCFBOmkMG8y713yEAM2dQh73TnuQ7e5uDcADxC3pIOwV7G4EYA8CCDgnd2hIQ9wFDvLGZEIHFQYUU4dJqt1F4eUD61QwRQXFscHHV2OCKIPtgI+xOnuMJe29j76bvu-f+4DoH3ylfs7J9D+QB2UtVinBxUx44QwWBqycaE4tZd44N8rx7ai1dk4IBTqnNO6cAMZ3r13RvOeoBNzCJFfNkRfpF1Sui2ZtIRs1KpW2Z3KN2Nx-LlxPh1fvc+9rv7X29fZ7ABzrnK30PMaUBtxExxbAekONcqsdRYRNGWBseopxe0UeTuITPeJ8A569z7jc1OOn+4Z0z4HJey9h+5xBSO+ochzHVJVkQ489T5EyIiP0Eb9Hd7SyvfvD7Ip4v3LnzXX28A66L8z3FKTZ8m+JuqE0+SLCXg3wceYtIrDHFUhE53eXe-c-YfDcSnUfP3enQPO-U-B-Y3efGTWySoOKG+bKGwKsaFQJCwI1bKYEG+SFLSAaPtXvY-ZpDcZpXAVpdpTpd3HpCqL3S-AvXXZnMgigqgjpDcIHEnRhR-BAlIQ2GEVaI2IwTUNQDfNzc2PqNIYcNtQA+6JpFpJXagrpVXSgXPEfMfWnKAqfIHVgxQtpDgrg1Q0veAivbWAQhYJYXIEeMQh8EQPWKQ-JM0WQtPZOIIXAYzEiW0IiDAVsdpe6DpdNJjcpa1fJZFa8EZIwQmKcC8UMXGI2aecwA-PSEgOXTgfALeXEKYfzTI+0ILMw16YOakXRMcHkY4SMKcdKTjakPsDYdUVdS8URJeDwA8cIbcDoXInubgRjPg-Qe5fYTjDkA4M5KotSElVYc0HxGxVw1o8gdojI7uRIRjVrdmdre+IMY0CuDQdUYEEVPoqQSoXYaQxFD0BCJwfkDwjAeAWIPtZ9VbDrOkSQGQOQRQFQdQFTRAIHfhaBHkfJI2RHbBZovyMAO4yvYEI1DTKoQ4TUeEUOKlQcCOXROkN6EMLvRZUE-uQ4bMIDVaMFKEdQFkZyI2WoQGYk8seVRWboDE16RFfYJkKEbxWVJHBAZEJfBSRkkMJI1LXBKjY9T1aknnA4EwRFBNKQbAj45jM0SoXA+QJEiwJIvTWbHoT7AUiCYwKU3IcWJdTtBkZzaCSEvJZEdUZIqpXksDWbUzC9GDCAVUmTXRfU-IRQAoeQE4ZkysIMNzeokEdYCwRUiDMdXeS9W0xdcOBzCmcFIcZzD0rkL0hSc4P02jejdgYMyZV+dhU0UUordSYsQjO3WoPKQXI4BM09IzEzXNdga4tre4g4WtBLE0TYm+ced05AmMjzV+TKerXzfkqTBdINMWC8clMwN9OoaQGuIbD-TTasHKJoypKbOxGbTNJrJMlMjrFaAczjJiaXUcqlQsO5YEZE2QEIuVVw9LebWbbLRDDgf+YKYIVVIHAKcIG8kEns9rYwdYAchUcoDBaoU0crPcmcQgzKQsGc4g08rskdC8q8oIB8tpdcFct84mIQ79FQY0Zk2QdQc2A6BUPdEMewTszLUsv1eCiXTIN8BoIGaEVQO+P9BLVdA6XAuQ-HShMHWg0nUPFcgxWzXRLSZacoLSWPUVfIbMXnJ5H-TCRi4PFXD3NQsnDi6rakOkdYSsGeEQUxccixPsIRNTbk00jPNI4yQfeg9il86s-onRMQLSMwaWC1JSERGoj0FQEk-JV5E8o-fS4A9XOS2QC8KQOkF8EMbcpSR+EmC5WwUMU4IgnvWmUghQygpQjg1iz3YyqsyvLbDIFQWoqEKQHTOwsXJ+avDvfjHSucg8dwzwtsZ8lK7WI4UWM1TGcNQsKilCRYc2HYukCxeyWWVI67DoxYuaLWN0XKC8CFYkpdTUqo4JbKJYAofKVE2WWY+YqATonlAazmbA4bFAjaHkWoPY0odIBPKcGwWvHsHCJwIAA */
|
||||
id: 'Modeling',
|
||||
|
||||
tsTypes: {} as import('./modelingMachine.typegen').Typegen0,
|
||||
@ -72,60 +145,7 @@ export const modelingMachine = createMachine(
|
||||
},
|
||||
|
||||
schema: {
|
||||
events: {} as
|
||||
| { type: 'Deselect all' }
|
||||
| { type: 'Deselect edge'; data: Selection & { type: 'edge' } }
|
||||
| { type: 'Deselect axis'; data: Axis }
|
||||
| {
|
||||
type: 'Deselect segment'
|
||||
data: Selection & { type: 'line' | 'arc' }
|
||||
}
|
||||
| { type: 'Deselect face'; data: Selection & { type: 'face' } }
|
||||
| {
|
||||
type: 'Deselect point'
|
||||
data: Selection & { type: 'point' | 'line-end' | 'line-mid' }
|
||||
}
|
||||
| { type: 'Enter sketch' }
|
||||
| { type: 'Select all'; data: Selection & { type: 'all ' } }
|
||||
| { type: 'Select edge'; data: Selection & { type: 'edge' } }
|
||||
| { type: 'Select axis'; data: Axis }
|
||||
| { type: 'Select segment'; data: Selection & { type: 'line' | 'arc' } }
|
||||
| { type: 'Select face'; data: Selection & { type: 'face' } }
|
||||
| { type: 'Select default plane'; data: { planeId: string } }
|
||||
| { type: 'Set selection'; data: SetSelections }
|
||||
| {
|
||||
type: 'Select point'
|
||||
data: Selection & { type: 'point' | 'line-end' | 'line-mid' }
|
||||
}
|
||||
| { type: 'Sketch no face' }
|
||||
| { type: 'Toggle gui mode' }
|
||||
| { type: 'Cancel' }
|
||||
| { type: 'CancelSketch' }
|
||||
| {
|
||||
type: 'Add point'
|
||||
data: {
|
||||
coords: { x: number; y: number }[]
|
||||
axis: 'xy' | 'xz' | 'yz' | '-xy' | '-xz' | '-yz' | null
|
||||
}
|
||||
}
|
||||
| { type: 'Equip tool' }
|
||||
| { type: 'Equip move tool' }
|
||||
| { type: 'Set radius' }
|
||||
| { type: 'Complete line' }
|
||||
| { type: 'Set distance' }
|
||||
| { type: 'Equip new tool' }
|
||||
| { type: 'update_code'; data: string }
|
||||
| { type: 'Make segment horizontal' }
|
||||
| { type: 'Make segment vertical' }
|
||||
| { type: 'Constrain horizontal distance' }
|
||||
| { type: 'Constrain vertical distance' }
|
||||
| { type: 'Constrain angle' }
|
||||
| { type: 'Constrain horizontally align' }
|
||||
| { type: 'Constrain vertically align' }
|
||||
| { type: 'Constrain length' }
|
||||
| { type: 'Constrain equal length' }
|
||||
| { type: 'extrude intent' },
|
||||
// ,
|
||||
events: {} as ModelingMachineEvent,
|
||||
},
|
||||
|
||||
states: {
|
||||
@ -354,6 +374,11 @@ export const modelingMachine = createMachine(
|
||||
cond: 'Can constrain length',
|
||||
},
|
||||
|
||||
'Constrain perpendicular distance': {
|
||||
target: 'Await perpendicular distance info',
|
||||
cond: 'Can constrain perpendicular distance',
|
||||
},
|
||||
|
||||
'Constrain horizontally align': {
|
||||
cond: 'Can constrain horizontally align',
|
||||
target: 'SketchIdle',
|
||||
@ -374,6 +399,20 @@ export const modelingMachine = createMachine(
|
||||
internal: true,
|
||||
actions: ['Constrain equal length'],
|
||||
},
|
||||
|
||||
'Constrain parallel': {
|
||||
target: 'SketchIdle',
|
||||
internal: true,
|
||||
cond: 'Can canstrain parallel',
|
||||
actions: ['Constrain parallel'],
|
||||
},
|
||||
|
||||
'Constrain remove constraints': {
|
||||
target: 'SketchIdle',
|
||||
internal: true,
|
||||
cond: 'Can constrain remove constraints',
|
||||
actions: ['Constrain remove constraints'],
|
||||
},
|
||||
},
|
||||
|
||||
entry: 'equip select',
|
||||
@ -448,6 +487,35 @@ export const modelingMachine = createMachine(
|
||||
|
||||
'Move Tool': {
|
||||
entry: 'set tool move',
|
||||
|
||||
on: {
|
||||
'Set selection': {
|
||||
target: 'Move Tool',
|
||||
internal: true,
|
||||
actions: 'Set selection',
|
||||
},
|
||||
},
|
||||
|
||||
states: {
|
||||
'Move init': {
|
||||
always: [
|
||||
{
|
||||
target: 'Move without re-execute',
|
||||
cond: 'can move',
|
||||
},
|
||||
{
|
||||
target: 'Move with execute',
|
||||
cond: 'can move with execute',
|
||||
},
|
||||
'No move',
|
||||
],
|
||||
},
|
||||
'Move without re-execute': {},
|
||||
'Move with execute': {},
|
||||
'No move': {},
|
||||
},
|
||||
|
||||
initial: 'Move init',
|
||||
},
|
||||
|
||||
'Await horizontal distance info': {
|
||||
@ -497,6 +565,18 @@ export const modelingMachine = createMachine(
|
||||
onError: 'SketchIdle',
|
||||
},
|
||||
},
|
||||
|
||||
'Await perpendicular distance info': {
|
||||
invoke: {
|
||||
src: 'Get perpendicular distance info',
|
||||
id: 'get-perpendicular-distance-info',
|
||||
onDone: {
|
||||
target: 'SketchIdle',
|
||||
actions: 'Set selection',
|
||||
},
|
||||
onError: 'SketchIdle',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
initial: 'SketchIdle',
|
||||
@ -582,6 +662,8 @@ export const modelingMachine = createMachine(
|
||||
angleBetweenInfo({ selectionRanges }).enabled,
|
||||
'Can constrain length': ({ selectionRanges }) =>
|
||||
setAngleLengthInfo({ selectionRanges }).enabled,
|
||||
'Can constrain perpendicular distance': ({ selectionRanges }) =>
|
||||
intersectInfo({ selectionRanges }).enabled,
|
||||
'Can constrain horizontally align': ({ selectionRanges }) =>
|
||||
horzVertDistanceInfo({ selectionRanges, constraint: 'setHorzDistance' })
|
||||
.enabled,
|
||||
@ -590,6 +672,10 @@ export const modelingMachine = createMachine(
|
||||
.enabled,
|
||||
'Can constrain equal length': ({ selectionRanges }) =>
|
||||
setEqualLengthInfo({ selectionRanges }).enabled,
|
||||
'Can canstrain parallel': ({ selectionRanges }) =>
|
||||
equalAngleInfo({ selectionRanges }).enabled,
|
||||
'Can constrain remove constraints': ({ selectionRanges }) =>
|
||||
removeConstrainingValuesInfo({ selectionRanges }).enabled,
|
||||
'has no selection': ({ selectionRanges }) => {
|
||||
if (selectionRanges?.codeBasedSelections?.length < 1) return true
|
||||
const selection = selectionRanges?.codeBasedSelections?.[0] || {}
|
||||
@ -620,6 +706,22 @@ export const modelingMachine = createMachine(
|
||||
})
|
||||
return !!isSketchPipe && hasClose && !hasExtrude
|
||||
},
|
||||
'can move': ({ selectionRanges }) =>
|
||||
// todo check all cursors are also in the right sketch
|
||||
selectionRanges.codeBasedSelections.every(
|
||||
(selection) =>
|
||||
getConstraintLevelFromSourceRange(
|
||||
selection.range,
|
||||
kclManager.ast
|
||||
) === 'free'
|
||||
),
|
||||
'can move with execute': ({ selectionRanges }) =>
|
||||
// todo check all cursors are also in the right sketch
|
||||
selectionRanges.codeBasedSelections.every((selection) =>
|
||||
['partial', 'free'].includes(
|
||||
getConstraintLevelFromSourceRange(selection.range, kclManager.ast)
|
||||
)
|
||||
),
|
||||
},
|
||||
actions: {
|
||||
'Add to code-based selection': assign({
|
||||
@ -778,6 +880,8 @@ export const modelingMachine = createMachine(
|
||||
tool: 'move',
|
||||
},
|
||||
}),
|
||||
// TODO implement source ranges for all of these constraints
|
||||
// need to make the async like the modal constraints
|
||||
'Make selection horizontal': ({ selectionRanges }) => {
|
||||
const { modifiedAst, pathToNodeMap } = applyConstraintHorzVert(
|
||||
selectionRanges,
|
||||
@ -785,10 +889,7 @@ export const modelingMachine = createMachine(
|
||||
kclManager.ast,
|
||||
kclManager.programMemory
|
||||
)
|
||||
kclManager.updateAst(modifiedAst, true, {
|
||||
// TODO re implement cursor shit
|
||||
// callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||
})
|
||||
kclManager.updateAst(modifiedAst, true)
|
||||
},
|
||||
'Make selection vertical': ({ selectionRanges }) => {
|
||||
const { modifiedAst, pathToNodeMap } = applyConstraintHorzVert(
|
||||
@ -797,39 +898,39 @@ export const modelingMachine = createMachine(
|
||||
kclManager.ast,
|
||||
kclManager.programMemory
|
||||
)
|
||||
kclManager.updateAst(modifiedAst, true, {
|
||||
// TODO re implement cursor shit
|
||||
// callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||
})
|
||||
kclManager.updateAst(modifiedAst, true)
|
||||
},
|
||||
'Constrain horizontally align': ({ selectionRanges }) => {
|
||||
const { modifiedAst, pathToNodeMap } = applyConstraintHorzVertAlign({
|
||||
selectionRanges,
|
||||
constraint: 'setVertDistance',
|
||||
})
|
||||
kclManager.updateAst(modifiedAst, true, {
|
||||
// TODO re implement cursor shit
|
||||
// callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||
})
|
||||
kclManager.updateAst(modifiedAst, true)
|
||||
},
|
||||
'Constrain vertically align': ({ selectionRanges }) => {
|
||||
const { modifiedAst, pathToNodeMap } = applyConstraintHorzVertAlign({
|
||||
selectionRanges,
|
||||
constraint: 'setHorzDistance',
|
||||
})
|
||||
kclManager.updateAst(modifiedAst, true, {
|
||||
// TODO re implement cursor shit
|
||||
// callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||
})
|
||||
kclManager.updateAst(modifiedAst, true)
|
||||
},
|
||||
'Constrain equal length': ({ selectionRanges }) => {
|
||||
const { modifiedAst, pathToNodeMap } = applyConstraintEqualLength({
|
||||
selectionRanges,
|
||||
})
|
||||
kclManager.updateAst(modifiedAst, true, {
|
||||
// TODO re implement cursor shit
|
||||
// callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||
kclManager.updateAst(modifiedAst, true)
|
||||
},
|
||||
'Constrain parallel': ({ selectionRanges }) => {
|
||||
const { modifiedAst, pathToNodeMap } = applyConstraintEqualAngle({
|
||||
selectionRanges,
|
||||
})
|
||||
kclManager.updateAst(modifiedAst, true)
|
||||
},
|
||||
'Constrain remove constraints': ({ selectionRanges }) => {
|
||||
const { modifiedAst, pathToNodeMap } = applyRemoveConstrainingValues({
|
||||
selectionRanges,
|
||||
})
|
||||
kclManager.updateAst(modifiedAst, true)
|
||||
},
|
||||
'AST extrude': ({ selectionRanges }) => {
|
||||
const pathToNode = getNodePathFromSourceRange(
|
||||
|
||||
@ -8,10 +8,12 @@
|
||||
"done.invoke.get-angle-info": { type: "done.invoke.get-angle-info"; data: unknown; __tip: "See the XState TS docs to learn how to strongly type this." };
|
||||
"done.invoke.get-horizontal-info": { type: "done.invoke.get-horizontal-info"; data: unknown; __tip: "See the XState TS docs to learn how to strongly type this." };
|
||||
"done.invoke.get-length-info": { type: "done.invoke.get-length-info"; data: unknown; __tip: "See the XState TS docs to learn how to strongly type this." };
|
||||
"done.invoke.get-perpendicular-distance-info": { type: "done.invoke.get-perpendicular-distance-info"; data: unknown; __tip: "See the XState TS docs to learn how to strongly type this." };
|
||||
"done.invoke.get-vertical-info": { type: "done.invoke.get-vertical-info"; data: unknown; __tip: "See the XState TS docs to learn how to strongly type this." };
|
||||
"error.platform.get-angle-info": { type: "error.platform.get-angle-info"; data: unknown };
|
||||
"error.platform.get-horizontal-info": { type: "error.platform.get-horizontal-info"; data: unknown };
|
||||
"error.platform.get-length-info": { type: "error.platform.get-length-info"; data: unknown };
|
||||
"error.platform.get-perpendicular-distance-info": { type: "error.platform.get-perpendicular-distance-info"; data: unknown };
|
||||
"error.platform.get-vertical-info": { type: "error.platform.get-vertical-info"; data: unknown };
|
||||
"xstate.init": { type: "xstate.init" };
|
||||
"xstate.stop": { type: "xstate.stop" };
|
||||
@ -20,13 +22,14 @@
|
||||
"Get angle info": "done.invoke.get-angle-info";
|
||||
"Get horizontal info": "done.invoke.get-horizontal-info";
|
||||
"Get length info": "done.invoke.get-length-info";
|
||||
"Get perpendicular distance info": "done.invoke.get-perpendicular-distance-info";
|
||||
"Get vertical info": "done.invoke.get-vertical-info";
|
||||
};
|
||||
missingImplementations: {
|
||||
actions: "AST add line segment" | "AST start new sketch" | "Modify AST" | "Set selection" | "Update code selection cursors" | "create path" | "set tool" | "show default planes" | "sketch exit execute" | "toast extrude failed";
|
||||
delays: never;
|
||||
guards: "Selection contains axis" | "Selection contains edge" | "Selection contains face" | "Selection contains line" | "Selection contains point" | "Selection is not empty" | "Selection is one face";
|
||||
services: "Get angle info" | "Get horizontal info" | "Get length info" | "Get vertical info";
|
||||
services: "Get angle info" | "Get horizontal info" | "Get length info" | "Get perpendicular distance info" | "Get vertical info";
|
||||
};
|
||||
eventsCausingActions: {
|
||||
"AST add line segment": "Add point";
|
||||
@ -37,26 +40,28 @@
|
||||
"Clear selection": "Deselect all";
|
||||
"Constrain equal length": "Constrain equal length";
|
||||
"Constrain horizontally align": "Constrain horizontally align";
|
||||
"Constrain parallel": "Constrain parallel";
|
||||
"Constrain remove constraints": "Constrain remove constraints";
|
||||
"Constrain vertically align": "Constrain vertically align";
|
||||
"Make selection horizontal": "Make segment horizontal";
|
||||
"Make selection vertical": "Make segment vertical";
|
||||
"Modify AST": "Complete line";
|
||||
"Remove from code-based selection": "Deselect edge" | "Deselect face" | "Deselect point";
|
||||
"Remove from other selection": "Deselect axis";
|
||||
"Set selection": "Set selection" | "done.invoke.get-angle-info" | "done.invoke.get-horizontal-info" | "done.invoke.get-length-info" | "done.invoke.get-vertical-info";
|
||||
"Set selection": "Set selection" | "done.invoke.get-angle-info" | "done.invoke.get-horizontal-info" | "done.invoke.get-length-info" | "done.invoke.get-perpendicular-distance-info" | "done.invoke.get-vertical-info";
|
||||
"Update code selection cursors": "Complete line" | "Deselect all" | "Deselect axis" | "Deselect edge" | "Deselect face" | "Deselect point" | "Deselect segment" | "Select edge" | "Select face" | "Select point" | "Select segment";
|
||||
"create path": "Select default plane";
|
||||
"default_camera_disable_sketch_mode": "Cancel";
|
||||
"edit mode enter": "Enter sketch";
|
||||
"edit_mode_exit": "Cancel";
|
||||
"equip select": "CancelSketch" | "Constrain equal length" | "Constrain horizontally align" | "Constrain vertically align" | "Deselect point" | "Deselect segment" | "Enter sketch" | "Make segment horizontal" | "Make segment vertical" | "Select default plane" | "Select point" | "Select segment" | "Set selection" | "done.invoke.get-angle-info" | "done.invoke.get-horizontal-info" | "done.invoke.get-length-info" | "done.invoke.get-vertical-info" | "error.platform.get-angle-info" | "error.platform.get-horizontal-info" | "error.platform.get-length-info" | "error.platform.get-vertical-info";
|
||||
"equip select": "CancelSketch" | "Constrain equal length" | "Constrain horizontally align" | "Constrain parallel" | "Constrain remove constraints" | "Constrain vertically align" | "Deselect point" | "Deselect segment" | "Enter sketch" | "Make segment horizontal" | "Make segment vertical" | "Select default plane" | "Select point" | "Select segment" | "Set selection" | "done.invoke.get-angle-info" | "done.invoke.get-horizontal-info" | "done.invoke.get-length-info" | "done.invoke.get-perpendicular-distance-info" | "done.invoke.get-vertical-info" | "error.platform.get-angle-info" | "error.platform.get-horizontal-info" | "error.platform.get-length-info" | "error.platform.get-perpendicular-distance-info" | "error.platform.get-vertical-info";
|
||||
"hide default planes": "Cancel" | "Select default plane" | "xstate.stop";
|
||||
"reset sketch metadata": "Cancel" | "Select default plane";
|
||||
"set default plane id": "Select default plane";
|
||||
"set sketch metadata": "Enter sketch";
|
||||
"set tool": "Equip new tool";
|
||||
"set tool line": "Equip tool";
|
||||
"set tool move": "Equip move tool";
|
||||
"set tool move": "Equip move tool" | "Set selection";
|
||||
"show default planes": "Enter sketch";
|
||||
"sketch exit execute": "Cancel" | "Complete line" | "xstate.stop";
|
||||
"sketch mode enabled": "Enter sketch" | "Select default plane";
|
||||
@ -66,11 +71,14 @@
|
||||
|
||||
};
|
||||
eventsCausingGuards: {
|
||||
"Can constrain angle": "Constrain angle";
|
||||
"Can canstrain parallel": "Constrain parallel";
|
||||
"Can constrain angle": "Constrain angle";
|
||||
"Can constrain equal length": "Constrain equal length";
|
||||
"Can constrain horizontal distance": "Constrain horizontal distance";
|
||||
"Can constrain horizontally align": "Constrain horizontally align";
|
||||
"Can constrain length": "Constrain length";
|
||||
"Can constrain perpendicular distance": "Constrain perpendicular distance";
|
||||
"Can constrain remove constraints": "Constrain remove constraints";
|
||||
"Can constrain vertical distance": "Constrain vertical distance";
|
||||
"Can constrain vertically align": "Constrain vertically align";
|
||||
"Can make selection horizontal": "Make segment horizontal";
|
||||
@ -82,6 +90,8 @@
|
||||
"Selection contains point": "Deselect point";
|
||||
"Selection is not empty": "Deselect all";
|
||||
"Selection is one face": "Enter sketch";
|
||||
"can move": "";
|
||||
"can move with execute": "";
|
||||
"has no selection": "extrude intent";
|
||||
"has valid extrude selection": "" | "extrude intent";
|
||||
"is editing existing sketch": "";
|
||||
@ -90,9 +100,11 @@
|
||||
"Get angle info": "Constrain angle";
|
||||
"Get horizontal info": "Constrain horizontal distance";
|
||||
"Get length info": "Constrain length";
|
||||
"Get perpendicular distance info": "Constrain perpendicular distance";
|
||||
"Get vertical info": "Constrain vertical distance";
|
||||
};
|
||||
matchesStates: "Sketch" | "Sketch no face" | "Sketch.Await angle info" | "Sketch.Await horizontal distance info" | "Sketch.Await length info" | "Sketch.Await vertical distance info" | "Sketch.Line Tool" | "Sketch.Line Tool.Done" | "Sketch.Line Tool.Init" | "Sketch.Line Tool.No Points" | "Sketch.Line Tool.Point Added" | "Sketch.Line Tool.Segment Added" | "Sketch.Move Tool" | "Sketch.SketchIdle" | "awaiting selection" | "checking selection" | "idle" | { "Sketch"?: "Await angle info" | "Await horizontal distance info" | "Await length info" | "Await vertical distance info" | "Line Tool" | "Move Tool" | "SketchIdle" | { "Line Tool"?: "Done" | "Init" | "No Points" | "Point Added" | "Segment Added"; }; };
|
||||
matchesStates: "Sketch" | "Sketch no face" | "Sketch.Await angle info" | "Sketch.Await horizontal distance info" | "Sketch.Await length info" | "Sketch.Await perpendicular distance info" | "Sketch.Await vertical distance info" | "Sketch.Line Tool" | "Sketch.Line Tool.Done" | "Sketch.Line Tool.Init" | "Sketch.Line Tool.No Points" | "Sketch.Line Tool.Point Added" | "Sketch.Line Tool.Segment Added" | "Sketch.Move Tool" | "Sketch.Move Tool.Move init" | "Sketch.Move Tool.Move with execute" | "Sketch.Move Tool.Move without re-execute" | "Sketch.Move Tool.No move" | "Sketch.SketchIdle" | "awaiting selection" | "checking selection" | "idle" | { "Sketch"?: "Await angle info" | "Await horizontal distance info" | "Await length info" | "Await perpendicular distance info" | "Await vertical distance info" | "Line Tool" | "Move Tool" | "SketchIdle" | { "Line Tool"?: "Done" | "Init" | "No Points" | "Point Added" | "Segment Added";
|
||||
"Move Tool"?: "Move init" | "Move with execute" | "Move without re-execute" | "No move"; }; };
|
||||
tags: never;
|
||||
}
|
||||
|
||||
@ -174,7 +174,7 @@ const Home = () => {
|
||||
return (
|
||||
<div className="h-screen overflow-hidden relative flex flex-col">
|
||||
<AppHeader showToolbar={false} />
|
||||
<div className="my-24 overflow-y-auto max-w-5xl w-full mx-auto">
|
||||
<div className="my-24 px-4 lg:px-0 overflow-y-auto max-w-5xl w-full mx-auto">
|
||||
<section className="flex justify-between">
|
||||
<h1 className="text-3xl text-bold">Your Projects</h1>
|
||||
<div className="flex">
|
||||
|
||||
@ -33,7 +33,8 @@ import {
|
||||
import { ONBOARDING_PROJECT_NAME } from './Onboarding'
|
||||
|
||||
export const Settings = () => {
|
||||
const loaderData = useRouteLoaderData(paths.FILE) as IndexLoaderData
|
||||
const loaderData =
|
||||
(useRouteLoaderData(paths.FILE) as IndexLoaderData) || undefined
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
const isFileSettings = location.pathname.includes(paths.FILE)
|
||||
@ -100,7 +101,7 @@ export const Settings = () => {
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-40 overflow-auto body-bg">
|
||||
<AppHeader showToolbar={false} project={loaderData?.project}>
|
||||
<AppHeader showToolbar={false} project={loaderData}>
|
||||
<ActionButton
|
||||
Element="link"
|
||||
to={location.pathname.replace(paths.SETTINGS, '')}
|
||||
|
||||
115
src/useStore.ts
@ -1,16 +1,8 @@
|
||||
import { create } from 'zustand'
|
||||
import { persist } from 'zustand/middleware'
|
||||
import { addLineHighlight, EditorView } from './editor/highlightextension'
|
||||
import {
|
||||
parse,
|
||||
Program,
|
||||
_executor,
|
||||
ProgramMemory,
|
||||
Position,
|
||||
PathToNode,
|
||||
Rotation,
|
||||
SourceRange,
|
||||
} from './lang/wasm'
|
||||
import { parse, Program, _executor, ProgramMemory } from './lang/wasm'
|
||||
import { Selection, Selections, SelectionRangeTypeMap } from 'lib/selections'
|
||||
import { enginelessExecutor } from './lib/testHelpers'
|
||||
import { EditorSelection } from '@codemirror/state'
|
||||
import { EngineCommandManager } from './lang/std/engineConnection'
|
||||
@ -18,25 +10,6 @@ import { KCLError } from './lang/errors'
|
||||
import { kclManager } from 'lang/KclSinglton'
|
||||
import { DefaultPlanes } from './wasm-lib/kcl/bindings/DefaultPlanes'
|
||||
|
||||
export type Axis = 'y-axis' | 'x-axis' | 'z-axis'
|
||||
|
||||
export type Selection = {
|
||||
type:
|
||||
| 'default'
|
||||
| 'line-end'
|
||||
| 'line-mid'
|
||||
| 'face'
|
||||
| 'point'
|
||||
| 'edge'
|
||||
| 'line'
|
||||
| 'arc'
|
||||
| 'all'
|
||||
range: SourceRange
|
||||
}
|
||||
export type Selections = {
|
||||
otherSelections: Axis[]
|
||||
codeBasedSelections: Selection[]
|
||||
}
|
||||
export type ToolTip =
|
||||
| 'lineTo'
|
||||
| 'line'
|
||||
@ -77,10 +50,6 @@ export type PaneType =
|
||||
| 'logs'
|
||||
| 'lspMessages'
|
||||
|
||||
export interface SelectionRangeTypeMap {
|
||||
[key: number]: Selection['type']
|
||||
}
|
||||
|
||||
export interface StoreState {
|
||||
editorView: EditorView | null
|
||||
setEditorView: (editorView: EditorView) => void
|
||||
@ -257,7 +226,7 @@ export async function executeCode({
|
||||
body: [],
|
||||
nonCodeMeta: {
|
||||
nonCodeNodes: {},
|
||||
start: null,
|
||||
start: [],
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -316,7 +285,7 @@ export async function executeAst({
|
||||
defaultPlanes
|
||||
))
|
||||
|
||||
await engineCommandManager.waitForAllCommands(ast, programMemory)
|
||||
await engineCommandManager.waitForAllCommands()
|
||||
return {
|
||||
logs: [],
|
||||
errors: [],
|
||||
@ -345,79 +314,3 @@ export async function executeAst({
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function dispatchCodeMirrorCursor({
|
||||
selections,
|
||||
editorView,
|
||||
}: {
|
||||
selections: Selections
|
||||
editorView: EditorView
|
||||
}): {
|
||||
selectionRangeTypeMap: SelectionRangeTypeMap
|
||||
} {
|
||||
const ranges: ReturnType<typeof EditorSelection.cursor>[] = []
|
||||
const selectionRangeTypeMap: SelectionRangeTypeMap = {}
|
||||
selections.codeBasedSelections.forEach(({ range, type }) => {
|
||||
if (range?.[1]) {
|
||||
ranges.push(EditorSelection.cursor(range[1]))
|
||||
selectionRangeTypeMap[range[1]] = type
|
||||
}
|
||||
})
|
||||
setTimeout(() => {
|
||||
ranges.length &&
|
||||
editorView.dispatch({
|
||||
selection: EditorSelection.create(
|
||||
ranges,
|
||||
selections.codeBasedSelections.length - 1
|
||||
),
|
||||
})
|
||||
})
|
||||
return {
|
||||
selectionRangeTypeMap,
|
||||
}
|
||||
}
|
||||
|
||||
export function setCodeMirrorCursor({
|
||||
codeSelection,
|
||||
currestSelections,
|
||||
editorView,
|
||||
isShiftDown,
|
||||
}: {
|
||||
codeSelection?: Selection
|
||||
currestSelections: Selections
|
||||
editorView: EditorView
|
||||
isShiftDown: boolean
|
||||
}): SelectionRangeTypeMap {
|
||||
// This DOES NOT set the `selectionRanges` in xstate context
|
||||
// instead it updates/dispatches to the editor, which in turn updates the xstate context
|
||||
// I've found this the best way to deal with the editor without causing an infinite loop
|
||||
// and really we want the editor to be in charge of cursor positions and for `selectionRanges` mirror it
|
||||
// because we want to respect the user manually placing the cursor too.
|
||||
const code = kclManager.code
|
||||
if (!codeSelection) {
|
||||
const selectionRangeTypeMap = dispatchCodeMirrorCursor({
|
||||
editorView,
|
||||
selections: {
|
||||
otherSelections: currestSelections.otherSelections,
|
||||
codeBasedSelections: [
|
||||
{
|
||||
range: [0, code.length ? code.length - 1 : 0],
|
||||
type: 'default',
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
return selectionRangeTypeMap
|
||||
}
|
||||
const selections: Selections = {
|
||||
...currestSelections,
|
||||
codeBasedSelections: isShiftDown
|
||||
? [...currestSelections.codeBasedSelections, codeSelection]
|
||||
: [codeSelection],
|
||||
}
|
||||
const selectionRangeTypeMap = dispatchCodeMirrorCursor({
|
||||
editorView,
|
||||
selections,
|
||||
})
|
||||
return selectionRangeTypeMap
|
||||
}
|
||||
|
||||
24
src/wasm-lib/Cargo.lock
generated
@ -1390,7 +1390,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kcl-lib"
|
||||
version = "0.1.33"
|
||||
version = "0.1.35"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@ -1426,9 +1426,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kittycad"
|
||||
version = "0.2.31"
|
||||
version = "0.2.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "539b323537b877fc8dd130362b8f1af9af8051c19208bb8bfd816ab7c330f2bb"
|
||||
checksum = "d341a81a4dfef43460d395c87d86c17e24affb96db0e7f4a35e8688f0e092344"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@ -1733,7 +1733,7 @@ checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575"
|
||||
[[package]]
|
||||
name = "openapitor"
|
||||
version = "0.0.9"
|
||||
source = "git+https://github.com/KittyCAD/kittycad.rs?branch=main#c122a9b1d6afe51c25e545b5e0bbeb91d367e6d2"
|
||||
source = "git+https://github.com/KittyCAD/kittycad.rs?branch=main#7e087ecaee2fdfdbdbe8648e769213130f777c45"
|
||||
dependencies = [
|
||||
"Inflector",
|
||||
"anyhow",
|
||||
@ -2047,9 +2047,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.67"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328"
|
||||
checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
@ -2549,9 +2549,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.188"
|
||||
version = "1.0.189"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e"
|
||||
checksum = "8e422a44e74ad4001bdc8eede9a4570ab52f71190e9c076d14369f38b9200537"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
@ -2567,9 +2567,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.188"
|
||||
version = "1.0.189"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2"
|
||||
checksum = "1e48d1f918009ce3145511378cf68d613e3b3d9137d67272562080d68a2b32d5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -3077,9 +3077,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.32.0"
|
||||
version = "1.33.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9"
|
||||
checksum = "4f38200e3ef7995e5ef13baec2f432a6da0aa9ac495b2c0e8f3b7eec2c92d653"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"bytes",
|
||||
|
||||
@ -11,7 +11,7 @@ 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.31", default-features = false, features = ["js"] }
|
||||
kittycad = { version = "0.2.33", default-features = false, features = ["js"] }
|
||||
serde_json = "1.0.107"
|
||||
uuid = { version = "1.4.1", features = ["v4", "js", "serde"] }
|
||||
wasm-bindgen = "0.2.87"
|
||||
@ -20,10 +20,10 @@ wasm-bindgen-futures = "0.4.37"
|
||||
[dev-dependencies]
|
||||
anyhow = "1"
|
||||
image = "0.24.7"
|
||||
kittycad = "0.2.31"
|
||||
kittycad = "0.2.33"
|
||||
pretty_assertions = "1.4.0"
|
||||
reqwest = { version = "0.11.22", default-features = false }
|
||||
tokio = { version = "1.32.0", features = ["rt-multi-thread", "macros", "time"] }
|
||||
tokio = { version = "1.33.0", features = ["rt-multi-thread", "macros", "time"] }
|
||||
twenty-twenty = "0.6.1"
|
||||
uuid = { version = "1.4.1", features = ["v4", "js", "serde"] }
|
||||
|
||||
|
||||
@ -14,7 +14,7 @@ proc-macro = true
|
||||
convert_case = "0.6.0"
|
||||
proc-macro2 = "1"
|
||||
quote = "1"
|
||||
serde = { version = "1.0.188", features = ["derive"] }
|
||||
serde = { version = "1.0.189", features = ["derive"] }
|
||||
serde_tokenstream = "0.2"
|
||||
syn = { version = "2.0.38", features = ["full"] }
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "kcl-lib"
|
||||
description = "KittyCAD Language"
|
||||
version = "0.1.33"
|
||||
version = "0.1.35"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
|
||||
@ -15,11 +15,11 @@ clap = { version = "4.4.6", features = ["cargo", "derive", "env", "unicode"], op
|
||||
dashmap = "5.5.3"
|
||||
derive-docs = { version = "0.1.4" }
|
||||
#derive-docs = { path = "../derive-docs" }
|
||||
kittycad = { version = "0.2.31", default-features = false, features = ["js"] }
|
||||
kittycad = { version = "0.2.33", default-features = false, features = ["js"] }
|
||||
lazy_static = "1.4.0"
|
||||
parse-display = "0.8.2"
|
||||
schemars = { version = "0.8", features = ["impl_json_schema", "url", "uuid1"] }
|
||||
serde = { version = "1.0.188", features = ["derive"] }
|
||||
serde = { version = "1.0.189", features = ["derive"] }
|
||||
serde_json = "1.0.107"
|
||||
thiserror = "1.0.49"
|
||||
ts-rs = { version = "7", package = "ts-rs-json-value", features = ["serde-json-impl", "schemars-impl", "uuid-impl"] }
|
||||
@ -37,7 +37,7 @@ web-sys = { version = "0.3.64", features = ["console"] }
|
||||
bson = { version = "2.7.0", features = ["uuid-1", "chrono"] }
|
||||
futures = { version = "0.3.28" }
|
||||
reqwest = { version = "0.11.22", default-features = false }
|
||||
tokio = { version = "1.32.0", features = ["full"] }
|
||||
tokio = { version = "1.33.0", features = ["full"] }
|
||||
tokio-tungstenite = { version = "0.20.0", features = ["rustls-tls-native-roots"] }
|
||||
tower-lsp = { version = "0.20.0", features = ["proposed"] }
|
||||
|
||||
@ -55,7 +55,7 @@ criterion = "0.5.1"
|
||||
expectorate = "1.1.0"
|
||||
itertools = "0.11.0"
|
||||
pretty_assertions = "1.4.0"
|
||||
tokio = { version = "1.32.0", features = ["rt-multi-thread", "macros", "time"] }
|
||||
tokio = { version = "1.33.0", features = ["rt-multi-thread", "macros", "time"] }
|
||||
|
||||
[[bench]]
|
||||
name = "compiler_benchmark"
|
||||
|
||||
@ -6,36 +6,30 @@ pub fn bench_lex(c: &mut Criterion) {
|
||||
c.bench_function("lex_pipes_on_pipes", |b| b.iter(|| lex(PIPES_PROGRAM)));
|
||||
}
|
||||
|
||||
pub fn bench_lex_parse(c: &mut Criterion) {
|
||||
c.bench_function("parse_lex_cube", |b| b.iter(|| lex_and_parse(CUBE_PROGRAM)));
|
||||
c.bench_function("parse_lex_big_kitt", |b| b.iter(|| lex_and_parse(KITT_PROGRAM)));
|
||||
c.bench_function("parse_lex_pipes_on_pipes", |b| b.iter(|| lex_and_parse(PIPES_PROGRAM)));
|
||||
pub fn bench_parse(c: &mut Criterion) {
|
||||
for (name, file) in [
|
||||
("pipes_on_pipes", PIPES_PROGRAM),
|
||||
("big_kitt", KITT_PROGRAM),
|
||||
("cube", CUBE_PROGRAM),
|
||||
] {
|
||||
let tokens = kcl_lib::token::lexer(file);
|
||||
c.bench_function(&format!("parse_{name}"), move |b| {
|
||||
let tok = tokens.clone();
|
||||
b.iter(move || {
|
||||
let parser = kcl_lib::parser::Parser::new(tok.clone());
|
||||
black_box(parser.ast().unwrap());
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn lex(program: &str) {
|
||||
black_box(kcl_lib::token::lexer(program));
|
||||
}
|
||||
|
||||
fn lex_and_parse(program: &str) {
|
||||
let tokens = kcl_lib::token::lexer(program);
|
||||
let parser = kcl_lib::parser::Parser::new(tokens);
|
||||
black_box(parser.ast().unwrap());
|
||||
}
|
||||
|
||||
criterion_group!(benches, bench_lex, bench_lex_parse);
|
||||
criterion_group!(benches, bench_lex, bench_parse);
|
||||
criterion_main!(benches);
|
||||
|
||||
const KITT_PROGRAM: &str = include_str!("../../tests/executor/inputs/kittycad_svg.kcl");
|
||||
const PIPES_PROGRAM: &str = include_str!("../../tests/executor/inputs/pipes_on_pipes.kcl");
|
||||
const CUBE_PROGRAM: &str = r#"fn cube = (pos, scale) => {
|
||||
const sg = startSketchAt(pos)
|
||||
|> line([0, scale], %)
|
||||
|> line([scale, 0], %)
|
||||
|> line([0, -scale], %)
|
||||
|
||||
return sg
|
||||
}
|
||||
|
||||
const b1 = cube([0,0], 10)
|
||||
const pt1 = b1[0]
|
||||
show(b1)"#;
|
||||
const CUBE_PROGRAM: &str = include_str!("../../tests/executor/inputs/cube.kcl");
|
||||
|
||||
@ -63,10 +63,14 @@ impl Program {
|
||||
.fold(String::new(), |mut output, (index, recast_str)| {
|
||||
let start_string = if index == 0 {
|
||||
// We need to indent.
|
||||
if let Some(start) = self.non_code_meta.start.clone() {
|
||||
start.format(&indentation)
|
||||
} else {
|
||||
if self.non_code_meta.start.is_empty() {
|
||||
indentation.to_string()
|
||||
} else {
|
||||
self.non_code_meta
|
||||
.start
|
||||
.iter()
|
||||
.map(|start| start.format(&indentation))
|
||||
.collect()
|
||||
}
|
||||
} else {
|
||||
// Do nothing, we already applied the indentation elsewhere.
|
||||
@ -82,7 +86,10 @@ impl Program {
|
||||
};
|
||||
|
||||
let custom_white_space_or_comment = match self.non_code_meta.non_code_nodes.get(&index) {
|
||||
Some(custom_white_space_or_comment) => custom_white_space_or_comment.format(&indentation),
|
||||
Some(noncodes) => noncodes
|
||||
.iter()
|
||||
.map(|custom_white_space_or_comment| custom_white_space_or_comment.format(&indentation))
|
||||
.collect::<String>(),
|
||||
None => String::new(),
|
||||
};
|
||||
let end_string = if custom_white_space_or_comment.is_empty() {
|
||||
@ -707,30 +714,35 @@ pub struct NonCodeNode {
|
||||
impl NonCodeNode {
|
||||
pub fn value(&self) -> String {
|
||||
match &self.value {
|
||||
NonCodeValue::InlineComment { value } => value.clone(),
|
||||
NonCodeValue::BlockComment { value } => value.clone(),
|
||||
NonCodeValue::NewLineBlockComment { value } => value.clone(),
|
||||
NonCodeValue::InlineComment { value, style: _ } => value.clone(),
|
||||
NonCodeValue::BlockComment { value, style: _ } => value.clone(),
|
||||
NonCodeValue::NewLineBlockComment { value, style: _ } => value.clone(),
|
||||
NonCodeValue::NewLine => "\n\n".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn format(&self, indentation: &str) -> String {
|
||||
match &self.value {
|
||||
NonCodeValue::InlineComment { value } => format!(" // {}\n", value),
|
||||
NonCodeValue::BlockComment { value } => {
|
||||
NonCodeValue::InlineComment {
|
||||
value,
|
||||
style: CommentStyle::Line,
|
||||
} => format!(" // {}\n", value),
|
||||
NonCodeValue::InlineComment {
|
||||
value,
|
||||
style: CommentStyle::Block,
|
||||
} => format!(" /* {} */", value),
|
||||
NonCodeValue::BlockComment { value, style } => {
|
||||
let add_start_new_line = if self.start == 0 { "" } else { "\n" };
|
||||
if value.contains('\n') {
|
||||
format!("{}{}/* {} */\n", add_start_new_line, indentation, value)
|
||||
} else {
|
||||
format!("{}{}// {}\n", add_start_new_line, indentation, value)
|
||||
match style {
|
||||
CommentStyle::Block => format!("{}{}/* {} */", add_start_new_line, indentation, value),
|
||||
CommentStyle::Line => format!("{}{}// {}\n", add_start_new_line, indentation, value),
|
||||
}
|
||||
}
|
||||
NonCodeValue::NewLineBlockComment { value } => {
|
||||
NonCodeValue::NewLineBlockComment { value, style } => {
|
||||
let add_start_new_line = if self.start == 0 { "" } else { "\n\n" };
|
||||
if value.contains('\n') {
|
||||
format!("{}{}/* {} */\n", add_start_new_line, indentation, value)
|
||||
} else {
|
||||
format!("{}{}// {}\n", add_start_new_line, indentation, value)
|
||||
match style {
|
||||
CommentStyle::Block => format!("{}{}/* {} */\n", add_start_new_line, indentation, value),
|
||||
CommentStyle::Line => format!("{}{}// {}\n", add_start_new_line, indentation, value),
|
||||
}
|
||||
}
|
||||
NonCodeValue::NewLine => "\n\n".to_string(),
|
||||
@ -738,14 +750,27 @@ impl NonCodeNode {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum CommentStyle {
|
||||
/// Like // foo
|
||||
Line,
|
||||
/// Like /* foo */
|
||||
Block,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
pub enum NonCodeValue {
|
||||
/// An inline comment.
|
||||
/// An example of this is the following: `1 + 1 // This is an inline comment`.
|
||||
/// Here are examples:
|
||||
/// `1 + 1 // This is an inline comment`.
|
||||
/// `1 + 1 /* Here's another */`.
|
||||
InlineComment {
|
||||
value: String,
|
||||
style: CommentStyle,
|
||||
},
|
||||
/// A block comment.
|
||||
/// An example of this is the following:
|
||||
@ -759,11 +784,13 @@ pub enum NonCodeValue {
|
||||
/// If it did it would be a `NewLineBlockComment`.
|
||||
BlockComment {
|
||||
value: String,
|
||||
style: CommentStyle,
|
||||
},
|
||||
/// A block comment that has a new line above it.
|
||||
/// The user explicitly added a new line above the block comment.
|
||||
NewLineBlockComment {
|
||||
value: String,
|
||||
style: CommentStyle,
|
||||
},
|
||||
// A new line like `\n\n` NOT a new line like `\n`.
|
||||
// This is also not a comment.
|
||||
@ -774,8 +801,8 @@ pub enum NonCodeValue {
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NonCodeMeta {
|
||||
pub non_code_nodes: HashMap<usize, NonCodeNode>,
|
||||
pub start: Option<NonCodeNode>,
|
||||
pub non_code_nodes: HashMap<usize, Vec<NonCodeNode>>,
|
||||
pub start: Vec<NonCodeNode>,
|
||||
}
|
||||
|
||||
// implement Deserialize manually because we to force the keys of non_code_nodes to be usize
|
||||
@ -788,15 +815,16 @@ impl<'de> Deserialize<'de> for NonCodeMeta {
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct NonCodeMetaHelper {
|
||||
non_code_nodes: HashMap<String, NonCodeNode>,
|
||||
start: Option<NonCodeNode>,
|
||||
non_code_nodes: HashMap<String, Vec<NonCodeNode>>,
|
||||
start: Vec<NonCodeNode>,
|
||||
}
|
||||
|
||||
let helper = NonCodeMetaHelper::deserialize(deserializer)?;
|
||||
let mut non_code_nodes = HashMap::new();
|
||||
for (key, value) in helper.non_code_nodes {
|
||||
non_code_nodes.insert(key.parse().map_err(serde::de::Error::custom)?, value);
|
||||
}
|
||||
let non_code_nodes = helper
|
||||
.non_code_nodes
|
||||
.into_iter()
|
||||
.map(|(key, value)| Ok((key.parse().map_err(serde::de::Error::custom)?, value)))
|
||||
.collect::<Result<HashMap<_, _>, _>>()?;
|
||||
Ok(NonCodeMeta {
|
||||
non_code_nodes,
|
||||
start: helper.start,
|
||||
@ -804,6 +832,12 @@ impl<'de> Deserialize<'de> for NonCodeMeta {
|
||||
}
|
||||
}
|
||||
|
||||
impl NonCodeMeta {
|
||||
pub fn insert(&mut self, i: usize, new: NonCodeNode) {
|
||||
self.non_code_nodes.entry(i).or_default().push(new);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(tag = "type")]
|
||||
@ -2385,7 +2419,9 @@ impl PipeExpression {
|
||||
let mut s = statement.recast(options, indentation_level + 1, true);
|
||||
let non_code_meta = self.non_code_meta.clone();
|
||||
if let Some(non_code_meta_value) = non_code_meta.non_code_nodes.get(&index) {
|
||||
s += non_code_meta_value.format(&indentation).trim_end_matches('\n')
|
||||
for val in non_code_meta_value {
|
||||
s += val.format(&indentation).trim_end_matches('\n')
|
||||
}
|
||||
}
|
||||
|
||||
if index != self.body.len() - 1 {
|
||||
@ -2869,13 +2905,32 @@ show(part001)"#;
|
||||
recasted,
|
||||
r#"fn myFn = () => {
|
||||
// this is a comment
|
||||
const yo = { a: { b: { c: '123' } } }
|
||||
/* block
|
||||
const yo = { a: { b: { c: '123' } } } /* block
|
||||
comment */
|
||||
|
||||
const key = 'c'
|
||||
// this is also a comment
|
||||
return things
|
||||
}
|
||||
"#
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn test_recast_comment_at_start() {
|
||||
let test_program = r#"
|
||||
/* comment at start */
|
||||
|
||||
const mySk1 = startSketchAt([0, 0])"#;
|
||||
let tokens = crate::token::lexer(test_program);
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let program = parser.ast().unwrap();
|
||||
|
||||
let recasted = program.recast(&Default::default(), 0);
|
||||
assert_eq!(
|
||||
recasted,
|
||||
r#"/* comment at start */
|
||||
|
||||
const mySk1 = startSketchAt([0, 0])
|
||||
"#
|
||||
);
|
||||
}
|
||||
@ -2913,14 +2968,13 @@ const mySk1 = startSketchOn('XY')
|
||||
|> lineTo({ to: [0, 1], tag: 'myTag' }, %)
|
||||
|> lineTo([1, 1], %)
|
||||
/* and
|
||||
here
|
||||
|
||||
a comment between pipe expression statements */
|
||||
here */
|
||||
// a comment between pipe expression statements
|
||||
|> rx(90, %)
|
||||
// and another with just white space between others below
|
||||
|> ry(45, %)
|
||||
|> rx(45, %)
|
||||
// one more for good measure
|
||||
// one more for good measure
|
||||
"#
|
||||
);
|
||||
}
|
||||
@ -2988,16 +3042,19 @@ const things = "things"
|
||||
let program = parser.ast().unwrap();
|
||||
|
||||
let recasted = program.recast(&Default::default(), 0);
|
||||
assert_eq!(recasted.trim(), some_program_string.trim());
|
||||
let expected = some_program_string.trim();
|
||||
// Currently new parser removes an empty line
|
||||
let actual = recasted.trim();
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_recast_comment_tokens_inside_strings() {
|
||||
let some_program_string = r#"let b = {
|
||||
"end": 141,
|
||||
"start": 125,
|
||||
"type": "NonCodeNode",
|
||||
"value": "
|
||||
end: 141,
|
||||
start: 125,
|
||||
type: "NonCodeNode",
|
||||
value: "
|
||||
// a comment
|
||||
"
|
||||
}"#;
|
||||
|
||||
@ -316,7 +316,17 @@ pub fn get_type_string_from_schema(schema: &schemars::schema::Schema) -> Result<
|
||||
if let Some(array_val) = &o.array {
|
||||
if let Some(schemars::schema::SingleOrVec::Single(items)) = &array_val.items {
|
||||
// Let's print out the object's properties.
|
||||
return Ok((format!("[{}]", get_type_string_from_schema(items)?.0), false));
|
||||
match array_val.max_items {
|
||||
Some(val) => {
|
||||
return Ok((
|
||||
format!("[{}]", (0..val).map(|_| "number").collect::<Vec<_>>().join(", ")),
|
||||
false,
|
||||
));
|
||||
}
|
||||
None => {
|
||||
return Ok((format!("[{}]", get_type_string_from_schema(items)?.0), false));
|
||||
}
|
||||
};
|
||||
} else if let Some(items) = &array_val.contains {
|
||||
return Ok((format!("[{}]", get_type_string_from_schema(items)?.0), false));
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity};
|
||||
|
||||
use crate::executor::SourceRange;
|
||||
|
||||
#[derive(Error, Debug, Serialize, Deserialize, ts_rs::TS)]
|
||||
#[derive(Error, Debug, Serialize, Deserialize, ts_rs::TS, Clone)]
|
||||
#[ts(export)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||
pub enum KclError {
|
||||
@ -28,7 +28,7 @@ pub enum KclError {
|
||||
Engine(KclErrorDetails),
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, ts_rs::TS)]
|
||||
#[derive(Debug, Serialize, Deserialize, ts_rs::TS, Clone)]
|
||||
#[ts(export)]
|
||||
pub struct KclErrorDetails {
|
||||
#[serde(rename = "sourceRanges")]
|
||||
@ -78,6 +78,22 @@ impl KclError {
|
||||
KclError::Engine(e) => e.source_ranges.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the inner error message.
|
||||
pub fn message(&self) -> &str {
|
||||
match &self {
|
||||
KclError::Syntax(e) => &e.message,
|
||||
KclError::Semantic(e) => &e.message,
|
||||
KclError::Type(e) => &e.message,
|
||||
KclError::Unimplemented(e) => &e.message,
|
||||
KclError::Unexpected(e) => &e.message,
|
||||
KclError::ValueAlreadyDefined(e) => &e.message,
|
||||
KclError::UndefinedValue(e) => &e.message,
|
||||
KclError::InvalidExpression(e) => &e.message,
|
||||
KclError::Engine(e) => &e.message,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_lsp_diagnostic(&self, code: &str) -> Diagnostic {
|
||||
let (message, _, _) = self.get_message_line_column(code);
|
||||
let source_ranges = self.source_ranges();
|
||||
|
||||
@ -2,7 +2,7 @@ use std::{collections::HashMap, str::FromStr};
|
||||
|
||||
use crate::{
|
||||
ast::types::{
|
||||
ArrayExpression, BinaryExpression, BinaryPart, BodyItem, CallExpression, ExpressionStatement,
|
||||
ArrayExpression, BinaryExpression, BinaryPart, BodyItem, CallExpression, CommentStyle, ExpressionStatement,
|
||||
FunctionExpression, Identifier, Literal, LiteralIdentifier, MemberExpression, MemberObject, NonCodeMeta,
|
||||
NonCodeNode, NonCodeValue, ObjectExpression, ObjectKeyInfo, ObjectProperty, PipeExpression, PipeSubstitution,
|
||||
Program, ReturnStatement, UnaryExpression, UnaryOperator, Value, VariableDeclaration, VariableDeclarator,
|
||||
@ -13,6 +13,8 @@ use crate::{
|
||||
token::{Token, TokenType},
|
||||
};
|
||||
|
||||
mod parser_impl;
|
||||
|
||||
pub const PIPE_SUBSTITUTION_OPERATOR: &str = "%";
|
||||
pub const PIPE_OPERATOR: &str = "|>";
|
||||
|
||||
@ -179,13 +181,19 @@ impl Parser {
|
||||
Ok(token)
|
||||
}
|
||||
|
||||
/// Use the new Winnow parser.
|
||||
pub fn ast(&self) -> Result<Program, KclError> {
|
||||
parser_impl::run_parser(&mut self.tokens.as_slice())
|
||||
}
|
||||
|
||||
/// Use the old handwritten recursive parser.
|
||||
pub fn ast_old(&self) -> Result<Program, KclError> {
|
||||
let body = self.make_body(
|
||||
0,
|
||||
vec![],
|
||||
NonCodeMeta {
|
||||
non_code_nodes: HashMap::new(),
|
||||
start: None,
|
||||
start: Vec::new(),
|
||||
},
|
||||
)?;
|
||||
let end = match self.get_token(body.last_index) {
|
||||
@ -209,7 +217,7 @@ impl Parser {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn make_literal(&self, index: usize) -> Result<Literal, KclError> {
|
||||
fn make_literal(&self, index: usize) -> Result<Literal, KclError> {
|
||||
let token = self.get_token(index)?;
|
||||
let value = if token.token_type == TokenType::Number {
|
||||
if let Ok(value) = token.value.parse::<i64>() {
|
||||
@ -295,6 +303,11 @@ impl Parser {
|
||||
));
|
||||
}
|
||||
|
||||
let is_block_style = non_code_tokens
|
||||
.first()
|
||||
.map(|tok| matches!(tok.token_type, TokenType::BlockComment))
|
||||
.unwrap_or_default();
|
||||
|
||||
let full_string = non_code_tokens
|
||||
.iter()
|
||||
.map(|t| {
|
||||
@ -336,11 +349,32 @@ 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.
|
||||
NonCodeValue::NewLineBlockComment { value: full_string }
|
||||
NonCodeValue::NewLineBlockComment {
|
||||
value: full_string,
|
||||
style: if is_block_style {
|
||||
CommentStyle::Block
|
||||
} else {
|
||||
CommentStyle::Line
|
||||
},
|
||||
}
|
||||
} else if is_new_line_comment {
|
||||
NonCodeValue::BlockComment { value: full_string }
|
||||
NonCodeValue::BlockComment {
|
||||
value: full_string,
|
||||
style: if is_block_style {
|
||||
CommentStyle::Block
|
||||
} else {
|
||||
CommentStyle::Line
|
||||
},
|
||||
}
|
||||
} else {
|
||||
NonCodeValue::InlineComment { value: full_string }
|
||||
NonCodeValue::InlineComment {
|
||||
value: full_string,
|
||||
style: if is_block_style {
|
||||
CommentStyle::Block
|
||||
} else {
|
||||
CommentStyle::Line
|
||||
},
|
||||
}
|
||||
},
|
||||
};
|
||||
Ok((Some(node), end_index - 1))
|
||||
@ -1033,7 +1067,7 @@ impl Parser {
|
||||
let non_code_meta = match previous_non_code_meta {
|
||||
Some(meta) => meta,
|
||||
None => NonCodeMeta {
|
||||
start: None,
|
||||
start: Vec::new(),
|
||||
non_code_nodes: HashMap::new(),
|
||||
},
|
||||
};
|
||||
@ -1064,7 +1098,7 @@ impl Parser {
|
||||
let mut _non_code_meta: NonCodeMeta;
|
||||
if let Some(node) = next_pipe.non_code_node {
|
||||
_non_code_meta = non_code_meta;
|
||||
_non_code_meta.non_code_nodes.insert(previous_values.len(), node);
|
||||
_non_code_meta.insert(previous_values.len(), node);
|
||||
} else {
|
||||
_non_code_meta = non_code_meta;
|
||||
}
|
||||
@ -1435,7 +1469,7 @@ impl Parser {
|
||||
self.make_params(next_brace_or_comma_token.index, _previous_params)
|
||||
}
|
||||
|
||||
pub fn make_unary_expression(&self, index: usize) -> Result<UnaryExpressionResult, KclError> {
|
||||
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() {
|
||||
@ -1631,9 +1665,11 @@ impl Parser {
|
||||
let next_token = self.next_meaningful_token(token_index, Some(0))?;
|
||||
if let Some(node) = &next_token.non_code_node {
|
||||
if previous_body.is_empty() {
|
||||
non_code_meta.start = next_token.non_code_node;
|
||||
if let Some(next) = next_token.non_code_node {
|
||||
non_code_meta.start.push(next);
|
||||
}
|
||||
} else {
|
||||
non_code_meta.non_code_nodes.insert(previous_body.len(), node.clone());
|
||||
non_code_meta.insert(previous_body.len(), node.clone());
|
||||
}
|
||||
}
|
||||
return self.make_body(next_token.index, previous_body, non_code_meta);
|
||||
@ -1641,14 +1677,14 @@ impl Parser {
|
||||
|
||||
let next = self.next_meaningful_token(token_index, None)?;
|
||||
if let Some(node) = &next.non_code_node {
|
||||
non_code_meta.non_code_nodes.insert(previous_body.len(), node.clone());
|
||||
non_code_meta.insert(previous_body.len(), node.clone());
|
||||
}
|
||||
|
||||
if token.token_type == TokenType::Keyword && VariableKind::from_str(&token.value).is_ok() {
|
||||
let declaration = self.make_variable_declaration(token_index)?;
|
||||
let next_thing = self.next_meaningful_token(declaration.last_index, None)?;
|
||||
if let Some(node) = &next_thing.non_code_node {
|
||||
non_code_meta.non_code_nodes.insert(previous_body.len(), node.clone());
|
||||
non_code_meta.insert(previous_body.len(), node.clone());
|
||||
}
|
||||
let mut _previous_body = previous_body;
|
||||
_previous_body.push(BodyItem::VariableDeclaration(VariableDeclaration {
|
||||
@ -1669,7 +1705,7 @@ impl Parser {
|
||||
let statement = self.make_return_statement(token_index)?;
|
||||
let next_thing = self.next_meaningful_token(statement.last_index, None)?;
|
||||
if let Some(node) = &next_thing.non_code_node {
|
||||
non_code_meta.non_code_nodes.insert(previous_body.len(), node.clone());
|
||||
non_code_meta.insert(previous_body.len(), node.clone());
|
||||
}
|
||||
let mut _previous_body = previous_body;
|
||||
_previous_body.push(BodyItem::ReturnStatement(ReturnStatement {
|
||||
@ -1693,7 +1729,7 @@ impl Parser {
|
||||
let expression = self.make_expression_statement(token_index)?;
|
||||
let next_thing = self.next_meaningful_token(expression.last_index, None)?;
|
||||
if let Some(node) = &next_thing.non_code_node {
|
||||
non_code_meta.non_code_nodes.insert(previous_body.len(), node.clone());
|
||||
non_code_meta.insert(previous_body.len(), node.clone());
|
||||
}
|
||||
let mut _previous_body = previous_body;
|
||||
_previous_body.push(BodyItem::ExpressionStatement(ExpressionStatement {
|
||||
@ -1716,7 +1752,7 @@ impl Parser {
|
||||
&& next_thing_token.token_type == TokenType::Operator
|
||||
{
|
||||
if let Some(node) = &next_thing.non_code_node {
|
||||
non_code_meta.non_code_nodes.insert(previous_body.len(), node.clone());
|
||||
non_code_meta.insert(previous_body.len(), node.clone());
|
||||
}
|
||||
let expression = self.make_expression_statement(token_index)?;
|
||||
let mut _previous_body = previous_body;
|
||||
@ -1749,7 +1785,7 @@ impl Parser {
|
||||
last_index: next_token_index,
|
||||
non_code_meta: NonCodeMeta {
|
||||
non_code_nodes: HashMap::new(),
|
||||
start: None,
|
||||
start: Vec::new(),
|
||||
},
|
||||
}
|
||||
} else {
|
||||
@ -1758,7 +1794,7 @@ impl Parser {
|
||||
vec![],
|
||||
NonCodeMeta {
|
||||
non_code_nodes: HashMap::new(),
|
||||
start: None,
|
||||
start: Vec::new(),
|
||||
},
|
||||
)?
|
||||
};
|
||||
@ -1913,6 +1949,7 @@ const key = 'c'"#,
|
||||
end: 60,
|
||||
value: NonCodeValue::BlockComment {
|
||||
value: "this is a comment".to_string(),
|
||||
style: CommentStyle::Line,
|
||||
},
|
||||
}),
|
||||
31,
|
||||
@ -1966,6 +2003,35 @@ const key = 'c'"#,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_math_parse() {
|
||||
let tokens = crate::token::lexer(r#"5 + "a""#);
|
||||
let actual = Parser::new(tokens).ast().unwrap().body;
|
||||
let expr = BinaryExpression {
|
||||
start: 0,
|
||||
end: 7,
|
||||
operator: BinaryOperator::Add,
|
||||
left: BinaryPart::Literal(Box::new(Literal {
|
||||
start: 0,
|
||||
end: 1,
|
||||
value: serde_json::Value::Number(serde_json::Number::from(5)),
|
||||
raw: "5".to_owned(),
|
||||
})),
|
||||
right: BinaryPart::Literal(Box::new(Literal {
|
||||
start: 4,
|
||||
end: 7,
|
||||
value: serde_json::Value::String("a".to_owned()),
|
||||
raw: r#""a""#.to_owned(),
|
||||
})),
|
||||
};
|
||||
let expected = vec![BodyItem::ExpressionStatement(ExpressionStatement {
|
||||
start: 0,
|
||||
end: 7,
|
||||
expression: Value::BinaryExpression(Box::new(expr)),
|
||||
})];
|
||||
assert_eq!(expected, actual);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_code_token() {
|
||||
let tokens = [
|
||||
@ -2600,7 +2666,7 @@ show(mySk1)"#;
|
||||
vec![],
|
||||
NonCodeMeta {
|
||||
non_code_nodes: HashMap::new(),
|
||||
start: None,
|
||||
start: Vec::new(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
@ -2636,10 +2702,7 @@ show(mySk1)"#;
|
||||
})),
|
||||
})),
|
||||
})],
|
||||
non_code_meta: NonCodeMeta {
|
||||
non_code_nodes: Default::default(),
|
||||
start: None,
|
||||
},
|
||||
non_code_meta: NonCodeMeta::default(),
|
||||
};
|
||||
|
||||
assert_eq!(result, expected_result);
|
||||
@ -2812,10 +2875,6 @@ z(-[["#,
|
||||
let parser = Parser::new(tokens);
|
||||
let result = parser.ast();
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.err().unwrap().to_string(),
|
||||
r#"syntax: KclErrorDetails { source_ranges: [SourceRange([1, 2])], message: "missing a closing brace for the function call" }"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -2831,7 +2890,7 @@ z(-[["#,
|
||||
// https://github.com/KittyCAD/modeling-app/issues/696
|
||||
assert_eq!(
|
||||
result.err().unwrap().to_string(),
|
||||
r#"semantic: KclErrorDetails { source_ranges: [], message: "file is empty" }"#
|
||||
r#"syntax: KclErrorDetails { source_ranges: [], message: "file is empty" }"#
|
||||
);
|
||||
}
|
||||
|
||||
@ -2845,7 +2904,7 @@ z(-[["#,
|
||||
// https://github.com/KittyCAD/modeling-app/issues/696
|
||||
assert_eq!(
|
||||
result.err().unwrap().to_string(),
|
||||
r#"semantic: KclErrorDetails { source_ranges: [], message: "file is empty" }"#
|
||||
r#"syntax: KclErrorDetails { source_ranges: [], message: "file is empty" }"#
|
||||
);
|
||||
}
|
||||
|
||||
@ -2863,7 +2922,7 @@ e
|
||||
.err()
|
||||
.unwrap()
|
||||
.to_string()
|
||||
.contains("expected to be started on a identifier or literal"));
|
||||
.contains("expected whitespace, found ')' which is brace"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -2872,7 +2931,11 @@ e
|
||||
let parser = Parser::new(tokens);
|
||||
let result = parser.ast();
|
||||
assert!(result.is_err());
|
||||
assert!(result.err().unwrap().to_string().contains("expected another token"));
|
||||
assert!(result
|
||||
.err()
|
||||
.unwrap()
|
||||
.to_string()
|
||||
.contains("expected whitespace, found ')' which is brace"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -2884,11 +2947,7 @@ e
|
||||
let parser = Parser::new(tokens);
|
||||
let result = parser.ast();
|
||||
assert!(result.is_err());
|
||||
assert!(result
|
||||
.err()
|
||||
.unwrap()
|
||||
.to_string()
|
||||
.contains("unexpected end of expression"));
|
||||
assert!(result.err().unwrap().to_string().contains("Unexpected token"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -2985,10 +3044,7 @@ e
|
||||
}],
|
||||
kind: VariableKind::Const,
|
||||
})],
|
||||
non_code_meta: NonCodeMeta {
|
||||
non_code_nodes: Default::default(),
|
||||
start: None,
|
||||
},
|
||||
non_code_meta: NonCodeMeta::default(),
|
||||
};
|
||||
|
||||
assert_eq!(result, expected_result);
|
||||
@ -3022,7 +3078,9 @@ e
|
||||
|
||||
#[test]
|
||||
fn test_error_stdlib_in_fn_name() {
|
||||
let some_program_string = r#"fn cos = () {}"#;
|
||||
let some_program_string = r#"fn cos = () => {
|
||||
return 1
|
||||
}"#;
|
||||
let tokens = crate::token::lexer(some_program_string);
|
||||
let parser = Parser::new(tokens);
|
||||
let result = parser.ast();
|
||||
@ -3123,9 +3181,12 @@ thing(false)
|
||||
let parser = Parser::new(tokens);
|
||||
let result = parser.ast();
|
||||
assert!(result.is_err());
|
||||
// TODO: https://github.com/KittyCAD/modeling-app/issues/784
|
||||
// Improve this error message.
|
||||
// It should say that the compiler is expecting a function expression on the RHS.
|
||||
assert_eq!(
|
||||
result.err().unwrap().to_string(),
|
||||
r#"syntax: KclErrorDetails { source_ranges: [SourceRange([0, 2])], message: "Expected a `let` variable kind, found: `fn`" }"#
|
||||
r#"syntax: KclErrorDetails { source_ranges: [SourceRange([11, 18])], message: "Unexpected token" }"#
|
||||
);
|
||||
}
|
||||
|
||||
@ -3163,15 +3224,6 @@ let other_thing = 2 * cos(3)"#;
|
||||
parser.ast().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_pipes_on_pipes() {
|
||||
let code = include_str!("../../tests/executor/inputs/pipes_on_pipes.kcl");
|
||||
|
||||
let tokens = crate::token::lexer(code);
|
||||
let parser = Parser::new(tokens);
|
||||
parser.ast().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_negative_arguments() {
|
||||
let some_program_string = r#"fn box = (p, h, l, w) => {
|
||||
|
||||
2068
src/wasm-lib/kcl/src/parser/parser_impl.rs
Normal file
107
src/wasm-lib/kcl/src/parser/parser_impl/error.rs
Normal file
@ -0,0 +1,107 @@
|
||||
use winnow::error::{ErrorKind, ParseError, StrContext};
|
||||
|
||||
use crate::{
|
||||
errors::{KclError, KclErrorDetails},
|
||||
token::Token,
|
||||
};
|
||||
|
||||
/// Accumulate context while backtracking errors
|
||||
/// Very similar to [`winnow::error::ContextError`] type,
|
||||
/// but the 'cause' field is always a [`KclError`],
|
||||
/// instead of a dynamic [`std::error::Error`] trait object.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ContextError<C = StrContext> {
|
||||
pub context: Vec<C>,
|
||||
pub cause: Option<KclError>,
|
||||
}
|
||||
|
||||
impl From<ParseError<&[Token], ContextError>> for KclError {
|
||||
fn from(err: ParseError<&[Token], ContextError>) -> Self {
|
||||
let Some(last_token) = err.input().last() else {
|
||||
return KclError::Syntax(KclErrorDetails {
|
||||
source_ranges: Default::default(),
|
||||
message: "file is empty".to_owned(),
|
||||
});
|
||||
};
|
||||
|
||||
let (input, offset, err) = (err.input().to_vec(), err.offset(), err.into_inner());
|
||||
|
||||
if let Some(e) = err.cause {
|
||||
return e;
|
||||
}
|
||||
|
||||
// See docs on `offset`.
|
||||
if offset >= input.len() {
|
||||
let context = err.context.first();
|
||||
return KclError::Syntax(KclErrorDetails {
|
||||
source_ranges: last_token.as_source_ranges(),
|
||||
message: match context {
|
||||
Some(what) => format!("Unexpected end of file. The compiler {what}"),
|
||||
None => "Unexpected end of file while still parsing".to_owned(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
let bad_token = &input[offset];
|
||||
// TODO: Add the Winnow parser context to the error.
|
||||
// See https://github.com/KittyCAD/modeling-app/issues/784
|
||||
KclError::Syntax(KclErrorDetails {
|
||||
source_ranges: bad_token.as_source_ranges(),
|
||||
message: "Unexpected token".to_owned(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> From<KclError> for ContextError<C> {
|
||||
fn from(e: KclError) -> Self {
|
||||
Self {
|
||||
context: Default::default(),
|
||||
cause: Some(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> std::default::Default for ContextError<C> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
context: Default::default(),
|
||||
cause: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<I, C> winnow::error::ParserError<I> for ContextError<C> {
|
||||
#[inline]
|
||||
fn from_error_kind(_input: &I, _kind: ErrorKind) -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn append(self, _input: &I, _kind: ErrorKind) -> Self {
|
||||
self
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn or(self, other: Self) -> Self {
|
||||
other
|
||||
}
|
||||
}
|
||||
|
||||
impl<C, I> winnow::error::AddContext<I, C> for ContextError<C> {
|
||||
#[inline]
|
||||
fn add_context(mut self, _input: &I, ctx: C) -> Self {
|
||||
self.context.push(ctx);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<C, I> winnow::error::FromExternalError<I, KclError> for ContextError<C> {
|
||||
#[inline]
|
||||
fn from_external_error(_input: &I, _kind: ErrorKind, e: KclError) -> Self {
|
||||
let mut err = Self::default();
|
||||
{
|
||||
err.cause = Some(e);
|
||||
}
|
||||
err
|
||||
}
|
||||
}
|
||||
@ -25,13 +25,26 @@ pub async fn extrude(args: Args) -> Result<MemoryItem, KclError> {
|
||||
}]
|
||||
async fn inner_extrude(length: f64, sketch_group: Box<SketchGroup>, args: Args) -> Result<Box<ExtrudeGroup>, KclError> {
|
||||
let id = uuid::Uuid::new_v4();
|
||||
// Extrude the element.
|
||||
args.send_modeling_cmd(
|
||||
id,
|
||||
kittycad::types::ModelingCmd::Extrude {
|
||||
target: sketch_group.id,
|
||||
distance: length,
|
||||
cap: true,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
let cmd = kittycad::types::ModelingCmd::Extrude {
|
||||
target: sketch_group.id,
|
||||
distance: length,
|
||||
cap: true,
|
||||
};
|
||||
args.send_modeling_cmd(id, cmd).await?;
|
||||
// Bring the object to the front of the scene.
|
||||
// See: https://github.com/KittyCAD/modeling-app/issues/806
|
||||
args.send_modeling_cmd(
|
||||
uuid::Uuid::new_v4(),
|
||||
kittycad::types::ModelingCmd::ObjectBringToFront {
|
||||
object_id: sketch_group.id,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(Box::new(ExtrudeGroup {
|
||||
id,
|
||||
|
||||
@ -63,9 +63,10 @@ impl StdLib {
|
||||
Box::new(crate::std::sketch::StartProfileAt),
|
||||
Box::new(crate::std::sketch::Close),
|
||||
Box::new(crate::std::sketch::Arc),
|
||||
Box::new(crate::std::sketch::TangentalArc),
|
||||
Box::new(crate::std::sketch::TangentalArcTo),
|
||||
Box::new(crate::std::sketch::TangentialArc),
|
||||
Box::new(crate::std::sketch::TangentialArcTo),
|
||||
Box::new(crate::std::sketch::BezierCurve),
|
||||
Box::new(crate::std::sketch::Hole),
|
||||
Box::new(crate::std::math::Cos),
|
||||
Box::new(crate::std::math::Sin),
|
||||
Box::new(crate::std::math::Tan),
|
||||
@ -230,6 +231,42 @@ impl Args {
|
||||
Ok((segment_name, sketch_group))
|
||||
}
|
||||
|
||||
fn get_sketch_groups(&self) -> Result<(Box<SketchGroup>, Box<SketchGroup>), KclError> {
|
||||
let first_value = self.args.first().ok_or_else(|| {
|
||||
KclError::Type(KclErrorDetails {
|
||||
message: format!("Expected a SketchGroup as the first argument, found `{:?}`", self.args),
|
||||
source_ranges: vec![self.source_range],
|
||||
})
|
||||
})?;
|
||||
|
||||
let sketch_group = if let MemoryItem::SketchGroup(sg) = first_value {
|
||||
sg.clone()
|
||||
} else {
|
||||
return Err(KclError::Type(KclErrorDetails {
|
||||
message: format!("Expected a SketchGroup as the first argument, found `{:?}`", self.args),
|
||||
source_ranges: vec![self.source_range],
|
||||
}));
|
||||
};
|
||||
|
||||
let second_value = self.args.get(1).ok_or_else(|| {
|
||||
KclError::Type(KclErrorDetails {
|
||||
message: format!("Expected a SketchGroup as the second argument, found `{:?}`", self.args),
|
||||
source_ranges: vec![self.source_range],
|
||||
})
|
||||
})?;
|
||||
|
||||
let second_sketch_group = if let MemoryItem::SketchGroup(sg) = second_value {
|
||||
sg.clone()
|
||||
} else {
|
||||
return Err(KclError::Type(KclErrorDetails {
|
||||
message: format!("Expected a SketchGroup as the second argument, found `{:?}`", self.args),
|
||||
source_ranges: vec![self.source_range],
|
||||
}));
|
||||
};
|
||||
|
||||
Ok((sketch_group, second_sketch_group))
|
||||
}
|
||||
|
||||
fn get_sketch_group(&self) -> Result<Box<SketchGroup>, KclError> {
|
||||
let first_value = self.args.first().ok_or_else(|| {
|
||||
KclError::Type(KclErrorDetails {
|
||||
|
||||
@ -1080,11 +1080,11 @@ async fn inner_arc(data: ArcData, sketch_group: Box<SketchGroup>, args: Args) ->
|
||||
Ok(new_sketch_group)
|
||||
}
|
||||
|
||||
/// Data to draw a tangental arc.
|
||||
/// Data to draw a tangential arc.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, ts_rs::TS)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase", untagged)]
|
||||
pub enum TangentalArcData {
|
||||
pub enum TangentialArcData {
|
||||
RadiusAndOffset {
|
||||
/// Radius of the arc.
|
||||
/// Not to be confused with Raiders of the Lost Ark.
|
||||
@ -1103,20 +1103,20 @@ pub enum TangentalArcData {
|
||||
Point([f64; 2]),
|
||||
}
|
||||
|
||||
/// Draw a tangental arc.
|
||||
pub async fn tangental_arc(args: Args) -> Result<MemoryItem, KclError> {
|
||||
let (data, sketch_group): (TangentalArcData, Box<SketchGroup>) = args.get_data_and_sketch_group()?;
|
||||
/// Draw a tangential arc.
|
||||
pub async fn tangential_arc(args: Args) -> Result<MemoryItem, KclError> {
|
||||
let (data, sketch_group): (TangentialArcData, Box<SketchGroup>) = args.get_data_and_sketch_group()?;
|
||||
|
||||
let new_sketch_group = inner_tangental_arc(data, sketch_group, args).await?;
|
||||
let new_sketch_group = inner_tangential_arc(data, sketch_group, args).await?;
|
||||
Ok(MemoryItem::SketchGroup(new_sketch_group))
|
||||
}
|
||||
|
||||
/// Draw an arc.
|
||||
#[stdlib {
|
||||
name = "tangentalArc",
|
||||
name = "tangentialArc",
|
||||
}]
|
||||
async fn inner_tangental_arc(
|
||||
data: TangentalArcData,
|
||||
async fn inner_tangential_arc(
|
||||
data: TangentialArcData,
|
||||
sketch_group: Box<SketchGroup>,
|
||||
args: Args,
|
||||
) -> Result<Box<SketchGroup>, KclError> {
|
||||
@ -1125,7 +1125,7 @@ async fn inner_tangental_arc(
|
||||
let id = uuid::Uuid::new_v4();
|
||||
|
||||
let to = match &data {
|
||||
TangentalArcData::RadiusAndOffset { radius, offset } => {
|
||||
TangentialArcData::RadiusAndOffset { radius, offset } => {
|
||||
// Calculate the end point from the angle and radius.
|
||||
let end_angle = Angle::from_degrees(*offset);
|
||||
let start_angle = Angle::from_degrees(0.0);
|
||||
@ -1147,7 +1147,7 @@ async fn inner_tangental_arc(
|
||||
.await?;
|
||||
to.into()
|
||||
}
|
||||
TangentalArcData::PointWithTag { to, .. } => {
|
||||
TangentialArcData::PointWithTag { to, .. } => {
|
||||
args.send_modeling_cmd(
|
||||
id,
|
||||
ModelingCmd::ExtendPath {
|
||||
@ -1166,7 +1166,7 @@ async fn inner_tangental_arc(
|
||||
|
||||
*to
|
||||
}
|
||||
TangentalArcData::Point(to) => {
|
||||
TangentialArcData::Point(to) => {
|
||||
args.send_modeling_cmd(
|
||||
id,
|
||||
ModelingCmd::ExtendPath {
|
||||
@ -1207,11 +1207,11 @@ async fn inner_tangental_arc(
|
||||
Ok(new_sketch_group)
|
||||
}
|
||||
|
||||
/// Data to draw a tangental arc to a specific point.
|
||||
/// Data to draw a tangential arc to a specific point.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, ts_rs::TS)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase", untagged)]
|
||||
pub enum TangentalArcToData {
|
||||
pub enum TangentialArcToData {
|
||||
/// A point with a tag.
|
||||
PointWithTag {
|
||||
/// Where the arc should end. Must lie in the same plane as the current path pen position. Must not be colinear with current path pen position.
|
||||
@ -1223,27 +1223,27 @@ pub enum TangentalArcToData {
|
||||
Point([f64; 2]),
|
||||
}
|
||||
|
||||
/// Draw a tangental arc to a specific point.
|
||||
pub async fn tangental_arc_to(args: Args) -> Result<MemoryItem, KclError> {
|
||||
let (data, sketch_group): (TangentalArcToData, Box<SketchGroup>) = args.get_data_and_sketch_group()?;
|
||||
/// Draw a tangential arc to a specific point.
|
||||
pub async fn tangential_arc_to(args: Args) -> Result<MemoryItem, KclError> {
|
||||
let (data, sketch_group): (TangentialArcToData, Box<SketchGroup>) = args.get_data_and_sketch_group()?;
|
||||
|
||||
let new_sketch_group = inner_tangental_arc_to(data, sketch_group, args).await?;
|
||||
let new_sketch_group = inner_tangential_arc_to(data, sketch_group, args).await?;
|
||||
Ok(MemoryItem::SketchGroup(new_sketch_group))
|
||||
}
|
||||
|
||||
/// Draw an arc.
|
||||
#[stdlib {
|
||||
name = "tangentalArcTo",
|
||||
name = "tangentialArcTo",
|
||||
}]
|
||||
async fn inner_tangental_arc_to(
|
||||
data: TangentalArcToData,
|
||||
async fn inner_tangential_arc_to(
|
||||
data: TangentialArcToData,
|
||||
sketch_group: Box<SketchGroup>,
|
||||
args: Args,
|
||||
) -> Result<Box<SketchGroup>, KclError> {
|
||||
let from: Point2d = sketch_group.get_coords_from_paths()?;
|
||||
let to = match &data {
|
||||
TangentalArcToData::PointWithTag { to, .. } => to,
|
||||
TangentalArcToData::Point(to) => to,
|
||||
TangentialArcToData::PointWithTag { to, .. } => to,
|
||||
TangentialArcToData::Point(to) => to,
|
||||
};
|
||||
|
||||
let delta = [to[0] - from.x, to[1] - from.y];
|
||||
@ -1270,7 +1270,7 @@ async fn inner_tangental_arc_to(
|
||||
base: BasePath {
|
||||
from: from.into(),
|
||||
to: *to,
|
||||
name: if let TangentalArcToData::PointWithTag { tag, .. } = data {
|
||||
name: if let TangentialArcToData::PointWithTag { tag, .. } = data {
|
||||
tag.to_string()
|
||||
} else {
|
||||
"".to_string()
|
||||
@ -1395,6 +1395,50 @@ async fn inner_bezier_curve(
|
||||
Ok(new_sketch_group)
|
||||
}
|
||||
|
||||
/// Use a sketch to cut a hole in another sketch.
|
||||
pub async fn hole(args: Args) -> Result<MemoryItem, KclError> {
|
||||
let (hole_sketch_group, sketch_group): (Box<SketchGroup>, Box<SketchGroup>) = args.get_sketch_groups()?;
|
||||
|
||||
let new_sketch_group = inner_hole(hole_sketch_group, sketch_group, args).await?;
|
||||
Ok(MemoryItem::SketchGroup(new_sketch_group))
|
||||
}
|
||||
|
||||
/// Use a sketch to cut a hole in another sketch.
|
||||
#[stdlib {
|
||||
name = "hole",
|
||||
}]
|
||||
async fn inner_hole(
|
||||
hole_sketch_group: Box<SketchGroup>,
|
||||
sketch_group: Box<SketchGroup>,
|
||||
args: Args,
|
||||
) -> Result<Box<SketchGroup>, KclError> {
|
||||
//TODO: batch these (once we have batch)
|
||||
|
||||
args.send_modeling_cmd(
|
||||
uuid::Uuid::new_v4(),
|
||||
ModelingCmd::Solid2DAddHole {
|
||||
object_id: sketch_group.id,
|
||||
hole_id: hole_sketch_group.id,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
//suggestion (mike)
|
||||
//we also hide the source hole since its essentially "consumed" by this operation
|
||||
args.send_modeling_cmd(
|
||||
uuid::Uuid::new_v4(),
|
||||
ModelingCmd::ObjectVisible {
|
||||
object_id: hole_sketch_group.id,
|
||||
hidden: true,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
// TODO: should we modify the sketch group to include the hole data, probably?
|
||||
|
||||
Ok(sketch_group)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
|
||||
@ -6,6 +6,8 @@ use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tower_lsp::lsp_types::SemanticTokenType;
|
||||
|
||||
use crate::{ast::types::VariableKind, executor::SourceRange};
|
||||
|
||||
mod tokeniser;
|
||||
|
||||
/// The types of tokens.
|
||||
@ -142,15 +144,39 @@ impl Token {
|
||||
TokenType::Whitespace | TokenType::LineComment | TokenType::BlockComment
|
||||
)
|
||||
}
|
||||
|
||||
pub fn as_source_range(&self) -> SourceRange {
|
||||
SourceRange([self.start, self.end])
|
||||
}
|
||||
|
||||
pub fn as_source_ranges(&self) -> Vec<SourceRange> {
|
||||
vec![self.as_source_range()]
|
||||
}
|
||||
|
||||
/// Is this token the beginning of a variable/function declaration?
|
||||
/// If so, what kind?
|
||||
/// If not, returns None.
|
||||
pub fn declaration_keyword(&self) -> Option<VariableKind> {
|
||||
if !matches!(self.token_type, TokenType::Keyword) {
|
||||
return None;
|
||||
}
|
||||
Some(match self.value.as_str() {
|
||||
"var" => VariableKind::Var,
|
||||
"let" => VariableKind::Let,
|
||||
"fn" => VariableKind::Fn,
|
||||
"const" => VariableKind::Const,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Token> for crate::executor::SourceRange {
|
||||
impl From<Token> for SourceRange {
|
||||
fn from(token: Token) -> Self {
|
||||
Self([token.start, token.end])
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Token> for crate::executor::SourceRange {
|
||||
impl From<&Token> for SourceRange {
|
||||
fn from(token: &Token) -> Self {
|
||||
Self([token.start, token.end])
|
||||
}
|
||||
|
||||
12
src/wasm-lib/tests/executor/inputs/cube.kcl
Normal file
@ -0,0 +1,12 @@
|
||||
fn cube = (pos, scale) => {
|
||||
const sg = startSketchAt(pos)
|
||||
|> line([0, scale], %)
|
||||
|> line([scale, 0], %)
|
||||
|> line([0, -scale], %)
|
||||
|
||||
return sg
|
||||
}
|
||||
|
||||
const b1 = cube([0,0], 10)
|
||||
const pt1 = b1[0]
|
||||
show(b1)
|
||||
@ -87,7 +87,7 @@ 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);
|
||||
twenty_twenty::assert_image("tests/executor/outputs/function_sketch.png", &result, 0.999);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
@ -107,7 +107,11 @@ async fn serial_test_execute_with_function_sketch_with_position() {
|
||||
show(box([0,0], 3, 6, 10))"#;
|
||||
|
||||
let result = execute_and_snapshot(code).await.unwrap();
|
||||
twenty_twenty::assert_image("tests/executor/outputs/function_sketch_with_position.png", &result, 1.0);
|
||||
twenty_twenty::assert_image(
|
||||
"tests/executor/outputs/function_sketch_with_position.png",
|
||||
&result,
|
||||
0.999,
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
@ -125,7 +129,7 @@ async fn serial_test_execute_with_angled_line() {
|
||||
show(part001)"#;
|
||||
|
||||
let result = execute_and_snapshot(code).await.unwrap();
|
||||
twenty_twenty::assert_image("tests/executor/outputs/angled_line.png", &result, 1.0);
|
||||
twenty_twenty::assert_image("tests/executor/outputs/angled_line.png", &result, 0.999);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
@ -152,7 +156,7 @@ const bracket = startSketchOn('XY')
|
||||
show(bracket)"#;
|
||||
|
||||
let result = execute_and_snapshot(code).await.unwrap();
|
||||
twenty_twenty::assert_image("tests/executor/outputs/parametric.png", &result, 1.0);
|
||||
twenty_twenty::assert_image("tests/executor/outputs/parametric.png", &result, 0.999);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
@ -169,14 +173,14 @@ const wallMountL = 8
|
||||
|
||||
const bracket = startSketchAt([0, 0])
|
||||
|> line([0, wallMountL], %)
|
||||
|> tangentalArc({
|
||||
|> tangentialArc({
|
||||
radius: filletR,
|
||||
offset: 90
|
||||
}, %)
|
||||
|> line([-shelfMountL, 0], %)
|
||||
|> line([0, -thickness], %)
|
||||
|> line([shelfMountL, 0], %)
|
||||
|> tangentalArc({
|
||||
|> tangentialArc({
|
||||
radius: filletR - thickness,
|
||||
offset: -90
|
||||
}, %)
|
||||
@ -187,7 +191,7 @@ const bracket = startSketchAt([0, 0])
|
||||
show(bracket)"#;
|
||||
|
||||
let result = execute_and_snapshot(code).await.unwrap();
|
||||
twenty_twenty::assert_image("tests/executor/outputs/parametric_with_tan_arc.png", &result, 1.0);
|
||||
twenty_twenty::assert_image("tests/executor/outputs/parametric_with_tan_arc.png", &result, 0.999);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
@ -215,7 +219,7 @@ async fn serial_test_execute_pipes_on_pipes() {
|
||||
let code = include_str!("inputs/pipes_on_pipes.kcl");
|
||||
|
||||
let result = execute_and_snapshot(code).await.unwrap();
|
||||
twenty_twenty::assert_image("tests/executor/outputs/pipes_on_pipes.png", &result, 1.0);
|
||||
twenty_twenty::assert_image("tests/executor/outputs/pipes_on_pipes.png", &result, 0.999);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
@ -223,11 +227,11 @@ async fn serial_test_execute_kittycad_svg() {
|
||||
let code = include_str!("inputs/kittycad_svg.kcl");
|
||||
|
||||
let result = execute_and_snapshot(code).await.unwrap();
|
||||
twenty_twenty::assert_image("tests/executor/outputs/kittycad_svg.png", &result, 1.0);
|
||||
twenty_twenty::assert_image("tests/executor/outputs/kittycad_svg.png", &result, 0.999);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_member_expression_sketch_group() {
|
||||
async fn serial_test_member_expression_sketch_group() {
|
||||
let code = r#"fn cube = (pos, scale) => {
|
||||
const sg = startSketchOn('XY')
|
||||
|> startProfileAt(pos, %)
|
||||
@ -256,7 +260,7 @@ show(b2)"#;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_close_arc() {
|
||||
async fn serial_test_close_arc() {
|
||||
let code = r#"const center = [0,0]
|
||||
const radius = 40
|
||||
const height = 3
|
||||
@ -270,11 +274,11 @@ const body = startSketchOn('XY')
|
||||
show(body)"#;
|
||||
|
||||
let result = execute_and_snapshot(code).await.unwrap();
|
||||
twenty_twenty::assert_image("tests/executor/outputs/close_arc.png", &result, 1.0);
|
||||
twenty_twenty::assert_image("tests/executor/outputs/close_arc.png", &result, 0.999);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_negative_args() {
|
||||
async fn serial_test_negative_args() {
|
||||
let code = r#"const width = 5
|
||||
const height = 10
|
||||
const length = 12
|
||||
@ -296,50 +300,50 @@ let thing = box(-12, -15, 10)
|
||||
box(-20, -5, 10)"#;
|
||||
|
||||
let result = execute_and_snapshot(code).await.unwrap();
|
||||
twenty_twenty::assert_image("tests/executor/outputs/negative_args.png", &result, 1.0);
|
||||
twenty_twenty::assert_image("tests/executor/outputs/negative_args.png", &result, 0.999);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_basic_tangental_arc() {
|
||||
async fn serial_test_basic_tangential_arc() {
|
||||
let code = r#"const boxSketch = startSketchAt([0, 0])
|
||||
|> line([0, 10], %)
|
||||
|> tangentalArc({radius: 5, offset: 90}, %)
|
||||
|> tangentialArc({radius: 5, offset: 90}, %)
|
||||
|> line([5, -15], %)
|
||||
|> extrude(10, %)
|
||||
"#;
|
||||
|
||||
let result = execute_and_snapshot(code).await.unwrap();
|
||||
twenty_twenty::assert_image("tests/executor/outputs/tangental_arc.png", &result, 1.0);
|
||||
twenty_twenty::assert_image("tests/executor/outputs/tangential_arc.png", &result, 0.999);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_basic_tangental_arc_with_point() {
|
||||
async fn serial_test_basic_tangential_arc_with_point() {
|
||||
let code = r#"const boxSketch = startSketchAt([0, 0])
|
||||
|> line([0, 10], %)
|
||||
|> tangentalArc([-5, 5], %)
|
||||
|> tangentialArc([-5, 5], %)
|
||||
|> line([5, -15], %)
|
||||
|> extrude(10, %)
|
||||
"#;
|
||||
|
||||
let result = execute_and_snapshot(code).await.unwrap();
|
||||
twenty_twenty::assert_image("tests/executor/outputs/tangental_arc_with_point.png", &result, 1.0);
|
||||
twenty_twenty::assert_image("tests/executor/outputs/tangential_arc_with_point.png", &result, 0.999);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_basic_tangental_arc_to() {
|
||||
async fn serial_test_basic_tangential_arc_to() {
|
||||
let code = r#"const boxSketch = startSketchAt([0, 0])
|
||||
|> line([0, 10], %)
|
||||
|> tangentalArcTo([-5, 15], %)
|
||||
|> tangentialArcTo([-5, 15], %)
|
||||
|> line([5, -15], %)
|
||||
|> extrude(10, %)
|
||||
"#;
|
||||
|
||||
let result = execute_and_snapshot(code).await.unwrap();
|
||||
twenty_twenty::assert_image("tests/executor/outputs/tangental_arc_to.png", &result, 1.0);
|
||||
twenty_twenty::assert_image("tests/executor/outputs/tangential_arc_to.png", &result, 0.999);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_different_planes_same_drawing() {
|
||||
async fn serial_test_different_planes_same_drawing() {
|
||||
let code = r#"const width = 5
|
||||
const height = 10
|
||||
const length = 12
|
||||
@ -362,11 +366,15 @@ let thing = box(-12, -15, 10, 'yz')
|
||||
box(-20, -5, 10, 'xy')"#;
|
||||
|
||||
let result = execute_and_snapshot(code).await.unwrap();
|
||||
twenty_twenty::assert_image("tests/executor/outputs/different_planes_same_drawing.png", &result, 1.0);
|
||||
twenty_twenty::assert_image(
|
||||
"tests/executor/outputs/different_planes_same_drawing.png",
|
||||
&result,
|
||||
0.999,
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_lots_of_planes() {
|
||||
async fn serial_test_lots_of_planes() {
|
||||
let code = r#"const sigmaAllow = 15000 // psi
|
||||
const width = 11 // inch
|
||||
const p = 150 // Force on shelf - lbs
|
||||
@ -380,11 +388,11 @@ const wallMountL = 8
|
||||
const bracket = startSketchOn('XY')
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> line([0, wallMountL], %)
|
||||
|> tangentalArc({ radius: filletR, offset: 90 }, %)
|
||||
|> tangentialArc({ radius: filletR, offset: 90 }, %)
|
||||
|> line([-shelfMountL, 0], %)
|
||||
|> line([0, -thickness], %)
|
||||
|> line([shelfMountL, 0], %)
|
||||
|> tangentalArc({
|
||||
|> tangentialArc({
|
||||
radius: filletR - thickness,
|
||||
offset: -90
|
||||
}, %)
|
||||
@ -421,5 +429,78 @@ const part004 = startSketchOn('YZ')
|
||||
"#;
|
||||
|
||||
let result = execute_and_snapshot(code).await.unwrap();
|
||||
twenty_twenty::assert_image("tests/executor/outputs/lots_of_planes.png", &result, 1.0);
|
||||
twenty_twenty::assert_image("tests/executor/outputs/lots_of_planes.png", &result, 0.999);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn serial_test_holes() {
|
||||
let code = r#"fn circle = (pos, radius) => {
|
||||
const sg = startSketchOn('XY')
|
||||
|> startProfileAt(pos, %)
|
||||
|> arc({angle_end: 360, angle_start: 0, radius: radius}, %)
|
||||
|> close(%)
|
||||
|
||||
return sg
|
||||
}
|
||||
|
||||
const square = startSketchOn('XY')
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> line([0, 10], %)
|
||||
|> line([10, 0], %)
|
||||
|> line([0, -10], %)
|
||||
|> close(%)
|
||||
|> hole(circle([2, 2], .5), %)
|
||||
|> hole(circle([2, 8], .5), %)
|
||||
|> extrude(2, %)
|
||||
|
||||
show(square)
|
||||
"#;
|
||||
|
||||
let result = execute_and_snapshot(code).await.unwrap();
|
||||
twenty_twenty::assert_image("tests/executor/outputs/holes.png", &result, 0.999);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn serial_test_rounded_with_holes() {
|
||||
let code = r#"fn circle = (pos, radius) => {
|
||||
const sg = startSketchOn('XY')
|
||||
|> startProfileAt([pos[0] + radius, pos[1]], %)
|
||||
|> arc({
|
||||
angle_end: 360,
|
||||
angle_start: 0,
|
||||
radius: radius
|
||||
}, %)
|
||||
|> close(%)
|
||||
return sg
|
||||
}
|
||||
|
||||
fn roundedRectangle = (pos, w, l, cornerRadius) => {
|
||||
const rr = startSketchOn('XY')
|
||||
|> startProfileAt([pos[0] - w/2, 0], %)
|
||||
|> lineTo([pos[0] - w/2, pos[1] - l/2 + cornerRadius], %)
|
||||
|> tangentialArcTo([pos[0] - w/2 + cornerRadius, pos[1] - l/2], %)
|
||||
|> lineTo([pos[0] + w/2 - cornerRadius, pos[1] - l/2], %)
|
||||
|> tangentialArcTo([pos[0] + w/2, pos[1] - l/2 + cornerRadius], %)
|
||||
|> lineTo([pos[0] + w/2, pos[1] + l/2 - cornerRadius], %)
|
||||
|> tangentialArcTo([pos[0] + w/2 - cornerRadius, pos[1] + l/2], %)
|
||||
|> lineTo([pos[0] - w/2 + cornerRadius, pos[1] + l/2], %)
|
||||
|> tangentialArcTo([pos[0] - w/2, pos[1] + l/2 - cornerRadius], %)
|
||||
|> close(%)
|
||||
return rr
|
||||
}
|
||||
|
||||
const holeRadius = 1
|
||||
const holeIndex = 6
|
||||
|
||||
const part = roundedRectangle([0, 0], 20, 20, 4)
|
||||
|> hole(circle([-holeIndex, holeIndex], holeRadius), %)
|
||||
|> hole(circle([holeIndex, holeIndex], holeRadius), %)
|
||||
|> hole(circle([-holeIndex, -holeIndex], holeRadius), %)
|
||||
|> hole(circle([holeIndex, -holeIndex], holeRadius), %)
|
||||
|> extrude(2, %)
|
||||
|
||||
show(part)"#;
|
||||
|
||||
let result = execute_and_snapshot(code).await.unwrap();
|
||||
twenty_twenty::assert_image("tests/executor/outputs/rounded_with_holes.png", &result, 0.999);
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 70 KiB |
BIN
src/wasm-lib/tests/executor/outputs/holes.png
Normal file
|
After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 71 KiB |
BIN
src/wasm-lib/tests/executor/outputs/rounded_with_holes.png
Normal file
|
After Width: | Height: | Size: 77 KiB |