Compare commits

..

3 Commits

175 changed files with 6463 additions and 7708 deletions

View File

@ -1,3 +1,3 @@
[codespell]
ignore-words-list: crate,everytime
skip: **/target,node_modules,build,**/Cargo.lock
skip: **/target,node_modules,build

View File

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

View File

@ -1,6 +1,6 @@
VITE_KC_API_WS_MODELING_URL=wss://api.zoo.dev/ws/modeling/commands
VITE_KC_API_BASE_URL=https://api.zoo.dev
VITE_KC_SITE_BASE_URL=https://zoo.dev
VITE_KC_API_WS_MODELING_URL=wss://api.kittycad.io/ws/modeling/commands
VITE_KC_API_BASE_URL=https://api.kittycad.io
VITE_KC_SITE_BASE_URL=https://kittycad.io
VITE_KC_SKIP_AUTH=false
VITE_KC_CONNECTION_TIMEOUT_MS=15000
VITE_KC_SENTRY_DSN=https://a814f2f66734989a90367f48feee28ca@o1042111.ingest.sentry.io/4505789425844224

View File

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

View File

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

View File

@ -104,7 +104,7 @@ jobs:
if: github.event_name == 'schedule'
run: |
VERSION=$(date +'%-y.%-m.%-d') yarn bump-jsons
echo "$(jq --arg url 'https://dl.zoo.dev/releases/modeling-app/nightly/last_update.json' \
echo "$(jq --arg url 'https://dl.kittycad.io/releases/modeling-app/nightly/last_update.json' \
'.tauri.updater.endpoints[]=$url' src-tauri/tauri.release.conf.json --indent 2)" > src-tauri/tauri.release.conf.json
- uses: actions/upload-artifact@v3
@ -123,7 +123,6 @@ jobs:
needs: [prepare-json-files]
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
steps:
@ -244,7 +243,6 @@ jobs:
args: "${{ matrix.os == 'macos-latest' && '--target universal-apple-darwin' || '' }} ${{ env.TAURI_CONF_ARGS }}"
- uses: actions/upload-artifact@v3
if: matrix.os != 'ubuntu-latest'
env:
PREFIX: ${{ matrix.os == 'macos-latest' && 'src-tauri/target/universal-apple-darwin' || 'src-tauri/target' }}
MODE: ${{ env.BUILD_RELEASE == 'true' && 'release' || 'debug' }}
@ -259,7 +257,7 @@ jobs:
export VITE_KC_API_BASE_URL
xvfb-run yarn test:e2e:tauri
env:
E2E_APPLICATION: "./src-tauri/target/${{ env.BUILD_RELEASE == 'true' && 'release' || 'debug' }}/zoo-modeling-app"
E2E_APPLICATION: "./src-tauri/target/${{ env.BUILD_RELEASE == 'true' && 'release' || 'debug' }}/kittycad-modeling"
KITTYCAD_API_TOKEN: ${{ env.BUILD_RELEASE == 'true' && secrets.KITTYCAD_API_TOKEN || secrets.KITTYCAD_API_TOKEN_DEV }}
@ -273,24 +271,26 @@ jobs:
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' }}
WEBSITE_DIR: ${{ github.event_name == 'release' && 'dl.zoo.dev/releases/modeling-app' || 'dl.zoo.dev/releases/modeling-app/nightly' }}
steps:
- uses: actions/download-artifact@v3
- name: Generate the update static endpoint
run: |
ls -l artifact/*/*oo*
ls -l artifact/*/*itty*
DARWIN_SIG=`cat artifact/macos/*.app.tar.gz.sig`
LINUX_SIG=`cat artifact/appimage/*.AppImage.tar.gz.sig`
WINDOWS_SIG=`cat artifact/msi/*.msi.zip.sig`
RELEASE_DIR=https://${WEBSITE_DIR}/${VERSION}
RELEASE_DIR=https://${BUCKET_DIR}/${VERSION}
jq --null-input \
--arg version "${VERSION}" \
--arg pub_date "${PUB_DATE}" \
--arg notes "${NOTES}" \
--arg darwin_sig "$DARWIN_SIG" \
--arg darwin_url "$RELEASE_DIR/macos/Zoo%20Modeling%20App.app.tar.gz" \
--arg darwin_url "$RELEASE_DIR/macos/KittyCAD%20Modeling.app.tar.gz" \
--arg linux_sig "$LINUX_SIG" \
--arg linux_url "$RELEASE_DIR/appimage/kittycad-modeling_${VERSION_NO_V}_amd64.AppImage.tar.gz" \
--arg windows_sig "$WINDOWS_SIG" \
--arg windows_url "$RELEASE_DIR/msi/Zoo%20Modeling%20App_${VERSION_NO_V}_x64_en-US.msi.zip" \
--arg windows_url "$RELEASE_DIR/msi/KittyCAD%20Modeling_${VERSION_NO_V}_x64_en-US.msi.zip" \
'{
"version": $version,
"pub_date": $pub_date,
@ -304,6 +304,10 @@ jobs:
"signature": $darwin_sig,
"url": $darwin_url
},
"linux-x86_64": {
"signature": $linux_sig,
"url": $linux_url
},
"windows-x86_64": {
"signature": $windows_sig,
"url": $windows_url
@ -314,13 +318,14 @@ jobs:
- name: Generate the download static endpoint
run: |
RELEASE_DIR=https://${WEBSITE_DIR}/${VERSION}
RELEASE_DIR=https://${BUCKET_DIR}/${VERSION}
jq --null-input \
--arg version "${VERSION}" \
--arg pub_date "${PUB_DATE}" \
--arg notes "${NOTES}" \
--arg darwin_url "$RELEASE_DIR/dmg/Zoo%20Modeling%20App_${VERSION_NO_V}_universal.dmg" \
--arg windows_url "$RELEASE_DIR/msi/Zoo%20Modeling%20App_${VERSION_NO_V}_x64_en-US.msi" \
--arg darwin_url "$RELEASE_DIR/dmg/KittyCAD%20Modeling_${VERSION_NO_V}_universal.dmg" \
--arg linux_url "$RELEASE_DIR/appimage/kittycad-modeling_${VERSION_NO_V}_amd64.AppImage" \
--arg windows_url "$RELEASE_DIR/msi/KittyCAD%20Modeling_${VERSION_NO_V}_x64_en-US.msi" \
'{
"version": $version,
"pub_date": $pub_date,
@ -329,6 +334,9 @@ jobs:
"dmg-universal": {
"url": $darwin_url
},
"appimage-x86_64": {
"url": $linux_url
},
"msi-x86_64": {
"url": $windows_url
}
@ -342,26 +350,26 @@ jobs:
credentials_json: '${{ secrets.GOOGLE_CLOUD_DL_SA }}'
- name: Set up Google Cloud SDK
uses: google-github-actions/setup-gcloud@v2.0.0
uses: google-github-actions/setup-gcloud@v1.1.1
with:
project_id: kittycadapi
- name: Upload release files to public bucket
uses: google-github-actions/upload-cloud-storage@v2.0.0
uses: google-github-actions/upload-cloud-storage@v1.0.3
with:
path: artifact
glob: '*/Zoo*'
glob: '*/*itty*'
parent: false
destination: ${{ env.BUCKET_DIR }}/${{ env.VERSION }}
- name: Upload update endpoint to public bucket
uses: google-github-actions/upload-cloud-storage@v2.0.0
uses: google-github-actions/upload-cloud-storage@v1.0.3
with:
path: last_update.json
destination: ${{ env.BUCKET_DIR }}
- name: Upload download endpoint to public bucket
uses: google-github-actions/upload-cloud-storage@v2.0.0
uses: google-github-actions/upload-cloud-storage@v1.0.3
with:
path: last_download.json
destination: ${{ env.BUCKET_DIR }}
@ -370,4 +378,4 @@ jobs:
if: ${{ github.event_name == 'release' }}
uses: softprops/action-gh-release@v1
with:
files: 'artifact/*/Zoo*'
files: artifact/*/*itty*

View File

@ -14,7 +14,6 @@ jobs:
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- uses: KittyCAD/action-install-cli@v0.2.16
- name: Install dependencies
run: yarn
- name: Install Playwright Browsers
@ -34,7 +33,6 @@ jobs:
env:
CI: true
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
snapshottoken: ${{ secrets.KITTYCAD_API_TOKEN }}
- uses: actions/upload-artifact@v3
if: always()
with:

9
.gitignore vendored
View File

@ -37,15 +37,6 @@ src/wasm-lib/lcov.info
e2e/playwright/playwright-secrets.env
e2e/playwright/temp1.png
e2e/playwright/temp2.png
# exports from snapshot-tests.spec.ts
e2e/playwright/export-snapshots/*.ply
e2e/playwright/export-snapshots/*.obj
e2e/playwright/export-snapshots/*.step
e2e/playwright/export-snapshots/*.stl
e2e/playwright/export-snapshots/*binary.gltf
e2e/playwright/export-snapshots/*embedded.gltf
/test-results/
/playwright-report/
/blob-report/

View File

@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2023 The Zoo Authors
Copyright (c) 2023 The KittyCAD Authors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@ -1,17 +1,17 @@
![Zoo Modeling App](/public/zma-logomark-outlined.png)
![KittyCAD Modeling App](/public/kcma-logomark.png)
## Zoo Modeling App
## KittyCAD Modeling App
live at [app.zoo.dev](https://app.zoo.dev/)
live at [app.kittycad.io](https://app.kittycad.io/)
A CAD application from the future, brought to you by the [Zoo team](https://zoo.dev).
A CAD application from the future, brought to you by the [KittyCAD team](https://kittycad.io).
Modeling App is our take on what a modern modelling experience can be. It is applying several lessons learned in the decades since most major CAD tools came into existence:
The KittyCAD modeling app is our take on what a modern modelling experience can be. It is applying several lessons learned in the decades since most major CAD tools came into existence:
- All artifacts—including parts and assemblies—should be represented as human-readable code. At the end of the day, your CAD project should be "plain text"
- This makes version control—which is a solved problem in software engineering—trivial for CAD
- All GUI (or point-and-click) interactions should be actions performed on this code representation under the hood
- This unlocks a hybrid approach to modeling. Whether you point-and-click as you always have or you write your own KCL code, you are performing the same action in Modeling App
- This unlocks a hybrid approach to modeling. Whether you point-and-click as you always have or you write your own KCL code, you are performing the same action in KittyCAD Modeling App
- Everything graphics _has_ to be built for the GPU
- Most CAD applications have had to retrofit support for GPUs, but our geometry engine is made for GPUs (primarily Nvidia's Vulkan), getting the order of magnitude rendering performance boost with it
- Make the resource-intensive pieces of an application auto-scaling
@ -19,9 +19,9 @@ Modeling App is our take on what a modern modelling experience can be. It is app
We are excited about what a small team of people could build in a short time with our API. We welcome you to try our API, build your own applications, or contribute to ours!
Modeling App is a _hybrid_ user interface for CAD modeling. You can point-and-click to design parts (and soon assemblies), but everything you make is really just [`kcl` code](https://github.com/KittyCAD/kcl-experiments) under the hood. All of your CAD models can be checked into source control such as GitHub and responsibly versioned, rolled back, and more.
KittyCAD Modeling App is a _hybrid_ user interface for CAD modeling. You can point-and-click to design parts (and soon assemblies), but everything you make is really just [`kcl` code](https://github.com/KittyCAD/kcl-experiments) under the hood. All of your CAD models can be checked into source control such as GitHub and responsibly versioned, rolled back, and more.
The 3D view in Modeling App is just a video stream from our hosted geometry engine. The app sends new modeling commands to the engine via WebSockets, which returns back video frames of the view within the engine.
The 3D view in KittyCAD Modeling App is just a video stream from our hosted geometry engine. The app sends new modeling commands to the engine via WebSockets, which returns back video frames of the view within the engine.
## Tools
@ -94,6 +94,7 @@ For running the rust (not tauri rust though) only, you can
cd src/wasm-lib
cargo test
```
but you will need to have install ffmpeg prior to.
## Tauri
@ -182,9 +183,9 @@ For more information on fuzzing you can check out
First time running plawright locally, you'll need to add the secrets file
```bash
touch ./e2e/playwright/playwright-secrets.env
echo 'token="your-token"\nsnapshottoken="your-snapshot-token"' > ./e2e/playwright/playwright-secrets2.env
echo 'token="your-token"' > ./e2e/playwright/playwright-secrets.env
```
then replace "your-token" with a dev token from dev.zoo.dev/account/api-tokens
then replace "your-token" with a dev token from dev.kittycad.io/account/api-tokens
then:
run playwright

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 207 KiB

View File

@ -19765,16 +19765,46 @@
"tags": [],
"args": [
{
"name": "to",
"type": "[number]",
"name": "data",
"type": "TangentialArcToData",
"schema": {
"type": "array",
"items": {
"type": "number",
"format": "double"
},
"maxItems": 2,
"minItems": 2
"description": "Data to draw a tangential arc to a specific point.",
"anyOf": [
{
"description": "A point with a tag.",
"type": "object",
"required": [
"tag",
"to"
],
"properties": {
"tag": {
"description": "The tag.",
"type": "string"
},
"to": {
"description": "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.",
"type": "array",
"items": {
"type": "number",
"format": "double"
},
"maxItems": 2,
"minItems": 2
}
}
},
{
"description": "A point 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.",
"type": "array",
"items": {
"type": "number",
"format": "double"
},
"maxItems": 2,
"minItems": 2
}
]
},
"required": true
},
@ -20216,15 +20246,6 @@
}
},
"required": true
},
{
"name": "tag",
"type": "String",
"schema": {
"type": "string",
"nullable": true
},
"required": true
}
],
"returnValue": {

View File

@ -3905,12 +3905,21 @@ Draw an arc.
```
tangentialArcTo(to: [number], sketch_group: SketchGroup, tag: String) -> SketchGroup
tangentialArcTo(data: TangentialArcToData, sketch_group: SketchGroup) -> SketchGroup
```
#### Arguments
* `to`: `[number]`
* `data`: `TangentialArcToData` - Data to draw a tangential arc to a specific point.
```
{
// The tag.
tag: string,
// 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.
to: [number, number],
} |
[number, number]
```
* `sketch_group`: `SketchGroup` - A sketch group is a collection of paths.
```
{
@ -3976,7 +3985,6 @@ tangentialArcTo(to: [number], sketch_group: SketchGroup, tag: String) -> SketchG
}],
}
```
* `tag`: `String`
#### Returns

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,189 @@
v 0 -4 0
v 0 0 0
v 0 -4 -1
v 0 0 -1
v 3.0950184 -4 -1
v 3.0950184 0 -1
v 5.9513144 -4 -3
v 5.9513144 0 -3
v 9.5 -4 -3
v 9.5 0 -3
v 9.5 -4 -2.5
v 9.5 0 -2.5
v 6.108964 -4 -2.5
v 6.108964 0 -2.5
v 3.4311862 -4 -0.625
v 4.323779 -4 -1.25
v 4.323779 -0 -1.25
v 3.4311862 -0 -0.625
v 2.5385938 0 0
v 2.5385938 -4 0
v 3.342784 -4 0.375
v 4.146974 -4 0.75
v 3.342784 -0 0.375
v 4.146974 -0 0.75
v 5.755354 0 1.5
v 5.755354 -4 1.5
v 9.5 -4 1.5
v 9.5 0 1.5
v 9.5 -4 2
v 9.5 0 2
v 5.644507 -4 2
v 5.644507 0 2
v 3.5 -4 1
v 3.5 0 1
v 0 -4 1
v 0 0 1
vt 0.0127 -0.0508
vt 0.0127 0.0508
vt -0.0127 -0.0508
vt -0.0127 0.0508
vt -0.039306734 0.0508
vt -0.039306734 -0.0508
vt 0.039306734 0.0508
vt 0.039306734 -0.0508
vt -0.04428355 0.0508
vt -0.04428355 -0.0508
vt 0.04428355 0.0508
vt 0.04428355 -0.0508
vt -0.045068305 0.0508
vt -0.045068305 -0.0508
vt 0.045068305 0.0508
vt 0.045068305 -0.0508
vt -0.00635 0.0508
vt -0.00635 -0.0508
vt 0.00635 0.0508
vt 0.00635 -0.0508
vt 0.04306616 -0.0508
vt 0.04306616 0.0508
vt -0.04306616 -0.0508
vt -0.04306616 0.0508
vt -0.027677217 -0.0508
vt 0.000000000000000048572257 -0.0508
vt 0.000000000000000048572257 0.0508
vt 0.055354435 -0.0508
vt 0.055354435 0.0508
vt -0.027677217 0.0508
vt -0.055354435 0.0508
vt -0.055354435 -0.0508
vt -0.02253807 0.0508
vt -0.04507614 0.0508
vt -0.04507614 -0.0508
vt 0.00000000000000005551115 0.0508
vt -0.02253807 -0.0508
vt 0.00000000000000005551115 -0.0508
vt 0.04507614 -0.0508
vt 0.04507614 0.0508
vt -0.047557 0.0508
vt -0.047557 -0.0508
vt 0.047557 0.0508
vt 0.047557 -0.0508
vt 0.04896476 -0.0508
vt 0.04896476 0.0508
vt -0.04896476 -0.0508
vt -0.04896476 0.0508
vt 0.03005076 -0.0508
vt 0.03005076 0.0508
vt -0.03005076 -0.0508
vt -0.03005076 0.0508
vt 0.04445 -0.0508
vt 0.04445 0.0508
vt -0.04445 -0.0508
vt -0.04445 0.0508
vt 0.08490671 0.009525
vt 0.06448028 0
vt 0.0889 0.0254
vt 0.08715213 -0.015875
vt 0.10982399 -0.03175
vt 0.07861347 -0.0254
vt 0.10533314 0.01905
vt 0.15116338 -0.0762
vt 0 -0.0254
vt 0 0
vt 0.2413 -0.0762
vt 0.15516768 -0.0635
vt 0.2413 -0.0635
vt 0.14337048 0.0508
vt 0.146186 0.0381
vt 0.2413 0.0381
vt 0.2413 0.0508
vt 0 0.0254
vn -1 -0 0
vn 0 -0 -1
vn -0.57357645 -0 -0.81915206
vn 1 -0 0
vn 0 -0 1
vn 0.57357645 -0 0.81915206
vn 0.42261827 -0 -0.9063078
vn -0.42261827 -0 0.9063078
vn -0 1 -0
vn 0 -1 0
o Unnamed-0
f 1/1/1 2/2/1 3/3/1
f 3/3/1 2/2/1 4/4/1
f 3/5/2 4/6/2 5/7/2
f 5/7/2 4/6/2 6/8/2
f 5/9/3 6/10/3 7/11/3
f 7/11/3 6/10/3 8/12/3
f 7/13/2 8/14/2 9/15/2
f 9/15/2 8/14/2 10/16/2
f 9/17/4 10/18/4 11/19/4
f 11/19/4 10/18/4 12/20/4
f 11/21/5 12/22/5 13/23/5
f 13/23/5 12/22/5 14/24/5
f 15/25/6 16/26/6 17/27/6
f 16/26/6 13/28/6 14/29/6
f 18/30/6 19/31/6 20/32/6
f 15/25/6 18/30/6 20/32/6
f 16/26/6 14/29/6 17/27/6
f 18/30/6 15/25/6 17/27/6
f 21/33/7 20/34/7 19/35/7
f 22/36/7 21/33/7 23/37/7
f 23/37/7 24/38/7 22/36/7
f 24/38/7 25/39/7 26/40/7
f 21/33/7 19/35/7 23/37/7
f 26/40/7 22/36/7 24/38/7
f 26/41/2 25/42/2 27/43/2
f 27/43/2 25/42/2 28/44/2
f 27/17/4 28/18/4 29/19/4
f 29/19/4 28/18/4 30/20/4
f 29/45/5 30/46/5 31/47/5
f 31/47/5 30/46/5 32/48/5
f 31/49/8 32/50/8 33/51/8
f 33/51/8 32/50/8 34/52/8
f 33/53/5 34/54/5 35/55/5
f 35/55/5 34/54/5 36/56/5
f 35/1/1 36/2/1 1/3/1
f 1/3/1 36/2/1 2/4/1
f 23/57/9 19/58/9 34/59/9
f 18/60/9 17/61/9 6/62/9
f 23/57/9 34/59/9 24/63/9
f 17/61/9 8/64/9 6/62/9
f 4/65/9 19/58/9 6/62/9
f 4/65/9 2/66/9 19/58/9
f 10/67/9 14/68/9 12/69/9
f 10/67/9 8/64/9 14/68/9
f 8/64/9 17/61/9 14/68/9
f 32/70/9 25/71/9 24/63/9
f 6/62/9 19/58/9 18/60/9
f 24/63/9 34/59/9 32/70/9
f 28/72/9 25/71/9 30/73/9
f 25/71/9 32/70/9 30/73/9
f 19/58/9 2/66/9 36/74/9
f 34/59/9 19/58/9 36/74/9
f 21/57/10 33/59/10 20/58/10
f 22/63/10 33/59/10 21/57/10
f 15/60/10 5/62/10 16/61/10
f 22/63/10 26/71/10 31/70/10
f 35/74/10 20/58/10 33/59/10
f 35/74/10 1/66/10 20/58/10
f 31/70/10 26/71/10 29/73/10
f 29/73/10 26/71/10 27/72/10
f 22/63/10 31/70/10 33/59/10
f 20/58/10 5/62/10 15/60/10
f 16/61/10 5/62/10 7/64/10
f 13/68/10 16/61/10 7/64/10
f 11/69/10 13/68/10 9/67/10
f 13/68/10 7/64/10 9/67/10
f 20/58/10 3/65/10 5/62/10
f 3/65/10 20/58/10 1/66/10

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

View File

@ -0,0 +1,282 @@
ply
format ascii 1.0
comment Generated by kittycad.io
element vertex 204
property float x
property float y
property float z
element face 68
property list uchar uint vertex_indices
end_header
0 0 4
0 0 0
0 -1 4
0 -1 4
0 0 0
0 -1 0
0 -1 4
0 -1 0
3.0950184 -1 4
3.0950184 -1 4
0 -1 0
3.0950184 -1 0
3.0950184 -1 4
3.0950184 -1 0
5.9513144 -3 4
5.9513144 -3 4
3.0950184 -1 0
5.9513144 -3 0
5.9513144 -3 4
5.9513144 -3 0
9.5 -3 4
9.5 -3 4
5.9513144 -3 0
9.5 -3 0
9.5 -3 4
9.5 -3 0
9.5 -2.5 4
9.5 -2.5 4
9.5 -3 0
9.5 -2.5 0
9.5 -2.5 4
9.5 -2.5 0
6.108964 -2.5 4
6.108964 -2.5 4
9.5 -2.5 0
6.108964 -2.5 0
3.4311862 -0.625 4
4.323779 -1.25 4
4.323779 -1.25 0
4.323779 -1.25 4
6.108964 -2.5 4
6.108964 -2.5 0
3.4311862 -0.625 0
2.5385938 0 0
2.5385938 0 4
3.4311862 -0.625 4
3.4311862 -0.625 0
2.5385938 0 4
4.323779 -1.25 4
6.108964 -2.5 0
4.323779 -1.25 0
3.4311862 -0.625 0
3.4311862 -0.625 4
4.323779 -1.25 0
3.342784 0.375 4
2.5385938 0 4
2.5385938 0 0
4.146974 0.75 4
3.342784 0.375 4
3.342784 0.375 0
3.342784 0.375 0
4.146974 0.75 0
4.146974 0.75 4
4.146974 0.75 0
5.755354 1.5 0
5.755354 1.5 4
3.342784 0.375 4
2.5385938 0 0
3.342784 0.375 0
5.755354 1.5 4
4.146974 0.75 4
4.146974 0.75 0
5.755354 1.5 4
5.755354 1.5 0
9.5 1.5 4
9.5 1.5 4
5.755354 1.5 0
9.5 1.5 0
9.5 1.5 4
9.5 1.5 0
9.5 2 4
9.5 2 4
9.5 1.5 0
9.5 2 0
9.5 2 4
9.5 2 0
5.644507 2 4
5.644507 2 4
9.5 2 0
5.644507 2 0
5.644507 2 4
5.644507 2 0
3.5 1 4
3.5 1 4
5.644507 2 0
3.5 1 0
3.5 1 4
3.5 1 0
0 1 4
0 1 4
3.5 1 0
0 1 0
0 1 4
0 1 0
0 0 4
0 0 4
0 1 0
0 0 0
3.342784 0.375 0
2.5385938 0 0
3.5 1 0
3.4311862 -0.625 0
4.323779 -1.25 0
3.0950184 -1 0
3.342784 0.375 0
3.5 1 0
4.146974 0.75 0
4.323779 -1.25 0
5.9513144 -3 0
3.0950184 -1 0
0 -1 0
2.5385938 0 0
3.0950184 -1 0
0 -1 0
0 0 0
2.5385938 0 0
9.5 -3 0
6.108964 -2.5 0
9.5 -2.5 0
9.5 -3 0
5.9513144 -3 0
6.108964 -2.5 0
5.9513144 -3 0
4.323779 -1.25 0
6.108964 -2.5 0
5.644507 2 0
5.755354 1.5 0
4.146974 0.75 0
3.0950184 -1 0
2.5385938 0 0
3.4311862 -0.625 0
4.146974 0.75 0
3.5 1 0
5.644507 2 0
9.5 1.5 0
5.755354 1.5 0
9.5 2 0
5.755354 1.5 0
5.644507 2 0
9.5 2 0
2.5385938 0 0
0 0 0
0 1 0
3.5 1 0
2.5385938 0 0
0 1 0
3.342784 0.375 4
3.5 1 4
2.5385938 0 4
4.146974 0.75 4
3.5 1 4
3.342784 0.375 4
3.4311862 -0.625 4
3.0950184 -1 4
4.323779 -1.25 4
4.146974 0.75 4
5.755354 1.5 4
5.644507 2 4
0 1 4
2.5385938 0 4
3.5 1 4
0 1 4
0 0 4
2.5385938 0 4
5.644507 2 4
5.755354 1.5 4
9.5 2 4
9.5 2 4
5.755354 1.5 4
9.5 1.5 4
4.146974 0.75 4
5.644507 2 4
3.5 1 4
2.5385938 0 4
3.0950184 -1 4
3.4311862 -0.625 4
4.323779 -1.25 4
3.0950184 -1 4
5.9513144 -3 4
6.108964 -2.5 4
4.323779 -1.25 4
5.9513144 -3 4
9.5 -2.5 4
6.108964 -2.5 4
9.5 -3 4
6.108964 -2.5 4
5.9513144 -3 4
9.5 -3 4
2.5385938 0 4
0 -1 4
3.0950184 -1 4
0 -1 4
2.5385938 0 4
0 0 4
3 0 1 2
3 3 4 5
3 6 7 8
3 9 10 11
3 12 13 14
3 15 16 17
3 18 19 20
3 21 22 23
3 24 25 26
3 27 28 29
3 30 31 32
3 33 34 35
3 36 37 38
3 39 40 41
3 42 43 44
3 45 46 47
3 48 49 50
3 51 52 53
3 54 55 56
3 57 58 59
3 60 61 62
3 63 64 65
3 66 67 68
3 69 70 71
3 72 73 74
3 75 76 77
3 78 79 80
3 81 82 83
3 84 85 86
3 87 88 89
3 90 91 92
3 93 94 95
3 96 97 98
3 99 100 101
3 102 103 104
3 105 106 107
3 108 109 110
3 111 112 113
3 114 115 116
3 117 118 119
3 120 121 122
3 123 124 125
3 126 127 128
3 129 130 131
3 132 133 134
3 135 136 137
3 138 139 140
3 141 142 143
3 144 145 146
3 147 148 149
3 150 151 152
3 153 154 155
3 156 157 158
3 159 160 161
3 162 163 164
3 165 166 167
3 168 169 170
3 171 172 173
3 174 175 176
3 177 178 179
3 180 181 182
3 183 184 185
3 186 187 188
3 189 190 191
3 192 193 194
3 195 196 197
3 198 199 200
3 201 202 203

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

View File

@ -74,171 +74,171 @@ DATA;
#58 = CARTESIAN_POINT('NONE', (0, 0.0254, 0.1016));
#59 = VERTEX_POINT('NONE', #58);
#60 = DIRECTION('NONE', (0, -1, 0));
#61 = VECTOR('NONE', #60, 1);
#61 = VECTOR('NONE', #60, 0.0254);
#62 = CARTESIAN_POINT('NONE', (0, 0, -0));
#63 = LINE('NONE', #62, #61);
#64 = DIRECTION('NONE', (0, 0, 1));
#65 = VECTOR('NONE', #64, 1);
#65 = VECTOR('NONE', #64, 0.1016);
#66 = CARTESIAN_POINT('NONE', (0, -0.0254, -0));
#67 = LINE('NONE', #66, #65);
#68 = DIRECTION('NONE', (0, -1, 0));
#69 = VECTOR('NONE', #68, 1);
#69 = VECTOR('NONE', #68, 0.0254);
#70 = CARTESIAN_POINT('NONE', (0, 0, 0.1016));
#71 = LINE('NONE', #70, #69);
#72 = DIRECTION('NONE', (0, 0, 1));
#73 = VECTOR('NONE', #72, 1);
#73 = VECTOR('NONE', #72, 0.1016);
#74 = CARTESIAN_POINT('NONE', (0, 0, -0));
#75 = LINE('NONE', #74, #73);
#76 = DIRECTION('NONE', (1, 0, 0));
#77 = VECTOR('NONE', #76, 1);
#77 = VECTOR('NONE', #76, 0.07861346939195568);
#78 = CARTESIAN_POINT('NONE', (0, -0.0254, -0));
#79 = LINE('NONE', #78, #77);
#80 = DIRECTION('NONE', (0, 0, 1));
#81 = VECTOR('NONE', #80, 1);
#81 = VECTOR('NONE', #80, 0.1016);
#82 = CARTESIAN_POINT('NONE', (0.07861346939195568, -0.0254, -0));
#83 = LINE('NONE', #82, #81);
#84 = DIRECTION('NONE', (1, 0, 0));
#85 = VECTOR('NONE', #84, 1);
#85 = VECTOR('NONE', #84, 0.07861346939195568);
#86 = CARTESIAN_POINT('NONE', (0, -0.0254, 0.1016));
#87 = LINE('NONE', #86, #85);
#88 = DIRECTION('NONE', (0.8191520442889919, -0.5735764363510459, 0));
#89 = VECTOR('NONE', #88, 1);
#89 = VECTOR('NONE', #88, 0.08856709721755177);
#90 = CARTESIAN_POINT('NONE', (0.07861346939195568, -0.0254, -0));
#91 = LINE('NONE', #90, #89);
#92 = DIRECTION('NONE', (0, 0, 1));
#93 = VECTOR('NONE', #92, 1);
#93 = VECTOR('NONE', #92, 0.1016);
#94 = CARTESIAN_POINT('NONE', (0.1511633881344551, -0.07619999999999998, -0));
#95 = LINE('NONE', #94, #93);
#96 = DIRECTION('NONE', (0.8191520442889919, -0.5735764363510459, 0));
#97 = VECTOR('NONE', #96, 1);
#97 = VECTOR('NONE', #96, 0.08856709721755177);
#98 = CARTESIAN_POINT('NONE', (0.07861346939195568, -0.0254, 0.1016));
#99 = LINE('NONE', #98, #97);
#100 = DIRECTION('NONE', (1, -0.0000000000000003079278779307945, 0));
#101 = VECTOR('NONE', #100, 1);
#101 = VECTOR('NONE', #100, 0.09013661186554489);
#102 = CARTESIAN_POINT('NONE', (0.1511633881344551, -0.07619999999999998, -0));
#103 = LINE('NONE', #102, #101);
#104 = DIRECTION('NONE', (0, 0, 1));
#105 = VECTOR('NONE', #104, 1);
#105 = VECTOR('NONE', #104, 0.1016);
#106 = CARTESIAN_POINT('NONE', (0.2413, -0.0762, -0));
#107 = LINE('NONE', #106, #105);
#108 = DIRECTION('NONE', (1, -0.0000000000000003079278779307945, 0));
#109 = VECTOR('NONE', #108, 1);
#109 = VECTOR('NONE', #108, 0.09013661186554489);
#110 = CARTESIAN_POINT('NONE', (0.1511633881344551, -0.07619999999999998, 0.1016));
#111 = LINE('NONE', #110, #109);
#112 = DIRECTION('NONE', (0, 1, 0));
#113 = VECTOR('NONE', #112, 1);
#113 = VECTOR('NONE', #112, 0.012700000000000003);
#114 = CARTESIAN_POINT('NONE', (0.2413, -0.0762, -0));
#115 = LINE('NONE', #114, #113);
#116 = DIRECTION('NONE', (0, 0, 1));
#117 = VECTOR('NONE', #116, 1);
#117 = VECTOR('NONE', #116, 0.1016);
#118 = CARTESIAN_POINT('NONE', (0.2413, -0.0635, -0));
#119 = LINE('NONE', #118, #117);
#120 = DIRECTION('NONE', (0, 1, 0));
#121 = VECTOR('NONE', #120, 1);
#121 = VECTOR('NONE', #120, 0.012700000000000003);
#122 = CARTESIAN_POINT('NONE', (0.2413, -0.0762, 0.1016));
#123 = LINE('NONE', #122, #121);
#124 = DIRECTION('NONE', (-1, 0, 0));
#125 = VECTOR('NONE', #124, 1);
#125 = VECTOR('NONE', #124, 0.08613231724678178);
#126 = CARTESIAN_POINT('NONE', (0.2413, -0.0635, -0));
#127 = LINE('NONE', #126, #125);
#128 = DIRECTION('NONE', (0, 0, 1));
#129 = VECTOR('NONE', #128, 1);
#129 = VECTOR('NONE', #128, 0.1016);
#130 = CARTESIAN_POINT('NONE', (0.1551676827532182, -0.0635, -0));
#131 = LINE('NONE', #130, #129);
#132 = DIRECTION('NONE', (-1, 0, 0));
#133 = VECTOR('NONE', #132, 1);
#133 = VECTOR('NONE', #132, 0.08613231724678178);
#134 = CARTESIAN_POINT('NONE', (0.2413, -0.0635, 0.1016));
#135 = LINE('NONE', #134, #133);
#136 = DIRECTION('NONE', (-0.8191520442889919, 0.573576436351046, 0));
#137 = VECTOR('NONE', #136, 1);
#136 = DIRECTION('NONE', (-0.8191520442889918, 0.573576436351046, 0));
#137 = VECTOR('NONE', #136, 0.11070887152193974);
#138 = CARTESIAN_POINT('NONE', (0.1551676827532182, -0.0635, -0));
#139 = LINE('NONE', #138, #137);
#140 = DIRECTION('NONE', (0, 0, 1));
#141 = VECTOR('NONE', #140, 1);
#141 = VECTOR('NONE', #140, 0.1016);
#142 = CARTESIAN_POINT('NONE', (0.06448028432509392, 0, -0));
#143 = LINE('NONE', #142, #141);
#144 = DIRECTION('NONE', (-0.8191520442889919, 0.573576436351046, 0));
#145 = VECTOR('NONE', #144, 1);
#144 = DIRECTION('NONE', (-0.8191520442889918, 0.573576436351046, 0));
#145 = VECTOR('NONE', #144, 0.11070887152193974);
#146 = CARTESIAN_POINT('NONE', (0.1551676827532182, -0.0635, 0.1016));
#147 = LINE('NONE', #146, #145);
#148 = DIRECTION('NONE', (0.90630778703665, 0.4226182617406993, 0));
#149 = VECTOR('NONE', #148, 1);
#149 = VECTOR('NONE', #148, 0.09015228031811025);
#150 = CARTESIAN_POINT('NONE', (0.06448028432509392, 0, -0));
#151 = LINE('NONE', #150, #149);
#152 = DIRECTION('NONE', (0, 0, 1));
#153 = VECTOR('NONE', #152, 1);
#153 = VECTOR('NONE', #152, 0.1016);
#154 = CARTESIAN_POINT('NONE', (0.14618599799650817, 0.03810000000000001, -0));
#155 = LINE('NONE', #154, #153);
#156 = DIRECTION('NONE', (0.90630778703665, 0.4226182617406993, 0));
#157 = VECTOR('NONE', #156, 1);
#157 = VECTOR('NONE', #156, 0.09015228031811025);
#158 = CARTESIAN_POINT('NONE', (0.06448028432509392, 0, 0.1016));
#159 = LINE('NONE', #158, #157);
#160 = DIRECTION('NONE', (1, -0.00000000000000007295344279228718, 0));
#161 = VECTOR('NONE', #160, 1);
#161 = VECTOR('NONE', #160, 0.09511400200349182);
#162 = CARTESIAN_POINT('NONE', (0.14618599799650817, 0.03810000000000001, -0));
#163 = LINE('NONE', #162, #161);
#164 = DIRECTION('NONE', (0, 0, 1));
#165 = VECTOR('NONE', #164, 1);
#165 = VECTOR('NONE', #164, 0.1016);
#166 = CARTESIAN_POINT('NONE', (0.2413, 0.0381, -0));
#167 = LINE('NONE', #166, #165);
#168 = DIRECTION('NONE', (1, -0.00000000000000007295344279228718, 0));
#169 = VECTOR('NONE', #168, 1);
#169 = VECTOR('NONE', #168, 0.09511400200349182);
#170 = CARTESIAN_POINT('NONE', (0.14618599799650817, 0.03810000000000001, 0.1016));
#171 = LINE('NONE', #170, #169);
#172 = DIRECTION('NONE', (0, 1, 0));
#173 = VECTOR('NONE', #172, 1);
#173 = VECTOR('NONE', #172, 0.012699999999999996);
#174 = CARTESIAN_POINT('NONE', (0.2413, 0.0381, -0));
#175 = LINE('NONE', #174, #173);
#176 = DIRECTION('NONE', (0, 0, 1));
#177 = VECTOR('NONE', #176, 1);
#177 = VECTOR('NONE', #176, 0.1016);
#178 = CARTESIAN_POINT('NONE', (0.2413, 0.0508, -0));
#179 = LINE('NONE', #178, #177);
#180 = DIRECTION('NONE', (0, 1, 0));
#181 = VECTOR('NONE', #180, 1);
#181 = VECTOR('NONE', #180, 0.012699999999999996);
#182 = CARTESIAN_POINT('NONE', (0.2413, 0.0381, 0.1016));
#183 = LINE('NONE', #182, #181);
#184 = DIRECTION('NONE', (-1, 0, 0));
#185 = VECTOR('NONE', #184, 1);
#185 = VECTOR('NONE', #184, 0.0979295242190572);
#186 = CARTESIAN_POINT('NONE', (0.2413, 0.0508, -0));
#187 = LINE('NONE', #186, #185);
#188 = DIRECTION('NONE', (0, 0, 1));
#189 = VECTOR('NONE', #188, 1);
#189 = VECTOR('NONE', #188, 0.1016);
#190 = CARTESIAN_POINT('NONE', (0.14337047578094278, 0.0508, -0));
#191 = LINE('NONE', #190, #189);
#192 = DIRECTION('NONE', (-1, 0, 0));
#193 = VECTOR('NONE', #192, 1);
#193 = VECTOR('NONE', #192, 0.0979295242190572);
#194 = CARTESIAN_POINT('NONE', (0.2413, 0.0508, 0.1016));
#195 = LINE('NONE', #194, #193);
#196 = DIRECTION('NONE', (-0.90630778703665, -0.42261826174069944, 0));
#197 = VECTOR('NONE', #196, 1);
#196 = DIRECTION('NONE', (-0.9063077870366499, -0.42261826174069944, 0));
#197 = VECTOR('NONE', #196, 0.06010152021207346);
#198 = CARTESIAN_POINT('NONE', (0.14337047578094278, 0.0508, -0));
#199 = LINE('NONE', #198, #197);
#200 = DIRECTION('NONE', (0, 0, 1));
#201 = VECTOR('NONE', #200, 1);
#201 = VECTOR('NONE', #200, 0.1016);
#202 = CARTESIAN_POINT('NONE', (0.08889999999999999, 0.0254, -0));
#203 = LINE('NONE', #202, #201);
#204 = DIRECTION('NONE', (-0.90630778703665, -0.42261826174069944, 0));
#205 = VECTOR('NONE', #204, 1);
#204 = DIRECTION('NONE', (-0.9063077870366499, -0.42261826174069944, 0));
#205 = VECTOR('NONE', #204, 0.06010152021207346);
#206 = CARTESIAN_POINT('NONE', (0.14337047578094278, 0.0508, 0.1016));
#207 = LINE('NONE', #206, #205);
#208 = DIRECTION('NONE', (-1, 0, 0));
#209 = VECTOR('NONE', #208, 1);
#209 = VECTOR('NONE', #208, 0.08889999999999999);
#210 = CARTESIAN_POINT('NONE', (0.08889999999999999, 0.0254, -0));
#211 = LINE('NONE', #210, #209);
#212 = DIRECTION('NONE', (0, 0, 1));
#213 = VECTOR('NONE', #212, 1);
#213 = VECTOR('NONE', #212, 0.1016);
#214 = CARTESIAN_POINT('NONE', (0, 0.0254, -0));
#215 = LINE('NONE', #214, #213);
#216 = DIRECTION('NONE', (-1, 0, 0));
#217 = VECTOR('NONE', #216, 1);
#217 = VECTOR('NONE', #216, 0.08889999999999999);
#218 = CARTESIAN_POINT('NONE', (0.08889999999999999, 0.0254, 0.1016));
#219 = LINE('NONE', #218, #217);
#220 = DIRECTION('NONE', (0, -1, 0));
#221 = VECTOR('NONE', #220, 1);
#221 = VECTOR('NONE', #220, 0.0254);
#222 = CARTESIAN_POINT('NONE', (0, 0.0254, -0));
#223 = LINE('NONE', #222, #221);
#224 = DIRECTION('NONE', (0, -1, 0));
#225 = VECTOR('NONE', #224, 1);
#225 = VECTOR('NONE', #224, 0.0254);
#226 = CARTESIAN_POINT('NONE', (0, 0.0254, 0.1016));
#227 = LINE('NONE', #226, #225);
#228 = EDGE_CURVE('NONE', #5, #7, #63, .T.);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

View File

@ -643,11 +643,7 @@ test('Command bar works and can change a setting', async ({ page }) => {
let cmdSearchBar = page.getByPlaceholder('Search commands')
// First try opening the command bar and closing it
// It has a different label on mac and windows/linux, "Meta+K" and "Ctrl+/" respectively
await page
.getByRole('button', { name: 'Ctrl+/' })
.or(page.getByRole('button', { name: '⌘K' }))
.click()
await page.getByRole('button', { name: '⌘K' }).click()
await expect(cmdSearchBar).toBeVisible()
await page.keyboard.press('Escape')
await expect(cmdSearchBar).not.toBeVisible()
@ -662,12 +658,12 @@ test('Command bar works and can change a setting', async ({ page }) => {
const themeOption = page.getByRole('option', { name: 'Set Theme' })
await expect(themeOption).toBeVisible()
await themeOption.click()
const themeInput = page.getByPlaceholder('Select an option')
const themeInput = page.getByPlaceholder(Themes.System)
await expect(themeInput).toBeVisible()
await expect(themeInput).toBeFocused()
// Select dark theme
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowUp')
await expect(page.getByRole('option', { name: Themes.Dark })).toHaveAttribute(
'data-headlessui-state',
'active'
@ -679,59 +675,3 @@ test('Command bar works and can change a setting', async ({ page }) => {
// Check that the theme changed
await expect(page.locator('body')).toHaveClass(`body-bg ${Themes.Dark}`)
})
test('Can extrude from the command bar', async ({ page, context }) => {
await context.addInitScript(async (token) => {
localStorage.setItem(
'persistCode',
`const part001 = startSketchOn('-XZ')
|> startProfileAt([-6.95, 4.98], %)
|> line([25.1, 0.41], %)
|> line([0.73, -14.93], %)
|> line([-23.44, 0.52], %)
|> close(%)`
)
})
const u = getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart()
let cmdSearchBar = page.getByPlaceholder('Search commands')
await page.keyboard.press('Meta+K')
await expect(cmdSearchBar).toBeVisible()
// Search for extrude command and choose it
await page.getByRole('option', { name: 'Extrude' }).click()
await expect(page.locator('#arg-form > label')).toContainText(
'Please select one face'
)
await expect(page.getByRole('button', { name: 'selection' })).toBeDisabled()
// Click to select face and set distance
await u.openAndClearDebugPanel()
await page.getByText('|> startProfileAt([-6.95, 4.98], %)').click()
await u.waitForCmdReceive('select_add')
await u.closeDebugPanel()
await page.getByRole('button', { name: 'Continue' }).click()
await expect(page.getByRole('button', { name: 'distance' })).toBeDisabled()
await page.keyboard.press('Enter')
// Review step and argument hotkeys
await page.keyboard.press('2')
await expect(page.getByRole('button', { name: '5' })).toBeDisabled()
await page.keyboard.press('Enter')
// Check that the code was updated
await page.keyboard.press('Enter')
await expect(page.locator('.cm-content')).toHaveText(
`const part001 = startSketchOn('-XZ')
|> startProfileAt([-6.95, 4.98], %)
|> line([25.1, 0.41], %)
|> line([0.73, -14.93], %)
|> line([-23.44, 0.52], %)
|> close(%)
|> extrude(5, %)`
)
})

View File

@ -14,7 +14,6 @@ try {
} catch (err) {
// probably running in CI
secrets.token = process.env.token || ''
secrets.snapshottoken = process.env.snapshottoken || ''
// add more env vars here to make them available in CI
}

View File

@ -5,8 +5,6 @@ import { v4 as uuidv4 } from 'uuid'
import { getUtils } from './test-utils'
import { Models } from '@kittycad/lib'
import fsp from 'fs/promises'
import { spawn } from 'child_process'
import { APP_NAME } from 'lib/constants'
test.beforeEach(async ({ context, page }) => {
await context.addInitScript(async (token) => {
@ -139,7 +137,6 @@ test('change camera, show planes', async ({ page, context }) => {
test('exports of each format should work', async ({ page, context }) => {
// FYI this test doesn't work with only engine running locally
// And you will need to have the KittyCAD CLI installed
const u = getUtils(page)
await context.addInitScript(async () => {
;(window as any).playwrightSkipFilePicker = true
@ -197,16 +194,9 @@ const part001 = startSketchOn('-XZ')
await page.waitForTimeout(1000)
await u.clearAndCloseDebugPanel()
await page.getByRole('button', { name: APP_NAME }).click()
await page.getByRole('button', { name: 'KittyCAD Modeling App' }).click()
interface Paths {
modelPath: string
imagePath: string
outputType: string
}
const doExport = async (
output: Models['OutputFormat_type']
): Promise<Paths> => {
const doExport = async (output: Models['OutputFormat_type']) => {
await page.getByRole('button', { name: 'Export Model' }).click()
const exportSelect = page.getByTestId('export-type')
@ -220,10 +210,10 @@ const part001 = startSketchOn('-XZ')
const downloadPromise = page.waitForEvent('download')
await page.getByRole('button', { name: 'Export', exact: true }).click()
const download = await downloadPromise
const downloadLocationer = (extra = '', isImage = false) =>
const downloadLocationer = (extra = '') =>
`./e2e/playwright/export-snapshots/${output.type}-${
'storage' in output ? output.storage : ''
}${extra}.${isImage ? 'png' : output.type}`
}${extra}.${output.type}`
const downloadLocation = downloadLocationer()
const downloadLocation2 = downloadLocationer('-2')
@ -259,11 +249,6 @@ const part001 = startSketchOn('-XZ')
)
await fsp.writeFile(downloadLocation, newFileContents)
}
return {
modelPath: downloadLocation,
imagePath: downloadLocationer('', true),
outputType: output.type,
}
}
const axisDirectionPair: Models['AxisDirectionPair_type'] = {
axis: 'z',
@ -273,116 +258,67 @@ const part001 = startSketchOn('-XZ')
forward: axisDirectionPair,
up: axisDirectionPair,
}
const exportLocations: Paths[] = []
// NOTE it was easiest to leverage existing types and have doExport take Models['OutputFormat_type'] as in input
// just note that only `type` and `storage` are used for selecting the drop downs is the app
// the rest are only there to make typescript happy
exportLocations.push(
await doExport({
type: 'step',
coords: sysType,
})
)
exportLocations.push(
await doExport({
type: 'ply',
coords: sysType,
selection: { type: 'default_scene' },
storage: 'ascii',
units: 'in',
})
)
exportLocations.push(
await doExport({
type: 'ply',
storage: 'binary_little_endian',
coords: sysType,
selection: { type: 'default_scene' },
units: 'in',
})
)
exportLocations.push(
await doExport({
type: 'ply',
storage: 'binary_big_endian',
coords: sysType,
selection: { type: 'default_scene' },
units: 'in',
})
)
exportLocations.push(
await doExport({
type: 'stl',
storage: 'ascii',
coords: sysType,
units: 'in',
selection: { type: 'default_scene' },
})
)
exportLocations.push(
await doExport({
type: 'stl',
storage: 'binary',
coords: sysType,
units: 'in',
selection: { type: 'default_scene' },
})
)
exportLocations.push(
await doExport({
// obj seems to be a little flaky, times out tests sometimes
type: 'obj',
coords: sysType,
units: 'in',
})
)
exportLocations.push(
await doExport({
type: 'gltf',
storage: 'embedded',
presentation: 'pretty',
})
)
exportLocations.push(
await doExport({
type: 'gltf',
storage: 'binary',
presentation: 'pretty',
})
)
// TODO: gltfs don't seem to work with snap shots. push onto exportLocations once it's figured out
await doExport({
type: 'step',
coords: sysType,
})
await doExport({
type: 'gltf',
storage: 'embedded',
presentation: 'pretty',
})
await doExport({
type: 'gltf',
storage: 'binary',
presentation: 'pretty',
})
await doExport({
type: 'gltf',
storage: 'standard',
presentation: 'pretty',
})
// close page to disconnect websocket since we can only have one open atm
await page.close()
// snapshot exports, good compromise to capture that exports are healthy without getting bogged down in "did the formatting change" changes
// context: https://github.com/KittyCAD/modeling-app/issues/1222
for (const { modelPath, imagePath, outputType } of exportLocations) {
const cliCommand = `export KITTYCAD_TOKEN=${secrets.snapshottoken} && kittycad file snapshot --output-format=png --src-format=${outputType} ${modelPath} ${imagePath}`
const child = spawn(cliCommand, { shell: true })
await new Promise((resolve, reject) => {
child.on('error', (code: any, msg: any) => {
console.log('error', code, msg)
reject()
})
child.on('exit', (code, msg) => {
console.log('exit', code, msg)
if (code !== 0) {
reject(`exit code ${code} for model ${modelPath}`)
} else {
resolve(true)
}
})
child.stderr.on('data', (data) => console.log(`stderr: ${data}`))
child.stdout.on('data', (data) => console.log(`stdout: ${data}`))
})
}
await doExport({
type: 'ply',
coords: sysType,
selection: { type: 'default_scene' },
storage: 'ascii',
units: 'in',
})
await doExport({
type: 'ply',
storage: 'binary_little_endian',
coords: sysType,
selection: { type: 'default_scene' },
units: 'in',
})
await doExport({
type: 'ply',
storage: 'binary_big_endian',
coords: sysType,
selection: { type: 'default_scene' },
units: 'in',
})
await doExport({
type: 'stl',
storage: 'ascii',
coords: sysType,
units: 'in',
selection: { type: 'default_scene' },
})
await doExport({
type: 'stl',
storage: 'binary',
coords: sysType,
units: 'in',
selection: { type: 'default_scene' },
})
await doExport({
// obj seems to be a little flaky, times out tests sometimes
type: 'obj',
coords: sysType,
units: 'in',
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 55 KiB

View File

@ -1,26 +1,19 @@
import { browser, $, expect } from '@wdio/globals'
import fs from 'fs/promises'
const defaultDir = `${process.env.HOME}/Documents/zoo-modeling-app-projects`
const userCodeDir = '/tmp/kittycad_user_code'
async function click(element: WebdriverIO.Element): Promise<void> {
// Workaround for .click(), see https://github.com/tauri-apps/tauri/issues/6541
await element.waitForClickable()
await browser.execute('arguments[0].click();', element)
}
describe('ZMA (Tauri, Linux)', () => {
it('opens the auth page and signs in', async () => {
// Clean up filesystem from previous tests
describe('KCMA (Tauri, Linux)', () => {
it('opens the auth page, signs in, and signs out', async () => {
// Clean up previous tests
await new Promise((resolve) => setTimeout(resolve, 100))
await fs.rm(defaultDir, { force: true, recursive: true })
await fs.rm(userCodeDir, { force: true })
await fs.rm('/tmp/kittycad_user_code', { force: true })
await browser.execute('window.localStorage.clear()')
const signInButton = await $('[data-testid="sign-in-button"]')
expect(await signInButton.getText()).toEqual('Sign in')
await click(signInButton)
// Workaround for .click(), see https://github.com/tauri-apps/tauri/issues/6541
await signInButton.waitForClickable()
await browser.execute('arguments[0].click();', signInButton)
await new Promise((resolve) => setTimeout(resolve, 2000))
// Get from main.rs
@ -56,51 +49,14 @@ describe('ZMA (Tauri, Linux)', () => {
// Now should be signed in
const newFileButton = await $('[data-testid="home-new-file"]')
expect(await newFileButton.getText()).toEqual('New file')
})
it('opens the settings page, checks filesystem settings, and closes the settings page', async () => {
// So let's sign out!
const menuButton = await $('[data-testid="user-sidebar-toggle"]')
await click(menuButton)
const settingsButton = await $('[data-testid="settings-button"]')
await click(settingsButton)
const defaultDirInput = await $('[data-testid="default-directory-input"]')
expect(await defaultDirInput.getValue()).toEqual(defaultDir)
const nameInput = await $('[data-testid="name-input"]')
expect(await nameInput.getValue()).toEqual('project-$nnn')
const closeButton = await $('[data-testid="close-button"]')
await click(closeButton)
})
it('checks that no file exists, creates a new file', async () => {
const homeSection = await $('[data-testid="home-section"]')
expect(await homeSection.getText()).toContain('No Projects found')
const newFileButton = await $('[data-testid="home-new-file"]')
await click(newFileButton)
await new Promise((resolve) => setTimeout(resolve, 1000))
expect(await homeSection.getText()).toContain('project-000')
})
it('opens the new file and expects an error on Linux', async () => {
const projectLink = await $('[data-testid="project-link"]')
await click(projectLink)
const error = await $('h3')
expect(await error.getText()).toContain(
"Can't find variable: RTCPeerConnection"
)
await browser.execute('window.location.href = "tauri://localhost/home"')
})
it('signs out', async () => {
const menuButton = await $('[data-testid="user-sidebar-toggle"]')
await click(menuButton)
await menuButton.waitForClickable()
await browser.execute('arguments[0].click();', menuButton)
const signoutButton = await $('[data-testid="user-sidebar-sign-out"]')
await click(signoutButton)
await signoutButton.waitForClickable()
await browser.execute('arguments[0].click();', signoutButton)
const newSignInButton = await $('[data-testid="sign-in-button"]')
expect(await newSignInButton.getText()).toEqual('Sign in')
})

View File

@ -7,17 +7,12 @@
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="An open-source CAD modeling tool from the future by Zoo."
content="An open-source CAD modeling tool from the future by KittyCAD."
/>
<link rel="apple-touch-icon" href="/logo192.png" />
<link rel="manifest" href="/manifest.json" />
<link rel="stylesheet" href="https://use.typekit.net/zzv8rvm.css" />
<script
defer
data-domain="app.zoo.dev"
src="https://plausible.corp.zoo.dev/js/script.js"
></script>
<title>Zoo Modeling App</title>
<script defer data-domain="app.kittycad.io" src="https://plausible.corp.kittycad.io/js/script.js"></script>
<title>KittyCAD Modeling App</title>
</head>
<body class="body-bg">
<noscript>You need to enable JavaScript to run this app.</noscript>

View File

@ -1,6 +1,6 @@
{
"name": "untitled-app",
"version": "0.14.0",
"version": "0.13.0",
"private": true,
"dependencies": {
"@codemirror/autocomplete": "^6.10.2",
@ -60,7 +60,6 @@
},
"scripts": {
"start": "vite",
"start:prod": "vite preview --port=3000",
"serve": "vite serve --port=3000",
"build": "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && source \"$HOME/.cargo/env\" && curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh -s -- -y && yarn build:wasm && vite build",
"build:local": "vite build",
@ -136,7 +135,7 @@
"prettier": "^2.8.0",
"setimmediate": "^1.0.5",
"tailwindcss": "^3.3.6",
"vite": "^4.5.2",
"vite": "^4.5.0",
"vite-plugin-eslint": "^1.8.1",
"vite-tsconfig-paths": "^4.2.1",
"wait-on": "^7.2.0",

View File

@ -4,9 +4,9 @@
First off, thank you so much for your interest in being a part of the closed Alpha program! We are thrilled to have others use our product and see what you build with it (and truthfully, how you break it too).
### Zoo Modeling App (ZMA)
### KittyCAD Modeling App (KCMA)
What we are introducing to you is our Zoo Modeling App (ZMA). ZMA is a CAD application that expresses a hybrid style of traditional CAD interface along with a code-CAD interface. ZMA is a great way for us to test our own APIs as well as inspire others to develop their own applications.
What we are introducing to you is our KittyCAD Modeling App (KCMA). KCMA is a CAD application that expresses a hybrid style of traditional CAD interface along with a code-CAD interface. KCMA is a great way for us to test our own APIs as well as inspire others to develop their own applications.
### Why Code?
@ -18,11 +18,11 @@ Plenty of you have professional CAD experience, and may not understand why codin
- Reproducibility
- Easier integration with other tools
### Before You Use ZMA
### Before You Use KCMA
Before you dive straight into the app, we wanted to lay some expectations out for you.
- ZMA is in early development. Kurt pitched the idea back in January, and the team has been working hard on it since then. ZMA has really basic CAD features for now, but we have plenty of features on our roadmap. Most of the features that you may be currently used to in your CAD workflow today will be available down the road.
- KCMA is in early development. Kurt pitched the idea back in January, and the team has been working hard on it since then. KCMA has really basic CAD features for now, but we have plenty of features on our roadmap. Most of the features that you may be currently used to in your CAD workflow today will be available down the road.
- For a list of all scripting functions, please reference our [documentation](https://github.com/KittyCAD/modeling-app/blob/main/docs/kcl/std.md). For a basic rundown of our types, please reference [this document](https://github.com/KittyCAD/modeling-app/blob/main/docs/kcl/types.md).
- With that being said, we have created an external new features list in [GH Discussions](https://github.com/KittyCAD/modeling-app/discussions). For our current priority list, please click [here](https://github.com/KittyCAD/modeling-app/blob/main/public/roadmap.md). Please upvote any features in the GH Discussions page that you would like to see implemented first. We will prioritize the highest upvoted items or items that are foundational for other features on the list. You can also add your own, but we will review it to make sure its not a duplicate or its feasible for the current state of the app.
- Please report any and all bugs/issues you find. Even the smallest bugs are important! You can report them in a GH Issue [here](https://github.com/KittyCAD/modeling-app/issues/new). You are more than welcome to link your GH Issue in the **bugs** section of our Discord, but if you want to discuss the bug further, please keep that in the GH Issue thread. Please include the severity of the bug in your GH Issue ticket (High, Medium, or Low). If you are having trouble deciding what severity the bug is, use this guideline:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

View File

@ -1,6 +1,6 @@
{
"short_name": "ZMA",
"name": "Zoo Modeling App",
"short_name": "KCMA",
"name": "KittyCAD Modeling App",
"icons": [
{
"src": "favicon.ico",

View File

@ -1,4 +1,4 @@
## Zoo Modeling App Roadmap
## KittyCAD Modeling App Roadmap
This document ties into our [GH Discussions Feature List](https://github.com/KittyCAD/modeling-app/discussions). Please upvote any features that you want to see next, or add ones that are not listed and we will review.

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 13 KiB

View File

@ -1,7 +0,0 @@
<svg width="438" height="145" viewBox="0 0 438 145" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M88.2136 25.3021V3.12744H0.595269V34.3994H79.827L0.609484 120.312H0.595269V120.326L0.581055 120.34L0.595269 120.355V141.364H20.8936L41.3341 119.189V141.364H128.952V110.092H49.7349L128.952 24.1649V3.12744L108.64 3.15587L88.2136 25.3021Z" fill="white"/>
<path d="M167.36 72.4372C167.36 49.7366 185.824 31.2719 208.525 31.2719C216.514 31.2719 223.976 33.5605 230.288 37.5121L251.78 14.3709C239.698 5.34466 224.73 0 208.525 0C168.582 0 136.088 32.4944 136.088 72.4372C136.088 90.5465 142.769 107.135 153.828 119.857L175.32 96.7156C170.316 89.9069 167.36 81.5061 167.36 72.4372Z" fill="white"/>
<path d="M241.745 48.1442C246.734 54.9671 249.691 63.3679 249.691 72.4368C249.691 95.1232 231.226 113.588 208.525 113.588C200.537 113.588 193.088 111.299 186.777 107.348L165.271 130.503C177.353 139.515 192.321 144.86 208.525 144.86C248.468 144.86 280.963 112.365 280.963 72.4368C280.963 54.3133 274.282 37.7249 263.223 25.0029L241.745 48.1442Z" fill="white"/>
<path d="M419.312 25.0029L397.834 48.1442C402.823 54.9671 405.779 63.3679 405.779 72.4368C405.779 95.1232 387.315 113.588 364.614 113.588C356.626 113.588 349.177 111.299 342.866 107.348L321.359 130.503C333.442 139.515 348.41 144.86 364.614 144.86C404.557 144.86 437.051 112.365 437.051 72.4368C437.051 54.3133 430.371 37.7249 419.312 25.0029Z" fill="white"/>
<path d="M323.449 72.4372C323.449 49.7366 341.913 31.2719 364.614 31.2719C372.603 31.2719 380.065 33.5605 386.376 37.5121L407.869 14.3709C395.786 5.34466 380.819 0 364.614 0C324.671 0 292.177 32.4944 292.177 72.4372C292.177 90.5465 298.858 107.135 309.916 119.857L331.409 96.7156C326.405 89.9069 323.449 81.5061 323.449 72.4372Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

162
src-tauri/Cargo.lock generated
View File

@ -547,12 +547,12 @@ dependencies = [
[[package]]
name = "ctor"
version = "0.2.6"
version = "0.1.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30d2b3721e861707777e3195b0158f950ae6dc4a27e4d02ff9f67e3eb3de199e"
checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096"
dependencies = [
"quote",
"syn 2.0.33",
"syn 1.0.109",
]
[[package]]
@ -1242,9 +1242,9 @@ dependencies = [
[[package]]
name = "h2"
version = "0.3.24"
version = "0.3.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9"
checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049"
dependencies = [
"bytes",
"fnv",
@ -1252,7 +1252,7 @@ dependencies = [
"futures-sink",
"futures-util",
"http",
"indexmap 2.0.0",
"indexmap 1.9.3",
"slab",
"tokio",
"tokio-util",
@ -1530,9 +1530,9 @@ dependencies = [
[[package]]
name = "infer"
version = "0.13.0"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f551f8c3a39f68f986517db0d1759de85881894fdc7db798bd2a9df9cb04b7fc"
checksum = "a898e4b7951673fce96614ce5751d13c40fc5674bc2d759288e46c3ab62598b3"
dependencies = [
"cfb",
]
@ -1652,9 +1652,9 @@ dependencies = [
[[package]]
name = "json-patch"
version = "1.2.0"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55ff1e1486799e3f64129f8ccad108b38290df9cd7015cd31bed17239f0789d6"
checksum = "1f54898088ccb91df1b492cc80029a6fdf1c48ca0db7c6822a8babad69c94658"
dependencies = [
"serde",
"serde_json",
@ -2194,11 +2194,11 @@ dependencies = [
[[package]]
name = "openssl"
version = "0.10.61"
version = "0.10.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b8419dc8cc6d866deb801274bba2e6f8f6108c1bb7fcc10ee5ab864931dbb45"
checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d"
dependencies = [
"bitflags 2.4.0",
"bitflags 1.3.2",
"cfg-if",
"foreign-types",
"libc",
@ -2226,9 +2226,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-sys"
version = "0.9.97"
version = "0.9.90"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3eaad34cdd97d81de97964fc7f29e2d104f483840d906ef56daa1912338460b"
checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6"
dependencies = [
"cc",
"libc",
@ -2400,17 +2400,9 @@ version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259"
dependencies = [
"phf_macros 0.10.0",
"phf_shared 0.10.0",
]
[[package]]
name = "phf"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc"
dependencies = [
"phf_macros 0.11.2",
"phf_shared 0.11.2",
"proc-macro-hack",
]
[[package]]
@ -2453,16 +2445,6 @@ dependencies = [
"rand 0.8.5",
]
[[package]]
name = "phf_generator"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0"
dependencies = [
"phf_shared 0.11.2",
"rand 0.8.5",
]
[[package]]
name = "phf_macros"
version = "0.8.0"
@ -2479,15 +2461,16 @@ dependencies = [
[[package]]
name = "phf_macros"
version = "0.11.2"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b"
checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0"
dependencies = [
"phf_generator 0.11.2",
"phf_shared 0.11.2",
"phf_generator 0.10.0",
"phf_shared 0.10.0",
"proc-macro-hack",
"proc-macro2",
"quote",
"syn 2.0.33",
"syn 1.0.109",
]
[[package]]
@ -2508,15 +2491,6 @@ dependencies = [
"siphasher",
]
[[package]]
name = "phf_shared"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b"
dependencies = [
"siphasher",
]
[[package]]
name = "phonenumber"
version = "0.3.3+8.13.9"
@ -3760,9 +3734,9 @@ dependencies = [
[[package]]
name = "tauri"
version = "1.5.3"
version = "1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32d563b672acde8d0cc4c1b1f5b855976923f67e8d6fe1eba51df0211e197be2"
checksum = "9bfe673cf125ef364d6f56b15e8ce7537d9ca7e4dae1cf6fbbdeed2e024db3d9"
dependencies = [
"anyhow",
"bytes",
@ -3857,9 +3831,9 @@ dependencies = [
[[package]]
name = "tauri-macros"
version = "1.4.2"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acea6445eececebd72ed7720cfcca46eee3b5bad8eb408be8f7ef2e3f7411500"
checksum = "613740228de92d9196b795ac455091d3a5fbdac2654abb8bb07d010b62ab43af"
dependencies = [
"heck 0.4.1",
"proc-macro2",
@ -3904,9 +3878,9 @@ dependencies = [
[[package]]
name = "tauri-runtime-wry"
version = "0.14.2"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "803a01101bc611ba03e13329951a1bde44287a54234189b9024b78619c1bc206"
checksum = "8141d72b6b65f2008911e9ef5b98a68d1e3413b7a1464e8f85eb3673bb19a895"
dependencies = [
"cocoa",
"gtk",
@ -3924,9 +3898,9 @@ dependencies = [
[[package]]
name = "tauri-utils"
version = "1.5.1"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a52165bb340e6f6a75f1f5eeeab1bb49f861c12abe3a176067d53642b5454986"
checksum = "34d55e185904a84a419308d523c2c6891d5e2dbcee740c4997eb42e75a7b0f46"
dependencies = [
"brotli",
"ctor",
@ -3939,7 +3913,7 @@ dependencies = [
"kuchikiki",
"log",
"memchr",
"phf 0.11.2",
"phf 0.10.1",
"proc-macro2",
"quote",
"semver",
@ -3949,7 +3923,7 @@ dependencies = [
"thiserror",
"url",
"walkdir",
"windows-version",
"windows 0.39.0",
]
[[package]]
@ -4773,36 +4747,12 @@ dependencies = [
"windows_x86_64_msvc 0.48.0",
]
[[package]]
name = "windows-targets"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd"
dependencies = [
"windows_aarch64_gnullvm 0.52.0",
"windows_aarch64_msvc 0.52.0",
"windows_i686_gnu 0.52.0",
"windows_i686_msvc 0.52.0",
"windows_x86_64_gnu 0.52.0",
"windows_x86_64_gnullvm 0.52.0",
"windows_x86_64_msvc 0.52.0",
]
[[package]]
name = "windows-tokens"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f838de2fe15fe6bac988e74b798f26499a8b21a9d97edec321e79b28d1d7f597"
[[package]]
name = "windows-version"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75aa004c988e080ad34aff5739c39d0312f4684699d6d71fc8a198d057b8b9b4"
dependencies = [
"windows-targets 0.52.0",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.42.2"
@ -4815,12 +4765,6 @@ version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea"
[[package]]
name = "windows_aarch64_msvc"
version = "0.37.0"
@ -4845,12 +4789,6 @@ version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef"
[[package]]
name = "windows_i686_gnu"
version = "0.37.0"
@ -4875,12 +4813,6 @@ version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241"
[[package]]
name = "windows_i686_gnu"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313"
[[package]]
name = "windows_i686_msvc"
version = "0.37.0"
@ -4905,12 +4837,6 @@ version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00"
[[package]]
name = "windows_i686_msvc"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a"
[[package]]
name = "windows_x86_64_gnu"
version = "0.37.0"
@ -4935,12 +4861,6 @@ version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.2"
@ -4953,12 +4873,6 @@ version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e"
[[package]]
name = "windows_x86_64_msvc"
version = "0.37.0"
@ -4983,12 +4897,6 @@ version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
[[package]]
name = "winnow"
version = "0.5.15"
@ -5020,9 +4928,9 @@ dependencies = [
[[package]]
name = "wry"
version = "0.24.6"
version = "0.24.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64a70547e8f9d85da0f5af609143f7bde3ac7457a6e1073104d9b73d6c5ac744"
checksum = "88ef04bdad49eba2e01f06e53688c8413bd6a87b0bc14b72284465cf96e3578e"
dependencies = [
"base64 0.13.1",
"block",

View File

@ -20,7 +20,7 @@ kittycad = "0.2.42"
oauth2 = "4.4.2"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tauri = { version = "1.5.3", features = [ "os-all", "dialog-all", "fs-all", "http-request", "path-all", "shell-open", "shell-open-api", "devtools"] }
tauri = { version = "1.5.2", features = [ "os-all", "dialog-all", "fs-all", "http-request", "path-all", "shell-open", "shell-open-api", "devtools"] }
tauri-plugin-fs-extra = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
tokio = { version = "1.34.0", features = ["time"] }
toml = "0.8.2"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 122 KiB

View File

@ -76,13 +76,16 @@ async fn login(app: tauri::AppHandle, host: &str) -> Result<String, InvokeError>
// Here we're using an env var to enable the /tmp file (windows not supported for now)
// and bypass the shell::open call as it fails on GitHub Actions.
let e2e_tauri_enabled = env::var("E2E_TAURI_ENABLED").is_ok();
if e2e_tauri_enabled {
if (e2e_tauri_enabled) {
println!(
"E2E_TAURI_ENABLED is set, won't open {} externally",
auth_uri.secret()
);
fs::write("/tmp/kittycad_user_code", details.user_code().secret())
.expect("Unable to write /tmp/kittycad_user_code file");
fs::write(
"/tmp/kittycad_user_code",
details.user_code().secret().to_string(),
)
.expect("Unable to write /tmp/kittycad_user_code file");
} else {
tauri::api::shell::open(&app.shell_scope(), auth_uri.secret(), None)
.map_err(|e| InvokeError::from_anyhow(e.into()))?;

View File

@ -6,8 +6,8 @@
"distDir": "../build"
},
"package": {
"productName": "zoo-modeling-app",
"version": "0.14.0"
"productName": "kittycad-modeling",
"version": "0.13.0"
},
"tauri": {
"allowlist": {
@ -84,7 +84,7 @@
"fullscreen": false,
"height": 1200,
"resizable": true,
"title": "Zoo Modeling App",
"title": "KittyCAD Modeling",
"width": 1800
}
]

View File

@ -1,6 +1,6 @@
{
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"package": {
"productName": "Zoo Modeling App"
"productName": "KittyCAD Modeling"
}
}

View File

@ -4,7 +4,7 @@
"updater": {
"active": true,
"endpoints": [
"https://dl.zoo.dev/releases/modeling-app/last_update.json"
"https://dl.kittycad.io/releases/modeling-app/last_update.json"
],
"dialog": true,
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEUzNzA4MjBEQjFBRTY4NzYKUldSMmFLNnhEWUp3NCtsT21Jd05wQktOaGVkOVp6MUFma0hNTDRDSnI2RkJJTEZOWG1ncFhqcU8K"

View File

@ -1,6 +1,6 @@
{
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"package": {
"productName": "Zoo Modeling App"
"productName": "KittyCAD Modeling"
}
}

View File

@ -8,7 +8,7 @@ import {
createRoutesFromElements,
} from 'react-router-dom'
import { GlobalStateProvider } from './components/GlobalStateProvider'
import CommandBarProvider from 'components/CommandBar/CommandBar'
import CommandBarProvider from 'components/CommandBar'
import ModelingMachineProvider from 'components/ModelingMachineProvider'
import { BROWSER_FILE_NAME } from 'Router'

View File

@ -172,8 +172,11 @@ export function App() {
<ModalContainer />
<Resizable
className={
'pointer-events-none h-full flex flex-col flex-1 z-10 my-5 ml-5 pr-1 transition-opacity transition-duration-75 ' +
+paneOpacity
'h-full flex flex-col flex-1 z-10 my-5 ml-5 pr-1 transition-opacity transition-duration-75 ' +
(buttonDownInStream || onboardingStatus === 'camera'
? ' pointer-events-none '
: ' ') +
paneOpacity
}
defaultSize={{
width: '550px',
@ -185,16 +188,10 @@ export function App() {
maxHeight={'auto'}
handleClasses={{
right:
'hover:bg-chalkboard-10/50 bg-transparent transition-colors duration-75 transition-ease-out delay-100 ' +
(buttonDownInStream || onboardingStatus === 'camera'
? 'pointer-events-none '
: 'pointer-events-auto'),
'hover:bg-liquid-30/40 dark:hover:bg-liquid-10/40 bg-transparent transition-colors duration-100 transition-ease-out delay-100',
}}
>
<div
id="code-pane"
className="h-full flex flex-col justify-between pointer-events-none"
>
<div id="code-pane" className="h-full flex flex-col justify-between">
<CollapsiblePanel
title="Code"
icon={faCode}

View File

@ -8,7 +8,7 @@ export const Auth = ({ children }: React.PropsWithChildren) => {
return isLoggingIn ? (
<Loading>
<span data-testid="initial-load">Loading Modeling App...</span>
<span data-testid="initial-load">Loading KittyCAD Modeling App...</span>
</Loading>
) : (
<>{children}</>

View File

@ -38,7 +38,7 @@ import {
settingsMachine,
} from './machines/settingsMachine'
import { ContextFrom } from 'xstate'
import CommandBarProvider from 'components/CommandBar/CommandBar'
import CommandBarProvider from 'components/CommandBar'
import { TEST, VITE_KC_SENTRY_DSN } from './env'
import * as Sentry from '@sentry/react'
import ModelingMachineProvider from 'components/ModelingMachineProvider'
@ -112,7 +112,6 @@ export type ProjectWithEntryPointMetadata = FileEntry & {
}
export type HomeLoaderData = {
projects: ProjectWithEntryPointMetadata[]
newDefaultDirectory?: string
}
type CreateBrowserRouterArg = Parameters<typeof createBrowserRouter>[0]
@ -260,7 +259,6 @@ const router = createBrowserRouter(
const projectDir = await initializeProjectDirectory(
persistedSettings.defaultDirectory || ''
)
let newDefaultDirectory: string | undefined = undefined
if (projectDir !== persistedSettings.defaultDirectory) {
localStorage.setItem(
SETTINGS_PERSIST_KEY,
@ -269,7 +267,6 @@ const router = createBrowserRouter(
defaultDirectory: projectDir,
})
)
newDefaultDirectory = projectDir
}
const projectsNoMeta = (await readDir(projectDir)).filter(
isProjectDirectory
@ -285,7 +282,6 @@ const router = createBrowserRouter(
return {
projects,
newDefaultDirectory,
}
},
children: [

View File

@ -4,11 +4,9 @@ import { engineCommandManager } from './lang/std/engineConnection'
import { useModelingContext } from 'hooks/useModelingContext'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { ActionButton } from 'components/ActionButton'
import usePlatform from 'hooks/usePlatform'
export const Toolbar = () => {
const platform = usePlatform()
const { commandBarSend } = useCommandsContext()
const { setCommandBarOpen } = useCommandsContext()
const { state, send, context } = useModelingContext()
const toolbarButtonsRef = useRef<HTMLUListElement>(null)
const bgClassName =
@ -179,15 +177,10 @@ export const Toolbar = () => {
<ActionButton
Element="button"
className="text-sm"
onClick={() =>
commandBarSend({
type: 'Find and select command',
data: { name: 'Extrude', ownerMachine: 'modeling' },
})
}
disabled={!state.can('Extrude')}
onClick={() => send('extrude intent')}
disabled={!state.can('extrude intent')}
title={
state.can('Extrude')
state.can('extrude intent')
? 'extrude'
: 'sketches need to be closed, or not already extruded'
}
@ -211,10 +204,10 @@ export const Toolbar = () => {
</menu>
<ActionButton
Element="button"
onClick={() => commandBarSend({ type: 'Open' })}
onClick={() => setCommandBarOpen(true)}
className="rounded-r-full pr-4 self-stretch border-energy-10 hover:border-energy-10 dark:border-chalkboard-80 bg-energy-10/50 hover:bg-energy-10 dark:bg-chalkboard-80 dark:text-energy-10"
>
{platform === 'darwin' ? '⌘K' : 'Ctrl+/'}
K
</ActionButton>
</div>
)

View File

@ -7,7 +7,6 @@ import styles from './AppHeader.module.css'
import { NetworkHealthIndicator } from './NetworkHealthIndicator'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { ActionButton } from './ActionButton'
import usePlatform from 'hooks/usePlatform'
interface AppHeaderProps extends React.PropsWithChildren {
showToolbar?: boolean
@ -23,8 +22,7 @@ export const AppHeader = ({
className = '',
enableMenu = false,
}: AppHeaderProps) => {
const platform = usePlatform()
const { commandBarSend } = useCommandsContext()
const { setCommandBarOpen } = useCommandsContext()
const { auth } = useGlobalStateContext()
const user = auth?.context?.user
@ -33,7 +31,7 @@ export const AppHeader = ({
className={
'w-full grid ' +
styles.header +
' overlaid-panes sticky top-0 z-20 py-1 px-2 bg-chalkboard-10/70 dark:bg-chalkboard-100/50 border-b dark:border-b-2 border-chalkboard-30 dark:border-chalkboard-90 items-center ' +
' overlaid-panes sticky top-0 z-20 py-1 px-5 bg-chalkboard-10/70 dark:bg-chalkboard-100/50 border-b dark:border-b-2 border-chalkboard-30 dark:border-chalkboard-90 items-center ' +
className
}
>
@ -49,12 +47,12 @@ export const AppHeader = ({
) : (
<ActionButton
Element="button"
onClick={() => commandBarSend({ type: 'Open' })}
onClick={() => setCommandBarOpen(true)}
className="text-sm self-center flex items-center w-fit gap-3"
>
Command Palette{' '}
<kbd className="bg-energy-10/50 dark:bg-chalkboard-100 dark:text-energy-10 inline-block px-1 py-0.5 border-energy-10 dark:border-chalkboard-90">
{platform === 'darwin' ? '⌘K' : 'Ctrl+/'}
K
</kbd>
</ActionButton>
)}

View File

@ -24,13 +24,13 @@ export const PanelHeader = ({
}: CollapsiblePanelProps) => {
return (
<summary className={styles.header}>
<div className="flex gap-2 items-center flex-1">
<div className="flex gap-2 align-center items-center flex-1">
<ActionIcon
icon={icon}
className="p-1"
size="sm"
bgClassName={
'dark:!bg-chalkboard-100 group-open:bg-chalkboard-80 dark:group-open:!bg-chalkboard-90 border border-transparent dark:group-open:border-chalkboard-60 rounded-sm ' +
'dark:!bg-chalkboard-100 group-open:bg-chalkboard-80 dark:group-open:!bg-chalkboard-90 group-open:border dark:group-open:border-chalkboard-60 rounded-sm ' +
(iconClassNames?.bg || '')
}
iconClassName={
@ -60,9 +60,7 @@ export const CollapsiblePanel = ({
<details
{...props}
data-testid={detailsTestId}
className={
styles.panel + ' pointer-events-auto group ' + (className || '')
}
className={styles.panel + ' group ' + (className || '')}
>
<PanelHeader
title={title}

View File

@ -0,0 +1,404 @@
import { Combobox, Dialog, Transition } from '@headlessui/react'
import {
Dispatch,
Fragment,
SetStateAction,
createContext,
useEffect,
useRef,
useState,
} from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
import Fuse from 'fuse.js'
import {
Command,
CommandArgument,
CommandArgumentOption,
} from '../lib/commands'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { CustomIcon } from './CustomIcon'
type ComboboxOption = Command | CommandArgumentOption
type CommandArgumentData = [string, any]
export const CommandsContext = createContext(
{} as {
commands: Command[]
addCommands: (commands: Command[]) => void
removeCommands: (commands: Command[]) => void
commandBarOpen: boolean
setCommandBarOpen: Dispatch<SetStateAction<boolean>>
}
)
export const CommandBarProvider = ({
children,
}: {
children: React.ReactNode
}) => {
const [commands, internalSetCommands] = useState([] as Command[])
const [commandBarOpen, setCommandBarOpen] = useState(false)
function sortCommands(a: Command, b: Command) {
if (b.owner === 'auth') return -1
if (a.owner === 'auth') return 1
return a.name.localeCompare(b.name)
}
useEffect(() => console.log('commands updated', commands), [commands])
const addCommands = (newCommands: Command[]) => {
internalSetCommands((prevCommands) =>
[...newCommands, ...prevCommands].sort(sortCommands)
)
}
const removeCommands = (newCommands: Command[]) => {
internalSetCommands((prevCommands) =>
prevCommands
.filter((command) => !newCommands.includes(command))
.sort(sortCommands)
)
}
return (
<CommandsContext.Provider
value={{
commands,
addCommands,
removeCommands,
commandBarOpen,
setCommandBarOpen,
}}
>
{children}
<CommandBar />
</CommandsContext.Provider>
)
}
const CommandBar = () => {
const { commands, commandBarOpen, setCommandBarOpen } = useCommandsContext()
useHotkeys(['meta+k', 'meta+/'], () => {
if (commands?.length === 0) return
setCommandBarOpen(!commandBarOpen)
})
const [selectedCommand, setSelectedCommand] = useState<Command>()
const [commandArguments, setCommandArguments] = useState<CommandArgument[]>(
[]
)
const [commandArgumentData, setCommandArgumentData] = useState<
CommandArgumentData[]
>([])
const [commandArgumentIndex, setCommandArgumentIndex] = useState<number>(0)
function clearState() {
setCommandBarOpen(false)
setSelectedCommand(undefined)
setCommandArguments([])
setCommandArgumentData([])
setCommandArgumentIndex(0)
}
function selectCommand(command: Command) {
console.log('selecting command', command)
if (!('args' in command && command.args?.length)) {
submitCommand({ command })
} else {
setCommandArguments(command.args)
setSelectedCommand(command)
}
}
function stepBack() {
if (!selectedCommand) {
clearState()
} else {
if (commandArgumentIndex === 0) {
setSelectedCommand(undefined)
} else {
setCommandArgumentIndex((prevIndex) => Math.max(0, prevIndex - 1))
}
if (commandArgumentData.length > 0) {
setCommandArgumentData((prevData) => prevData.slice(0, -1))
}
}
}
function appendCommandArgumentData(data: { name: any }) {
const transformedData = [
commandArguments[commandArgumentIndex].name,
data.name,
]
if (commandArgumentIndex + 1 === commandArguments.length) {
submitCommand({
dataArr: [
...commandArgumentData,
transformedData,
] as CommandArgumentData[],
})
} else {
setCommandArgumentData(
(prevData) => [...prevData, transformedData] as CommandArgumentData[]
)
setCommandArgumentIndex((prevIndex) => prevIndex + 1)
}
}
function submitCommand({
command = selectedCommand,
dataArr = commandArgumentData,
}) {
console.log('submitting command', command, dataArr)
if (dataArr.length === 0) {
command?.callback()
} else {
const data = Object.fromEntries(dataArr)
console.log('submitting data', data)
command?.callback(data)
}
setCommandBarOpen(false)
}
function getDisplayValue(command: Command) {
if (
'args' in command &&
command.args &&
command.args?.length > 0 &&
'formatFunction' in command &&
command.formatFunction
) {
command.formatFunction(
command.args.map((c, i) =>
commandArgumentData[i] ? commandArgumentData[i][0] : `<${c.name}>`
)
)
}
return command.name
}
return (
<Transition.Root
show={commandBarOpen || false}
afterLeave={() => clearState()}
as={Fragment}
>
<Dialog
onClose={() => {
setCommandBarOpen(false)
}}
className="fixed inset-0 z-40 overflow-y-auto pb-4 pt-1"
>
<Transition.Child
enter="duration-100 ease-out"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="duration-75 ease-in"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<Dialog.Panel
className="relative w-full max-w-xl py-2 mx-auto border rounded shadow-lg bg-chalkboard-10 dark:bg-chalkboard-100 dark:border-chalkboard-70"
as="div"
>
{!(
commandArguments &&
commandArguments.length &&
selectedCommand
) ? (
<CommandComboBox
options={commands}
handleSelection={selectCommand}
stepBack={stepBack}
/>
) : (
<>
<div className="px-4 text-sm flex flex-wrap gap-2">
<p className="pr-4 flex gap-2 items-center">
{selectedCommand &&
'icon' in selectedCommand &&
selectedCommand.icon && (
<CustomIcon
name={selectedCommand.icon}
className="w-5 h-5"
/>
)}
{getDisplayValue(selectedCommand)}
</p>
{commandArguments.map((arg, i) => (
<p
key={arg.name}
className={`w-fit px-2 py-1 rounded-sm flex gap-2 items-center border ${
i === commandArgumentIndex
? 'bg-energy-10/50 dark:bg-energy-10/20 border-energy-10 dark:border-energy-10'
: 'bg-chalkboard-20/50 dark:bg-chalkboard-80/50 border-chalkboard-20 dark:border-chalkboard-80'
}`}
>
{commandArgumentIndex >= i && commandArgumentData[i] ? (
commandArgumentData[i][1]
) : arg.defaultValue ? (
arg.defaultValue
) : (
<em>{arg.name}</em>
)}
</p>
))}
</div>
<div className="block w-full my-2 h-[1px] bg-chalkboard-20 dark:bg-chalkboard-80" />
<Argument
arg={commandArguments[commandArgumentIndex]}
appendCommandArgumentData={appendCommandArgumentData}
stepBack={stepBack}
/>
</>
)}
</Dialog.Panel>
</Transition.Child>
</Dialog>
</Transition.Root>
)
}
function Argument({
arg,
appendCommandArgumentData,
stepBack,
}: {
arg: CommandArgument
appendCommandArgumentData: Dispatch<SetStateAction<any>>
stepBack: () => void
}) {
const { setCommandBarOpen } = useCommandsContext()
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
if (inputRef.current) {
inputRef.current.focus()
inputRef.current.select()
}
}, [arg, inputRef])
return arg.type === 'select' ? (
<CommandComboBox
options={arg.options}
handleSelection={appendCommandArgumentData}
stepBack={stepBack}
placeholder="Select an option"
/>
) : (
<form
onSubmit={(event) => {
event.preventDefault()
appendCommandArgumentData({ name: inputRef.current?.value })
}}
>
<label className="flex items-center mx-4 my-4">
<span className="px-2 py-1 rounded-l bg-chalkboard-100 dark:bg-chalkboard-80 text-chalkboard-10 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80">
{arg.name}
</span>
<input
ref={inputRef}
className="flex-grow px-2 py-1 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80 !bg-transparent focus:outline-none"
placeholder="Enter a value"
defaultValue={arg.defaultValue}
onKeyDown={(event) => {
if (event.metaKey && event.key === 'k') setCommandBarOpen(false)
if (event.key === 'Backspace' && !event.currentTarget.value) {
stepBack()
}
}}
autoFocus
/>
</label>
</form>
)
}
export default CommandBarProvider
function CommandComboBox({
options,
handleSelection,
stepBack,
placeholder,
}: {
options: ComboboxOption[]
handleSelection: Dispatch<SetStateAction<any>>
stepBack: () => void
placeholder?: string
}) {
const { setCommandBarOpen } = useCommandsContext()
const [query, setQuery] = useState('')
const [filteredOptions, setFilteredOptions] = useState<ComboboxOption[]>()
const defaultOption =
options.find((o) => 'isCurrent' in o && o.isCurrent) || null
const fuse = new Fuse(options, {
keys: ['name', 'description'],
threshold: 0.3,
})
useEffect(() => {
const results = fuse.search(query).map((result) => result.item)
setFilteredOptions(query.length > 0 ? results : options)
}, [query])
return (
<Combobox defaultValue={defaultOption} onChange={handleSelection}>
<div className="flex items-center gap-2 px-4 pb-2 border-solid border-0 border-b border-b-chalkboard-20 dark:border-b-chalkboard-80">
<CustomIcon
name="search"
className="w-5 h-5 bg-energy-10/50 dark:bg-chalkboard-90 dark:text-energy-10"
/>
<Combobox.Input
onChange={(event) => setQuery(event.target.value)}
className="w-full bg-transparent focus:outline-none selection:bg-energy-10/50 dark:selection:bg-energy-10/20 dark:focus:outline-none"
onKeyDown={(event) => {
if (event.metaKey && event.key === 'k') setCommandBarOpen(false)
if (event.key === 'Backspace' && !event.currentTarget.value) {
stepBack()
}
}}
placeholder={
(defaultOption && defaultOption.name) ||
placeholder ||
'Search commands'
}
autoCapitalize="off"
autoComplete="off"
autoCorrect="off"
spellCheck="false"
autoFocus
/>
</div>
<Combobox.Options
static
className="overflow-y-auto max-h-96 cursor-pointer"
>
{filteredOptions?.map((option) => (
<Combobox.Option
key={option.name}
value={option}
className="flex items-center gap-2 px-4 py-1 first:mt-2 last:mb-2 ui-active:bg-energy-10/50 dark:ui-active:bg-chalkboard-90"
>
{'icon' in option && option.icon && (
<CustomIcon
name={option.icon}
className="w-5 h-5 dark:text-energy-10"
/>
)}
<p className="flex-grow">{option.name} </p>
{'isCurrent' in option && option.isCurrent && (
<small className="text-chalkboard-70 dark:text-chalkboard-50">
current
</small>
)}
</Combobox.Option>
))}
</Combobox.Options>
</Combobox>
)
}

View File

@ -1,114 +0,0 @@
import { Combobox } from '@headlessui/react'
import Fuse from 'fuse.js'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { CommandArgumentOption } from 'lib/commandTypes'
import { useEffect, useRef, useState } from 'react'
function CommandArgOptionInput({
options,
argName,
stepBack,
onSubmit,
placeholder,
}: {
options: CommandArgumentOption<unknown>[]
argName: string
stepBack: () => void
onSubmit: (data: unknown) => void
placeholder?: string
}) {
const { commandBarSend, commandBarState } = useCommandsContext()
const inputRef = useRef<HTMLInputElement>(null)
const formRef = useRef<HTMLFormElement>(null)
const [argValue, setArgValue] = useState<(typeof options)[number]['value']>(
options.find((o) => 'isCurrent' in o && o.isCurrent)?.value ||
commandBarState.context.argumentsToSubmit[argName] ||
options[0].value
)
const [query, setQuery] = useState('')
const [filteredOptions, setFilteredOptions] = useState<typeof options>()
const fuse = new Fuse(options, {
keys: ['name', 'description'],
threshold: 0.3,
})
useEffect(() => {
inputRef.current?.focus()
inputRef.current?.select()
}, [inputRef])
useEffect(() => {
const results = fuse.search(query).map((result) => result.item)
setFilteredOptions(query.length > 0 ? results : options)
}, [query])
function handleSelectOption(option: CommandArgumentOption<unknown>) {
setArgValue(option)
onSubmit(option.value)
}
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
onSubmit(argValue)
}
return (
<form id="arg-form" onSubmit={handleSubmit} ref={formRef}>
<Combobox value={argValue} onChange={handleSelectOption} name="options">
<div className="flex items-center mx-4 mt-4 mb-2">
<label
htmlFor="option-input"
className="capitalize px-2 py-1 rounded-l bg-chalkboard-100 dark:bg-chalkboard-80 text-chalkboard-10 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80"
>
{argName}
</label>
<Combobox.Input
id="option-input"
ref={inputRef}
onChange={(event) => setQuery(event.target.value)}
className="flex-grow px-2 py-1 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80 !bg-transparent focus:outline-none"
onKeyDown={(event) => {
if (event.metaKey && event.key === 'k')
commandBarSend({ type: 'Close' })
if (event.key === 'Backspace' && !event.currentTarget.value) {
stepBack()
}
}}
placeholder={
(argValue as CommandArgumentOption<unknown>)?.name ||
placeholder ||
'Select an option for ' + argName
}
autoCapitalize="off"
autoComplete="off"
autoCorrect="off"
spellCheck="false"
autoFocus
/>
</div>
<Combobox.Options
static
className="overflow-y-auto max-h-96 cursor-pointer"
>
{filteredOptions?.map((option) => (
<Combobox.Option
key={option.name}
value={option}
className="flex items-center gap-2 px-4 py-1 first:mt-2 last:mb-2 ui-active:bg-energy-10/50 dark:ui-active:bg-chalkboard-90"
>
<p className="flex-grow">{option.name} </p>
{'isCurrent' in option && option.isCurrent && (
<small className="text-chalkboard-70 dark:text-chalkboard-50">
current
</small>
)}
</Combobox.Option>
))}
</Combobox.Options>
</Combobox>
</form>
)
}
export default CommandArgOptionInput

View File

@ -1,166 +0,0 @@
import { Dialog, Popover, Transition } from '@headlessui/react'
import { Fragment, createContext, useEffect } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { useMachine } from '@xstate/react'
import { commandBarMachine } from 'machines/commandBarMachine'
import { EventFrom, StateFrom } from 'xstate'
import CommandBarArgument from './CommandBarArgument'
import CommandComboBox from '../CommandComboBox'
import { useLocation } from 'react-router-dom'
import CommandBarReview from './CommandBarReview'
type CommandsContextType = {
commandBarState: StateFrom<typeof commandBarMachine>
commandBarSend: (event: EventFrom<typeof commandBarMachine>) => void
}
export const CommandsContext = createContext<CommandsContextType>({
commandBarState: commandBarMachine.initialState,
commandBarSend: () => {},
})
export const CommandBarProvider = ({
children,
}: {
children: React.ReactNode
}) => {
const { pathname } = useLocation()
const [commandBarState, commandBarSend] = useMachine(commandBarMachine, {
guards: {
'Arguments are ready': (context, _) => {
return context.selectedCommand?.args
? context.argumentsToSubmit.length ===
Object.keys(context.selectedCommand.args)?.length
: false
},
'Command has no arguments': (context, _event) => {
return (
!context.selectedCommand?.args ||
Object.keys(context.selectedCommand?.args).length === 0
)
},
},
})
// Close the command bar when navigating
useEffect(() => {
commandBarSend({ type: 'Close' })
}, [pathname])
return (
<CommandsContext.Provider
value={{
commandBarState,
commandBarSend,
}}
>
{children}
<CommandBar />
</CommandsContext.Provider>
)
}
const CommandBar = () => {
const { commandBarState, commandBarSend } = useCommandsContext()
const {
context: { selectedCommand, currentArgument, commands },
} = commandBarState
const isSelectionArgument = currentArgument?.inputType === 'selection'
const WrapperComponent = isSelectionArgument ? Popover : Dialog
useHotkeys(['mod+k', 'mod+/'], () => {
if (commandBarState.context.commands.length === 0) return
if (commandBarState.matches('Closed')) {
commandBarSend({ type: 'Open' })
} else {
commandBarSend({ type: 'Close' })
}
})
function stepBack() {
if (!currentArgument) {
if (commandBarState.matches('Review')) {
const entries = Object.entries(selectedCommand?.args || {})
commandBarSend({
type: commandBarState.matches('Review')
? 'Edit argument'
: 'Change current argument',
data: {
arg: {
name: entries[entries.length - 1][0],
...entries[entries.length - 1][1],
},
},
})
} else {
commandBarSend({ type: 'Deselect command' })
}
} else {
const entries = Object.entries(selectedCommand?.args || {})
const index = entries.findIndex(
([key, _]) => key === currentArgument.name
)
if (index === 0) {
commandBarSend({ type: 'Deselect command' })
} else {
commandBarSend({
type: 'Change current argument',
data: {
arg: { name: entries[index - 1][0], ...entries[index - 1][1] },
},
})
}
}
}
return (
<Transition.Root
show={!commandBarState.matches('Closed') || false}
afterLeave={() => {
if (selectedCommand?.onCancel) selectedCommand.onCancel()
commandBarSend({ type: 'Clear' })
}}
as={Fragment}
>
<WrapperComponent
open={!commandBarState.matches('Closed') || isSelectionArgument}
onClose={() => {
commandBarSend({ type: 'Close' })
}}
className={
'fixed inset-0 z-50 overflow-y-auto pb-4 pt-1 ' +
(isSelectionArgument ? 'pointer-events-none' : '')
}
>
<Transition.Child
enter="duration-100 ease-out"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="duration-75 ease-in"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<WrapperComponent.Panel
className="relative z-50 pointer-events-auto w-full max-w-xl py-2 mx-auto border rounded shadow-lg bg-chalkboard-10 dark:bg-chalkboard-100 dark:border-chalkboard-70"
as="div"
>
{commandBarState.matches('Selecting command') ? (
<CommandComboBox options={commands} />
) : commandBarState.matches('Gathering arguments') ? (
<CommandBarArgument stepBack={stepBack} />
) : (
commandBarState.matches('Review') && (
<CommandBarReview stepBack={stepBack} />
)
)}
</WrapperComponent.Panel>
</Transition.Child>
</WrapperComponent>
</Transition.Root>
)
}
export default CommandBarProvider

View File

@ -1,80 +0,0 @@
import CommandArgOptionInput from './CommandArgOptionInput'
import CommandBarBasicInput from './CommandBarBasicInput'
import CommandBarSelectionInput from './CommandBarSelectionInput'
import { CommandArgument } from 'lib/commandTypes'
import { useCommandsContext } from 'hooks/useCommandsContext'
import CommandBarHeader from './CommandBarHeader'
function CommandBarArgument({ stepBack }: { stepBack: () => void }) {
const { commandBarState, commandBarSend } = useCommandsContext()
const {
context: { currentArgument },
} = commandBarState
function onSubmit(data: unknown) {
if (!currentArgument) return
commandBarSend({
type: 'Submit argument',
data: {
[currentArgument.name]:
currentArgument.inputType === 'number'
? parseFloat((data as string) || '0')
: data,
},
})
}
return (
currentArgument && (
<CommandBarHeader>
<ArgumentInput
arg={currentArgument}
stepBack={stepBack}
onSubmit={onSubmit}
/>
</CommandBarHeader>
)
)
}
export default CommandBarArgument
function ArgumentInput({
arg,
stepBack,
onSubmit,
}: {
arg: CommandArgument<unknown> & { name: string }
stepBack: () => void
onSubmit: (event: any) => void
}) {
switch (arg.inputType) {
case 'options':
return (
<CommandArgOptionInput
options={arg.options}
argName={arg.name}
stepBack={stepBack}
onSubmit={onSubmit}
placeholder="Select an option"
/>
)
case 'selection':
return (
<CommandBarSelectionInput
arg={arg}
stepBack={stepBack}
onSubmit={onSubmit}
/>
)
default:
return (
<CommandBarBasicInput
arg={arg}
stepBack={stepBack}
onSubmit={onSubmit}
/>
)
}
}

View File

@ -1,66 +0,0 @@
import { useCommandsContext } from 'hooks/useCommandsContext'
import { CommandArgument } from 'lib/commandTypes'
import { useEffect, useRef } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
function CommandBarBasicInput({
arg,
stepBack,
onSubmit,
}: {
arg: CommandArgument<unknown> & {
inputType: 'number' | 'string'
name: string
}
stepBack: () => void
onSubmit: (event: unknown) => void
}) {
const { commandBarSend, commandBarState } = useCommandsContext()
useHotkeys('mod + k, mod + /', () => commandBarSend({ type: 'Close' }))
const inputRef = useRef<HTMLInputElement>(null)
const inputType = arg.inputType === 'number' ? 'number' : 'text'
useEffect(() => {
if (inputRef.current) {
inputRef.current.focus()
inputRef.current.select()
}
}, [arg, inputRef])
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
onSubmit(inputRef.current?.value)
}
return (
<form id="arg-form" onSubmit={handleSubmit}>
<label className="flex items-center mx-4 my-4">
<span className="capitalize px-2 py-1 rounded-l bg-chalkboard-100 dark:bg-chalkboard-80 text-chalkboard-10 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80">
{arg.name}
</span>
<input
id="arg-form"
name={inputType}
ref={inputRef}
type={inputType}
required
className="flex-grow px-2 py-1 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80 !bg-transparent focus:outline-none"
placeholder="Enter a value"
defaultValue={
(commandBarState.context.argumentsToSubmit[arg.name] as
| string
| undefined) || (arg.defaultValue as string)
}
onKeyDown={(event) => {
if (event.key === 'Backspace' && !event.currentTarget.value) {
stepBack()
}
}}
autoFocus
/>
</label>
</form>
)
}
export default CommandBarBasicInput

View File

@ -1,171 +0,0 @@
import { useCommandsContext } from 'hooks/useCommandsContext'
import { CustomIcon } from '../CustomIcon'
import React, { useState } from 'react'
import { ActionButton } from '../ActionButton'
import { Selections, getSelectionTypeDisplayText } from 'lib/selections'
import { useHotkeys } from 'react-hotkeys-hook'
function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
const { commandBarState, commandBarSend } = useCommandsContext()
const {
context: { selectedCommand, currentArgument, argumentsToSubmit },
} = commandBarState
const isReviewing = commandBarState.matches('Review')
const [showShortcuts, setShowShortcuts] = useState(false)
useHotkeys(
'alt',
() => setShowShortcuts(true),
{ enableOnFormTags: true, enableOnContentEditable: true },
[showShortcuts]
)
useHotkeys(
'alt',
() => setShowShortcuts(false),
{ keyup: true, enableOnFormTags: true, enableOnContentEditable: true },
[showShortcuts]
)
useHotkeys(
[
'alt+1',
'alt+2',
'alt+3',
'alt+4',
'alt+5',
'alt+6',
'alt+7',
'alt+8',
'alt+9',
'alt+0',
],
(_, b) => {
if (b.keys && !Number.isNaN(parseInt(b.keys[0], 10))) {
if (!selectedCommand?.args) return
const argName = Object.keys(selectedCommand.args)[
parseInt(b.keys[0], 10) - 1
]
const arg = selectedCommand?.args[argName]
commandBarSend({
type: 'Change current argument',
data: { arg: { ...arg, name: argName } },
})
}
},
{ keyup: true, enableOnFormTags: true, enableOnContentEditable: true },
[argumentsToSubmit, selectedCommand]
)
return (
selectedCommand &&
argumentsToSubmit && (
<>
<div className="px-4 text-sm flex gap-4 items-start">
<div className="flex flex-1 flex-wrap gap-2">
<p
data-command-name={selectedCommand?.name}
className="pr-4 flex gap-2 items-center"
>
{selectedCommand &&
'icon' in selectedCommand &&
selectedCommand.icon && (
<CustomIcon name={selectedCommand.icon} className="w-5 h-5" />
)}
{selectedCommand?.name}
</p>
{Object.entries(selectedCommand?.args || {}).map(
([argName, arg], i) => (
<button
disabled={!isReviewing && currentArgument?.name === argName}
onClick={() => {
commandBarSend({
type: isReviewing
? 'Edit argument'
: 'Change current argument',
data: { arg: { ...arg, name: argName } },
})
}}
key={argName}
className={`relative w-fit px-2 py-1 rounded-sm flex gap-2 items-center border ${
argName === currentArgument?.name
? 'disabled:bg-energy-10/50 dark:disabled:bg-energy-10/20 disabled:border-energy-10 dark:disabled:border-energy-10 disabled:text-chalkboard-100 dark:disabled:text-chalkboard-10'
: 'bg-chalkboard-20/50 dark:bg-chalkboard-80/50 border-chalkboard-20 dark:border-chalkboard-80'
}`}
>
{argumentsToSubmit[argName] ? (
arg.inputType === 'selection' ? (
getSelectionTypeDisplayText(
argumentsToSubmit[argName] as Selections
)
) : typeof argumentsToSubmit[argName] === 'object' ? (
JSON.stringify(argumentsToSubmit[argName])
) : (
argumentsToSubmit[argName]
)
) : arg.payload ? (
arg.inputType === 'selection' ? (
getSelectionTypeDisplayText(arg.payload as Selections)
) : typeof arg.payload === 'object' ? (
JSON.stringify(arg.payload)
) : (
arg.payload
)
) : (
<em>{argName}</em>
)}
{showShortcuts && (
<small className="absolute -top-[1px] right-full translate-x-1/2 px-0.5 rounded-sm bg-chalkboard-80 text-chalkboard-10 dark:bg-energy-10 dark:text-chalkboard-100">
<span className="sr-only">Hotkey: </span>
{i + 1}
</small>
)}
</button>
)
)}
</div>
{isReviewing ? <ReviewingButton /> : <GatheringArgsButton />}
</div>
<div className="block w-full my-2 h-[1px] bg-chalkboard-20 dark:bg-chalkboard-80" />
{children}
</>
)
)
}
function ReviewingButton() {
return (
<ActionButton
Element="button"
autoFocus
type="submit"
form="review-form"
className="w-fit !p-0 rounded-sm border !border-chalkboard-100 dark:!border-energy-10 hover:shadow"
icon={{
icon: 'checkmark',
bgClassName:
'p-1 rounded-sm !bg-chalkboard-100 hover:!bg-chalkboard-110 dark:!bg-energy-20 dark:hover:!bg-energy-10',
iconClassName: '!text-energy-10 dark:!text-chalkboard-100',
}}
>
<span className="sr-only">Submit command</span>
</ActionButton>
)
}
function GatheringArgsButton() {
return (
<ActionButton
Element="button"
type="submit"
form="arg-form"
className="w-fit !p-0 rounded-sm"
icon={{
icon: 'arrowRight',
bgClassName: 'p-1 rounded-sm',
}}
>
<span className="sr-only">Continue</span>
</ActionButton>
)
}
export default CommandBarHeader

View File

@ -1,81 +0,0 @@
import { useCommandsContext } from 'hooks/useCommandsContext'
import CommandBarHeader from './CommandBarHeader'
import { useHotkeys } from 'react-hotkeys-hook'
function CommandBarReview({ stepBack }: { stepBack: () => void }) {
const { commandBarState, commandBarSend } = useCommandsContext()
const {
context: { argumentsToSubmit, selectedCommand },
} = commandBarState
useHotkeys('backspace', stepBack, {
enableOnFormTags: true,
enableOnContentEditable: true,
})
useHotkeys(
'1, 2, 3, 4, 5, 6, 7, 8, 9, 0',
(_, b) => {
if (b.keys && !Number.isNaN(parseInt(b.keys[0], 10))) {
if (!selectedCommand?.args) return
const argName = Object.keys(selectedCommand.args)[
parseInt(b.keys[0], 10) - 1
]
const arg = selectedCommand?.args[argName]
commandBarSend({
type: 'Edit argument',
data: { arg: { ...arg, name: argName } },
})
}
},
{ keyup: true, enableOnFormTags: true, enableOnContentEditable: true },
[argumentsToSubmit, selectedCommand]
)
Object.keys(argumentsToSubmit).forEach((key, i) => {
const arg = selectedCommand?.args ? selectedCommand?.args[key] : undefined
if (!arg) return
})
function submitCommand() {
commandBarSend({
type: 'Submit command',
data: argumentsToSubmit,
})
}
return (
<CommandBarHeader>
<p className="px-4">Confirm {selectedCommand?.name}</p>
<form
id="review-form"
className="absolute opacity-0 inset-0 pointer-events-none"
onSubmit={submitCommand}
>
{Object.entries(argumentsToSubmit).map(([key, value], i) => {
const arg = selectedCommand?.args
? selectedCommand?.args[key]
: undefined
if (!arg) return null
return (
<input
id={key}
name={key}
key={key}
type="text"
defaultValue={
typeof value === 'object'
? JSON.stringify(value)
: (value as string)
}
hidden
/>
)
})}
</form>
</CommandBarHeader>
)
}
export default CommandBarReview

View File

@ -1,114 +0,0 @@
import { useSelector } from '@xstate/react'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { useKclContext } from 'lang/KclSinglton'
import { CommandArgument } from 'lib/commandTypes'
import {
ResolvedSelectionType,
canSubmitSelectionArg,
getSelectionType,
getSelectionTypeDisplayText,
} from 'lib/selections'
import { modelingMachine } from 'machines/modelingMachine'
import { useEffect, useRef, useState } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
import { StateFrom } from 'xstate'
const selectionSelector = (snapshot: StateFrom<typeof modelingMachine>) =>
snapshot.context.selectionRanges
function CommandBarSelectionInput({
arg,
stepBack,
onSubmit,
}: {
arg: CommandArgument<unknown> & { inputType: 'selection'; name: string }
stepBack: () => void
onSubmit: (data: unknown) => void
}) {
const { code } = useKclContext()
const inputRef = useRef<HTMLInputElement>(null)
const { commandBarSend } = useCommandsContext()
const [hasSubmitted, setHasSubmitted] = useState(false)
const selection = useSelector(arg.actor, selectionSelector)
const [selectionsByType, setSelectionsByType] = useState<
'none' | ResolvedSelectionType[]
>(
selection.codeBasedSelections[0]?.range[1] === code.length
? 'none'
: getSelectionType(selection)
)
const [canSubmitSelection, setCanSubmitSelection] = useState<boolean>(
canSubmitSelectionArg(selectionsByType, arg)
)
useHotkeys('tab', () => onSubmit(selection), {
enableOnFormTags: true,
enableOnContentEditable: true,
keyup: true,
})
useEffect(() => {
inputRef.current?.focus()
}, [selection, inputRef])
useEffect(() => {
setSelectionsByType(
selection.codeBasedSelections[0]?.range[1] === code.length
? 'none'
: getSelectionType(selection)
)
}, [selection])
useEffect(() => {
setCanSubmitSelection(canSubmitSelectionArg(selectionsByType, arg))
}, [selectionsByType, arg])
function handleChange() {
inputRef.current?.focus()
}
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
if (!canSubmitSelection) {
setHasSubmitted(true)
return
}
onSubmit(selection)
}
return (
<form id="arg-form" onSubmit={handleSubmit}>
<label
className={
'relative flex items-center mx-4 my-4 ' +
(!hasSubmitted || canSubmitSelection || 'text-destroy-50')
}
>
{canSubmitSelection
? getSelectionTypeDisplayText(selection) + ' selected'
: `Please select ${arg.multiple ? 'one or more faces' : 'one face'}`}
<input
id="selection"
name="selection"
ref={inputRef}
required
placeholder="Select an entity with your mouse"
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
onKeyDown={(event) => {
if (event.key === 'Backspace') {
stepBack()
} else if (event.key === 'Escape') {
commandBarSend({ type: 'Close' })
}
}}
onChange={handleChange}
value={JSON.stringify(selection || {})}
/>
</label>
</form>
)
}
export default CommandBarSelectionInput

View File

@ -1,90 +0,0 @@
import { Combobox } from '@headlessui/react'
import Fuse from 'fuse.js'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { Command } from 'lib/commandTypes'
import { useEffect, useState } from 'react'
import { CustomIcon } from './CustomIcon'
function CommandComboBox({
options,
placeholder,
}: {
options: Command[]
placeholder?: string
}) {
const { commandBarSend } = useCommandsContext()
const [query, setQuery] = useState('')
const [filteredOptions, setFilteredOptions] = useState<typeof options>()
const defaultOption =
options.find((o) => 'isCurrent' in o && o.isCurrent) || null
const fuse = new Fuse(options, {
keys: ['name', 'description'],
threshold: 0.3,
})
useEffect(() => {
const results = fuse.search(query).map((result) => result.item)
setFilteredOptions(query.length > 0 ? results : options)
}, [query])
function handleSelection(command: Command) {
commandBarSend({ type: 'Select command', data: { command } })
}
return (
<Combobox defaultValue={defaultOption} onChange={handleSelection}>
<div className="flex items-center gap-2 px-4 pb-2 border-solid border-0 border-b border-b-chalkboard-20 dark:border-b-chalkboard-80">
<CustomIcon
name="search"
className="w-5 h-5 bg-energy-10/50 dark:bg-chalkboard-90 dark:text-energy-10"
/>
<Combobox.Input
onChange={(event) => setQuery(event.target.value)}
className="w-full bg-transparent focus:outline-none selection:bg-energy-10/50 dark:selection:bg-energy-10/20 dark:focus:outline-none"
onKeyDown={(event) => {
if (
(event.metaKey && event.key === 'k') ||
(event.key === 'Backspace' && !event.currentTarget.value)
) {
commandBarSend({ type: 'Close' })
}
}}
placeholder={
(defaultOption && defaultOption.name) ||
placeholder ||
'Search commands'
}
autoCapitalize="off"
autoComplete="off"
autoCorrect="off"
spellCheck="false"
autoFocus
/>
</div>
<Combobox.Options
static
className="overflow-y-auto max-h-96 cursor-pointer"
>
{filteredOptions?.map((option) => (
<Combobox.Option
key={option.name}
value={option}
className="flex items-center gap-2 px-4 py-1 first:mt-2 last:mb-2 ui-active:bg-energy-10/50 dark:ui-active:bg-chalkboard-90"
>
{'icon' in option && option.icon && (
<CustomIcon
name={option.icon}
className="w-5 h-5 dark:text-energy-10"
/>
)}
<p className="flex-grow">{option.name} </p>
</Combobox.Option>
))}
</Combobox.Options>
</Combobox>
)
}
export default CommandComboBox

View File

@ -3,7 +3,6 @@ export type CustomIconName =
| 'arrowLeft'
| 'arrowRight'
| 'arrowUp'
| 'checkmark'
| 'close'
| 'equal'
| 'extrude'
@ -91,22 +90,6 @@ export const CustomIcon = ({
/>
</svg>
)
case 'checkmark':
return (
<svg
{...props}
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M8.29956 13.5388L13.9537 6L14.7537 6.6L8.75367 14.6L8.00012 14.6536L5 11.6536L5.70709 10.9465L8.29956 13.5388Z"
fill="currentColor"
/>
</svg>
)
case 'close':
return (
<svg

View File

@ -10,48 +10,45 @@ const DownloadAppBanner = () => {
return (
<Dialog
className="fixed inset-0 z-50"
className="fixed inset-0 top-auto z-50 bg-warn-20 text-warn-80 px-8 py-4"
open={!isBannerDismissed}
onClose={() => ({})}
>
<Dialog.Overlay className="fixed inset-0 bg-chalkboard-100/50" />
<Dialog.Panel className="absolute inset-0 top-auto bg-warn-20 text-warn-80 px-8 py-4">
<div className="max-w-3xl mx-auto">
<div className="flex gap-2 justify-between items-start">
<h2 className="text-xl font-bold mb-4">
Modeling App is better as a desktop app!
</h2>
<ActionButton
Element="button"
onClick={() => setBannerDismissed(true)}
icon={{
icon: 'close',
className: 'p-1',
bgClassName:
'bg-warn-70 hover:bg-warn-80 dark:bg-warn-70 dark:hover:bg-warn-80',
iconClassName:
'text-warn-10 group-hover:text-warn-10 dark:text-warn-10 dark:group-hover:text-warn-10',
}}
className="!p-0 !bg-transparent !border-transparent"
/>
</div>
<p>
The browser version of the app only saves your data temporarily in{' '}
<code className="text-base inline-block px-0.5 bg-warn-30/50 rounded">
localStorage
</code>
, and isn't backed up anywhere! Visit{' '}
<a
href="https://zoo.dev/modeling-app/download"
rel="noopener noreferrer"
target="_blank"
className="!text-warn-80 dark:!text-warn-80 dark:hover:!text-warn-70 underline"
>
our website
</a>{' '}
to download the app for the best experience.
</p>
<Dialog.Panel className="max-w-3xl mx-auto">
<div className="flex gap-2 justify-between items-start">
<h2 className="text-xl font-bold mb-4">
KittyCAD Modeling App is better as a desktop app!
</h2>
<ActionButton
Element="button"
onClick={() => setBannerDismissed(true)}
icon={{
icon: 'close',
className: 'p-1',
bgClassName:
'bg-warn-70 hover:bg-warn-80 dark:bg-warn-70 dark:hover:bg-warn-80',
iconClassName:
'text-warn-10 group-hover:text-warn-10 dark:text-warn-10 dark:group-hover:text-warn-10',
}}
className="!p-0 !bg-transparent !border-transparent"
/>
</div>
<p>
The browser version of the app only saves your data temporarily in{' '}
<code className="text-base inline-block px-0.5 bg-warn-30/50 rounded">
localStorage
</code>
, and isn't backed up anywhere! Visit{' '}
<a
href="https://kittycad.io/modeling-app/download"
rel="noopener noreferrer"
target="_blank"
className="text-warn-80 dark:text-warn-80 dark:hover:text-warn-70 underline"
>
our website
</a>{' '}
to download the app for the best experience.
</p>
</Dialog.Panel>
</Dialog>
)

View File

@ -40,7 +40,7 @@ export const FileMachineProvider = ({
children: React.ReactNode
}) => {
const navigate = useNavigate()
const { commandBarSend } = useCommandsContext()
const { setCommandBarOpen } = useCommandsContext()
const { project } = useRouteLoaderData(paths.FILE) as IndexLoaderData
const [state, send] = useMachine(fileMachine, {
@ -54,7 +54,7 @@ export const FileMachineProvider = ({
event: EventFrom<typeof fileMachine>
) => {
if (event.data && 'name' in event.data) {
commandBarSend({ type: 'Close' })
setCommandBarOpen(false)
navigate(
`${paths.FILE}/${encodeURIComponent(
context.selectedDirectory + sep + event.data.name

View File

@ -1,11 +1,19 @@
import { useMachine } from '@xstate/react'
import { useNavigate } from 'react-router-dom'
import { paths } from '../Router'
import { authMachine, TOKEN_PERSIST_KEY } from '../machines/authMachine'
import {
authCommandBarConfig,
authMachine,
TOKEN_PERSIST_KEY,
} from '../machines/authMachine'
import withBaseUrl from '../lib/withBaseURL'
import React, { createContext, useEffect, useRef } from 'react'
import useStateMachineCommands from '../hooks/useStateMachineCommands'
import { SETTINGS_PERSIST_KEY, settingsMachine } from 'machines/settingsMachine'
import {
SETTINGS_PERSIST_KEY,
settingsCommandBarConfig,
settingsMachine,
} from 'machines/settingsMachine'
import { toast } from 'react-hot-toast'
import { setThemeClass, Themes } from 'lib/theme'
import {
@ -15,9 +23,8 @@ import {
Prop,
StateFrom,
} from 'xstate'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { isTauri } from 'lib/isTauri'
import { settingsCommandBarConfig } from 'lib/commandBarConfigs/settingsCommandConfig'
import { authCommandBarConfig } from 'lib/commandBarConfigs/authCommandConfig'
type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T>
@ -38,6 +45,7 @@ export const GlobalStateProvider = ({
children: React.ReactNode
}) => {
const navigate = useNavigate()
const { commands } = useCommandsContext()
// Settings machine setup
const retrievedSettings = useRef(
@ -73,9 +81,10 @@ export const GlobalStateProvider = ({
})
useStateMachineCommands({
machineId: 'settings',
state: settingsState,
send: settingsSend,
commands,
owner: 'settings',
commandBarConfig: settingsCommandBarConfig,
})
@ -112,10 +121,11 @@ export const GlobalStateProvider = ({
})
useStateMachineCommands({
machineId: 'auth',
state: authState,
send: authSend,
commands,
commandBarConfig: authCommandBarConfig,
owner: 'auth',
})
return (

View File

@ -15,23 +15,23 @@ const Loading = ({ children }: React.PropsWithChildren) => {
data-testid="loading"
>
<svg viewBox="0 0 10 10" className="w-8 h-8">
<circle cx="5" cy="5" r="4" stroke="var(--energy-50)" fill="none" />
<circle cx="5" cy="5" r="4" stroke="var(--liquid-20)" fill="none" />
<circle
cx="5"
cy="5"
r="4"
stroke="var(--energy-10)"
stroke="var(--liquid-10)"
fill="none"
strokeDasharray="4, 4"
className="animate-spin origin-center"
/>
</svg>
<p className="text-base mt-4 text-energy-80 dark:text-energy-30">
<p className="text-base mt-4 text-liquid-80 dark:text-liquid-20">
{children || 'Loading'}
</p>
<p
className={
'text-sm mt-4 text-energy-70 dark:text-energy-50 transition-opacity duration-500' +
'text-sm mt-4 text-liquid-90 dark:text-liquid-10 transition-opacity duration-500' +
(hasLongLoadTime ? ' opacity-100' : ' opacity-0')
}
>

View File

@ -1,33 +0,0 @@
export const Logo = ({
className = 'w-auto h-5 text-chalkboard-120 dark:text-chalkboard-10',
...props
}: React.SVGProps<SVGSVGElement>) => (
<svg
{...props}
className={className}
viewBox="0 0 438 145"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M88.2136 25.3021V3.12744H0.595269V34.3994H79.827L0.609484 120.312H0.595269V120.326L0.581055 120.34L0.595269 120.355V141.364H20.8936L41.3341 119.189V141.364H128.952V110.092H49.7349L128.952 24.1649V3.12744L108.64 3.15587L88.2136 25.3021Z"
fill="currentColor"
/>
<path
d="M167.36 72.4372C167.36 49.7366 185.824 31.2719 208.525 31.2719C216.514 31.2719 223.976 33.5605 230.288 37.5121L251.78 14.3709C239.698 5.34466 224.73 0 208.525 0C168.582 0 136.088 32.4944 136.088 72.4372C136.088 90.5465 142.769 107.135 153.828 119.857L175.32 96.7156C170.316 89.9069 167.36 81.5061 167.36 72.4372Z"
fill="currentColor"
/>
<path
d="M241.745 48.1442C246.734 54.9671 249.691 63.3679 249.691 72.4368C249.691 95.1232 231.226 113.588 208.525 113.588C200.537 113.588 193.088 111.299 186.777 107.348L165.271 130.503C177.353 139.515 192.321 144.86 208.525 144.86C248.468 144.86 280.963 112.365 280.963 72.4368C280.963 54.3133 274.282 37.7249 263.223 25.0029L241.745 48.1442Z"
fill="currentColor"
/>
<path
d="M419.312 25.0029L397.834 48.1442C402.823 54.9671 405.779 63.3679 405.779 72.4368C405.779 95.1232 387.315 113.588 364.614 113.588C356.626 113.588 349.177 111.299 342.866 107.348L321.359 130.503C333.442 139.515 348.41 144.86 364.614 144.86C404.557 144.86 437.051 112.365 437.051 72.4368C437.051 54.3133 430.371 37.7249 419.312 25.0029Z"
fill="currentColor"
/>
<path
d="M323.449 72.4372C323.449 49.7366 341.913 31.2719 364.614 31.2719C372.603 31.2719 380.065 33.5605 386.376 37.5121L407.869 14.3709C395.786 5.34466 380.819 0 364.614 0C324.671 0 292.177 32.4944 292.177 72.4372C292.177 90.5465 298.858 107.135 309.916 119.857L331.409 96.7156C326.405 89.9069 323.449 81.5061 323.449 72.4372Z"
fill="currentColor"
/>
</svg>
)

View File

@ -29,26 +29,19 @@ import {
addNewSketchLn,
compareVec2Epsilon,
} from 'lang/std/sketch'
import { kclManager, useKclContext } from 'lang/KclSinglton'
import { kclManager } from 'lang/KclSinglton'
import { applyConstraintHorzVertDistance } from './Toolbar/SetHorzVertDistance'
import {
angleBetweenInfo,
applyConstraintAngleBetween,
} from './Toolbar/SetAngleBetween'
import { applyConstraintAngleLength } from './Toolbar/setAngleLength'
import { toast } from 'react-hot-toast'
import { pathMapToSelections } from 'lang/util'
import { useStore } from 'useStore'
import {
canExtrudeSelection,
handleSelectionBatch,
handleSelectionWithShift,
isSelectionLastLine,
isSketchPipe,
} from 'lib/selections'
import { handleSelectionBatch, handleSelectionWithShift } from 'lib/selections'
import { applyConstraintIntersect } from './Toolbar/Intersect'
import { applyConstraintAbsDistance } from './Toolbar/SetAbsDistance'
import useStateMachineCommands from 'hooks/useStateMachineCommands'
import { modelingMachineConfig } from 'lib/commandBarConfigs/modelingCommandConfig'
type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T>
@ -66,7 +59,6 @@ export const ModelingMachineProvider = ({
children: React.ReactNode
}) => {
const { auth } = useGlobalStateContext()
const { code } = useKclContext()
const token = auth?.context?.token
const streamRef = useRef<HTMLDivElement>(null)
useSetupEngineManager(streamRef, token)
@ -76,6 +68,8 @@ export const ModelingMachineProvider = ({
editorView: s.editorView,
}))
// const { commands } = useCommandsContext()
// Settings machine setup
// const retrievedSettings = useRef(
// localStorage?.getItem(MODELING_PERSIST_KEY) || '{}'
@ -89,85 +83,148 @@ export const ModelingMachineProvider = ({
// >
// )
const [modelingState, modelingSend, modelingActor] = useMachine(
modelingMachine,
{
// context: persistedSettings,
actions: {
'Modify AST': () => {},
'Update code selection cursors': () => {},
'show default planes': () => {
kclManager.showPlanes()
const [modelingState, modelingSend] = useMachine(modelingMachine, {
// context: persistedSettings,
actions: {
'Modify AST': () => {},
'Update code selection cursors': () => {},
'show default planes': () => {
kclManager.showPlanes()
},
'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(),
cmd: {
type: 'edit_mode_enter',
target: sketchUuid,
},
})
return sketchUuid
},
'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(),
cmd: {
type: 'edit_mode_enter',
target: sketchUuid,
},
})
return sketchUuid
},
}),
'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 {}
}),
'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: 'start_path',
data: null,
raw: {} as any,
}
if (!segmentId) {
// Something really weird must have happened for this to happen.
console.error('segmentId is undefined for starting a new sketch')
return {}
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,
}
const _addStartSketch = addStartSketch(
kclManager.executeAstMock(astWithUpdatedSource, true)
return {
sketchPathToNode: _pathToNode,
}
}
),
'AST add line segment': async (
{ sketchPathToNode, sketchEnginePathId },
{ data: { coords, segmentId } }
) => {
if (!sketchPathToNode) return
const lastCoord = coords[coords.length - 1]
const pathInfo = await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'path_get_info',
path_id: sketchEnginePathId,
},
})
const firstSegment = pathInfo?.data?.data?.segments.find(
(seg: any) => seg.command === 'line_to'
)
const firstSegCoords = await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'curve_get_control_points',
curve_id: firstSegment.command_id,
},
})
const startPathCoord = firstSegCoords?.data?.data?.control_points[0]
const isClose = compareVec2Epsilon(
[startPathCoord.x, startPathCoord.y],
[lastCoord.x, lastCoord.y]
)
let _modifiedAst: Program
if (!isClose) {
const newSketchLn = addNewSketchLn({
node: kclManager.ast,
programMemory: kclManager.programMemory,
to: [lastCoord.x, lastCoord.y],
from: [coords[0].x, coords[0].y],
fnName: 'line',
pathToNode: sketchPathToNode,
})
const _modifiedAst = newSketchLn.modifiedAst
kclManager.executeAstMock(_modifiedAst, true).then(() => {
const lineCallExp = getNodeFromPath<CallExpression>(
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
newSketchLn.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: 'start_path',
data: null,
raw: {} as any,
}
const lineCallExp = updatedPipeNode.body.find(
(exp) =>
exp.type === 'CallExpression' && exp.callee.name === 'line'
)
if (lineCallExp)
if (segmentId)
engineCommandManager.artifactMap[segmentId] = {
type: 'result',
range: [lineCallExp.start, lineCallExp.end],
@ -176,203 +233,112 @@ export const ModelingMachineProvider = ({
data: null,
raw: {} as any,
}
kclManager.executeAstMock(astWithUpdatedSource, true)
return {
sketchPathToNode: _pathToNode,
}
}
),
'AST add line segment': async (
{ sketchPathToNode, sketchEnginePathId },
{ data: { coords, segmentId } }
) => {
if (!sketchPathToNode) return
const lastCoord = coords[coords.length - 1]
const pathInfo = await engineCommandManager.sendSceneCommand({
})
} else {
_modifiedAst = addCloseToPipe({
node: kclManager.ast,
programMemory: kclManager.programMemory,
pathToNode: sketchPathToNode,
})
engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'path_get_info',
path_id: sketchEnginePathId,
},
cmd: { type: 'edit_mode_exit' },
})
const firstSegment = pathInfo?.data?.data?.segments.find(
(seg: any) => seg.command === 'line_to'
)
const firstSegCoords = await engineCommandManager.sendSceneCommand({
engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'curve_get_control_points',
curve_id: firstSegment.command_id,
},
cmd: { type: 'default_camera_disable_sketch_mode' },
})
const startPathCoord = firstSegCoords?.data?.data?.control_points[0]
kclManager.executeAstMock(_modifiedAst, true)
// updateAst(_modifiedAst, true)
}
},
'sketch exit execute': () => {
kclManager.executeAst()
},
'set tool': () => {}, // TODO
'toast extrude failed': () => {
toast.error(
'Extrude failed, sketches need to be closed, or not already extruded'
)
},
'Set selection': assign(({ selectionRanges }, event) => {
if (event.type !== 'Set selection') return {} // this was needed for ts after adding 'Set selection' action to on done modal events
const setSelections = event.data
if (!editorView) return {}
if (setSelections.selectionType === 'mirrorCodeMirrorSelections')
return { selectionRanges: setSelections.selection }
else if (setSelections.selectionType === 'otherSelection') {
// TODO KittyCAD/engine/issues/1620: send axis highlight when it's working (if that's what we settle on)
// const axisAddCmd: EngineCommand = {
// type: 'modeling_cmd_req',
// cmd: {
// type: 'highlight_set_entities',
// entities: [
// setSelections.selection === 'x-axis'
// ? X_AXIS_UUID
// : Y_AXIS_UUID,
// ],
// },
// cmd_id: uuidv4(),
// }
const isClose = compareVec2Epsilon(
[startPathCoord.x, startPathCoord.y],
[lastCoord.x, lastCoord.y]
)
// if (!isShiftDown) {
// engineCommandManager
// .sendSceneCommand({
// type: 'modeling_cmd_req',
// cmd: {
// type: 'select_clear',
// },
// cmd_id: uuidv4(),
// })
// .then(() => {
// engineCommandManager.sendSceneCommand(axisAddCmd)
// })
// } else {
// engineCommandManager.sendSceneCommand(axisAddCmd)
// }
let _modifiedAst: Program
if (!isClose) {
const newSketchLn = addNewSketchLn({
node: kclManager.ast,
programMemory: kclManager.programMemory,
to: [lastCoord.x, lastCoord.y],
from: [coords[0].x, coords[0].y],
fnName: 'line',
pathToNode: sketchPathToNode,
const {
codeMirrorSelection,
selectionRangeTypeMap,
otherSelections,
} = handleSelectionWithShift({
otherSelection: setSelections.selection,
currentSelections: selectionRanges,
isShiftDown,
})
setTimeout(() => {
editorView.dispatch({
selection: codeMirrorSelection,
})
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,
programMemory: kclManager.programMemory,
pathToNode: sketchPathToNode,
})
engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: { type: 'edit_mode_exit' },
})
engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: { type: 'default_camera_disable_sketch_mode' },
})
kclManager.executeAstMock(_modifiedAst, true)
// updateAst(_modifiedAst, true)
}
},
'sketch exit execute': () => {
kclManager.executeAst()
},
'set tool': () => {}, // TODO
'Set selection': assign(({ selectionRanges }, event) => {
if (event.type !== 'Set selection') return {} // this was needed for ts after adding 'Set selection' action to on done modal events
const setSelections = event.data
if (!editorView) return {}
if (setSelections.selectionType === 'mirrorCodeMirrorSelections')
return { selectionRanges: setSelections.selection }
else if (setSelections.selectionType === 'otherSelection') {
// TODO KittyCAD/engine/issues/1620: send axis highlight when it's working (if that's what we settle on)
// const axisAddCmd: EngineCommand = {
// type: 'modeling_cmd_req',
// cmd: {
// type: 'highlight_set_entities',
// entities: [
// setSelections.selection === 'x-axis'
// ? X_AXIS_UUID
// : Y_AXIS_UUID,
// ],
// },
// cmd_id: uuidv4(),
// }
// if (!isShiftDown) {
// engineCommandManager
// .sendSceneCommand({
// type: 'modeling_cmd_req',
// cmd: {
// type: 'select_clear',
// },
// cmd_id: uuidv4(),
// })
// .then(() => {
// engineCommandManager.sendSceneCommand(axisAddCmd)
// })
// } else {
// engineCommandManager.sendSceneCommand(axisAddCmd)
// }
const {
codeMirrorSelection,
selectionRangeTypeMap,
})
return {
selectionRangeTypeMap,
selectionRanges: {
codeBasedSelections: selectionRanges.codeBasedSelections,
otherSelections,
} = handleSelectionWithShift({
otherSelection: setSelections.selection,
currentSelections: selectionRanges,
isShiftDown,
})
setTimeout(() => {
editorView.dispatch({
selection: codeMirrorSelection,
})
})
return {
selectionRangeTypeMap,
selectionRanges: {
codeBasedSelections: selectionRanges.codeBasedSelections,
otherSelections,
},
}
} else if (setSelections.selectionType === 'singleCodeCursor') {
// 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.
// for more details on how selections see `src/lib/selections.ts`.
const {
codeMirrorSelection,
selectionRangeTypeMap,
otherSelections,
} = handleSelectionWithShift({
codeSelection: setSelections.selection,
currentSelections: selectionRanges,
isShiftDown,
})
if (codeMirrorSelection) {
setTimeout(() => {
editorView.dispatch({
selection: codeMirrorSelection,
})
})
}
if (!setSelections.selection) {
return {
selectionRangeTypeMap,
selectionRanges: {
codeBasedSelections: selectionRanges.codeBasedSelections,
otherSelections,
},
}
}
return {
selectionRangeTypeMap,
selectionRanges: {
codeBasedSelections: selectionRanges.codeBasedSelections,
otherSelections,
},
}
},
}
} else if (setSelections.selectionType === 'singleCodeCursor') {
// This DOES NOT set the `selectionRanges` in xstate context
// same as comment above
const { codeMirrorSelection, selectionRangeTypeMap } =
handleSelectionBatch({
selections: setSelections.selection,
})
// 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.
// for more details on how selections see `src/lib/selections.ts`.
const {
codeMirrorSelection,
selectionRangeTypeMap,
otherSelections,
} = handleSelectionWithShift({
codeSelection: setSelections.selection,
currentSelections: selectionRanges,
isShiftDown,
})
if (codeMirrorSelection) {
setTimeout(() => {
editorView.dispatch({
@ -380,167 +346,180 @@ export const ModelingMachineProvider = ({
})
})
}
return { selectionRangeTypeMap }
}),
},
guards: {
'Selection contains axis': () => true,
'Selection contains edge': () => true,
'Selection contains face': () => true,
'Selection contains line': () => true,
'Selection contains point': () => true,
'Selection is not empty': () => true,
'has valid extrude selection': ({ selectionRanges }) => {
// A user can begin extruding if they either have 1+ faces selected or nothing selected
// TODO: I believe this guard only allows for extruding a single face at a time
if (selectionRanges.codeBasedSelections.length < 1) return false
const isPipe = isSketchPipe(selectionRanges)
if (isSelectionLastLine(selectionRanges, code)) return true
if (!isPipe) return false
return canExtrudeSelection(selectionRanges)
},
'Selection is one face': ({ selectionRanges }) => {
return !!isCursorInSketchCommandRange(
engineCommandManager.artifactMap,
selectionRanges
)
},
},
services: {
'Get horizontal info': async ({
selectionRanges,
}): Promise<SetSelections> => {
const { modifiedAst, pathToNodeMap } =
await applyConstraintHorzVertDistance({
constraint: 'setHorzDistance',
selectionRanges,
})
await kclManager.updateAst(modifiedAst, true)
return {
selectionType: 'completeSelection',
selection: pathMapToSelections(
kclManager.ast,
selectionRanges,
pathToNodeMap
),
}
},
'Get vertical info': async ({
selectionRanges,
}): Promise<SetSelections> => {
const { modifiedAst, pathToNodeMap } =
await applyConstraintHorzVertDistance({
constraint: 'setVertDistance',
selectionRanges,
})
await kclManager.updateAst(modifiedAst, true)
return {
selectionType: 'completeSelection',
selection: pathMapToSelections(
kclManager.ast,
selectionRanges,
pathToNodeMap
),
}
},
'Get angle info': async ({
selectionRanges,
}): Promise<SetSelections> => {
const { modifiedAst, pathToNodeMap } = await (angleBetweenInfo({
selectionRanges,
}).enabled
? applyConstraintAngleBetween({
selectionRanges,
})
: applyConstraintAngleLength({
selectionRanges,
angleOrLength: 'setAngle',
}))
await kclManager.updateAst(modifiedAst, true)
return {
selectionType: 'completeSelection',
selection: pathMapToSelections(
kclManager.ast,
selectionRanges,
pathToNodeMap
),
}
},
'Get length info': async ({
selectionRanges,
}): Promise<SetSelections> => {
const { modifiedAst, pathToNodeMap } =
await applyConstraintAngleLength({ selectionRanges })
await kclManager.updateAst(modifiedAst, true)
return {
selectionType: 'completeSelection',
selection: pathMapToSelections(
kclManager.ast,
selectionRanges,
pathToNodeMap
),
}
},
'Get perpendicular distance info': async ({
selectionRanges,
}): Promise<SetSelections> => {
const { modifiedAst, pathToNodeMap } = await applyConstraintIntersect(
{
selectionRanges,
if (!setSelections.selection) {
return {
selectionRangeTypeMap,
selectionRanges: {
codeBasedSelections: selectionRanges.codeBasedSelections,
otherSelections,
},
}
)
await kclManager.updateAst(modifiedAst, true)
return {
selectionType: 'completeSelection',
selection: pathMapToSelections(
kclManager.ast,
selectionRanges,
pathToNodeMap
),
}
},
'Get ABS X info': async ({
selectionRanges,
}): Promise<SetSelections> => {
const { modifiedAst, pathToNodeMap } =
await applyConstraintAbsDistance({
constraint: 'xAbs',
selectionRanges,
return {
selectionRangeTypeMap,
selectionRanges: {
codeBasedSelections: selectionRanges.codeBasedSelections,
otherSelections,
},
}
}
// This DOES NOT set the `selectionRanges` in xstate context
// same as comment above
const { codeMirrorSelection, selectionRangeTypeMap } =
handleSelectionBatch({
selections: setSelections.selection,
})
if (codeMirrorSelection) {
setTimeout(() => {
editorView.dispatch({
selection: codeMirrorSelection,
})
await kclManager.updateAst(modifiedAst, true)
return {
selectionType: 'completeSelection',
selection: pathMapToSelections(
kclManager.ast,
selectionRanges,
pathToNodeMap
),
}
},
'Get ABS Y info': async ({
selectionRanges,
}): Promise<SetSelections> => {
const { modifiedAst, pathToNodeMap } =
await applyConstraintAbsDistance({
constraint: 'yAbs',
selectionRanges,
})
await kclManager.updateAst(modifiedAst, true)
return {
selectionType: 'completeSelection',
selection: pathMapToSelections(
kclManager.ast,
selectionRanges,
pathToNodeMap
),
}
},
})
}
return { selectionRangeTypeMap }
}),
},
guards: {
'Selection contains axis': () => true,
'Selection contains edge': () => true,
'Selection contains face': () => true,
'Selection contains line': () => true,
'Selection contains point': () => true,
'Selection is not empty': () => true,
'Selection is one face': ({ selectionRanges }) => {
return !!isCursorInSketchCommandRange(
engineCommandManager.artifactMap,
selectionRanges
)
},
devTools: true,
}
)
},
services: {
'Get horizontal info': async ({
selectionRanges,
}): Promise<SetSelections> => {
const { modifiedAst, pathToNodeMap } =
await applyConstraintHorzVertDistance({
constraint: 'setHorzDistance',
selectionRanges,
})
await kclManager.updateAst(modifiedAst, true)
return {
selectionType: 'completeSelection',
selection: pathMapToSelections(
kclManager.ast,
selectionRanges,
pathToNodeMap
),
}
},
'Get vertical info': async ({
selectionRanges,
}): Promise<SetSelections> => {
const { modifiedAst, pathToNodeMap } =
await applyConstraintHorzVertDistance({
constraint: 'setVertDistance',
selectionRanges,
})
await kclManager.updateAst(modifiedAst, true)
return {
selectionType: 'completeSelection',
selection: pathMapToSelections(
kclManager.ast,
selectionRanges,
pathToNodeMap
),
}
},
'Get angle info': async ({ selectionRanges }): Promise<SetSelections> => {
const { modifiedAst, pathToNodeMap } = await (angleBetweenInfo({
selectionRanges,
}).enabled
? applyConstraintAngleBetween({
selectionRanges,
})
: applyConstraintAngleLength({
selectionRanges,
angleOrLength: 'setAngle',
}))
await kclManager.updateAst(modifiedAst, true)
return {
selectionType: 'completeSelection',
selection: pathMapToSelections(
kclManager.ast,
selectionRanges,
pathToNodeMap
),
}
},
'Get length info': async ({
selectionRanges,
}): Promise<SetSelections> => {
const { modifiedAst, pathToNodeMap } = await applyConstraintAngleLength(
{ selectionRanges }
)
await kclManager.updateAst(modifiedAst, true)
return {
selectionType: 'completeSelection',
selection: pathMapToSelections(
kclManager.ast,
selectionRanges,
pathToNodeMap
),
}
},
'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
),
}
},
'Get ABS X info': async ({ selectionRanges }): Promise<SetSelections> => {
const { modifiedAst, pathToNodeMap } = await applyConstraintAbsDistance(
{
constraint: 'xAbs',
selectionRanges,
}
)
await kclManager.updateAst(modifiedAst, true)
return {
selectionType: 'completeSelection',
selection: pathMapToSelections(
kclManager.ast,
selectionRanges,
pathToNodeMap
),
}
},
'Get ABS Y info': async ({ selectionRanges }): Promise<SetSelections> => {
const { modifiedAst, pathToNodeMap } = await applyConstraintAbsDistance(
{
constraint: 'yAbs',
selectionRanges,
}
)
await kclManager.updateAst(modifiedAst, true)
return {
selectionType: 'completeSelection',
selection: pathMapToSelections(
kclManager.ast,
selectionRanges,
pathToNodeMap
),
}
},
},
devTools: true,
})
useEffect(() => {
engineCommandManager.onPlaneSelected((plane_id: string) => {
@ -559,17 +538,13 @@ export const ModelingMachineProvider = ({
})
}, [modelingSend])
useStateMachineCommands({
machineId: 'modeling',
state: modelingState,
send: modelingSend,
actor: modelingActor,
commandBarConfig: modelingMachineConfig,
onCancel: () => {
console.log('firing onCancel!!')
modelingSend({ type: 'Cancel' })
},
})
// useStateMachineCommands({
// state: settingsState,
// send: settingsSend,
// commands,
// owner: 'settings',
// commandBarMeta: settingsCommandBarMeta,
// })
return (
<ModelingMachineContext.Provider

View File

@ -1,7 +1,7 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { BrowserRouter } from 'react-router-dom'
import { GlobalStateProvider } from './GlobalStateProvider'
import CommandBarProvider from './CommandBar/CommandBar'
import CommandBarProvider from './CommandBar'
import {
NETWORK_CONTENT,
NetworkHealthIndicator,

View File

@ -1,4 +1,8 @@
import { faExclamation, faWifi } from '@fortawesome/free-solid-svg-icons'
import {
faCheck,
faExclamation,
faWifi,
} from '@fortawesome/free-solid-svg-icons'
import { Popover } from '@headlessui/react'
import { useEffect, useState } from 'react'
import { ActionIcon } from './ActionIcon'
@ -73,8 +77,8 @@ export const NetworkHealthIndicator = () => {
data-testid="network-good"
>
<ActionIcon
icon="checkmark"
bgClassName={'bg-succeed-10/50 dark:bg-succeed-80/50 rounded-sm'}
icon={faCheck}
bgClassName={'bg-succeed-10/50 dark:bg-succeed-80/50 rounded'}
iconClassName={'text-succeed-80 dark:text-succeed-30'}
/>
{NETWORK_CONTENT.good}

View File

@ -107,7 +107,6 @@ function ProjectCard({
<Link
className="flex-1 text-liquid-100 after:content-[''] after:absolute after:inset-0"
to={`${paths.FILE}/${encodeURIComponent(project.path)}`}
data-testid="project-link"
>
{project.name?.replace(FILE_EXT, '')}
</Link>
@ -144,7 +143,7 @@ function ProjectCard({
className: 'p-1',
size: 'xs',
bgClassName: 'bg-destroy-80',
iconClassName: '!text-destroy-20 dark:!text-destroy-40',
iconClassName: 'text-destroy-20 dark:text-destroy-40',
}}
className="!p-0 hover:border-destroy-40 dark:hover:border-destroy-40"
onClick={(e) => {
@ -186,7 +185,8 @@ function ProjectCard({
bgClassName: 'bg-destroy-80',
className: 'p-1',
size: 'sm',
iconClassName: '!text-destroy-70 dark:!text-destroy-40',
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"
>

View File

@ -3,8 +3,7 @@ import { BrowserRouter } from 'react-router-dom'
import ProjectSidebarMenu from './ProjectSidebarMenu'
import { ProjectWithEntryPointMetadata } from '../Router'
import { GlobalStateProvider } from './GlobalStateProvider'
import CommandBarProvider from './CommandBar/CommandBar'
import { APP_NAME } from 'lib/constants'
import CommandBarProvider from './CommandBar'
const now = new Date()
const projectWellFormed = {
@ -72,7 +71,9 @@ describe('ProjectSidebarMenu tests', () => {
fireEvent.click(screen.getByTestId('project-sidebar-toggle'))
expect(screen.getByTestId('projectName')).toHaveTextContent(APP_NAME)
expect(screen.getByTestId('projectName')).toHaveTextContent(
'KittyCAD Modeling App'
)
})
test('Renders as a link if set to do so', () => {

View File

@ -8,8 +8,6 @@ import { ExportButton } from './ExportButton'
import { Fragment } from 'react'
import { FileTree } from './FileTree'
import { sep } from '@tauri-apps/api/path'
import { Logo } from './Logo'
import { APP_NAME } from 'lib/constants'
const ProjectSidebarMenu = ({
project,
@ -23,29 +21,37 @@ const ProjectSidebarMenu = ({
return renderAsLink ? (
<Link
to={paths.HOME}
className="rounded-sm h-9 mr-auto max-h-min min-w-max border-0 py-1 px-2 flex items-center gap-3 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-energy-50 dark:hover:bg-chalkboard-90"
className="rounded-sm h-9 mr-auto max-h-min min-w-max border-0 p-0.5 pr-2 flex items-center gap-4 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-energy-50 dark:hover:bg-chalkboard-90"
data-testid="project-sidebar-link"
>
<Logo />
<img
src="/kitt-8bit-winking.svg"
alt="KittyCAD App"
className="w-auto h-9"
/>
<span
className="hidden text-sm text-chalkboard-110 dark:text-chalkboard-20 whitespace-nowrap lg:block"
data-testid="project-sidebar-link-name"
>
{project?.name ? project.name : APP_NAME}
{project?.name ? project.name : 'KittyCAD Modeling App'}
</span>
</Link>
) : (
<Popover className="relative">
<Popover.Button
className="rounded-sm h-9 mr-auto max-h-min min-w-max border-0 py-1 px-2 flex items-center gap-3 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-energy-50 dark:hover:bg-chalkboard-90"
className="rounded-sm h-9 mr-auto max-h-min min-w-max border-0 p-0.5 pr-2 flex items-center gap-4 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-energy-50 dark:hover:bg-chalkboard-90"
data-testid="project-sidebar-toggle"
>
<Logo />
<img
src="/kitt-8bit-winking.svg"
alt="KittyCAD App"
className="w-auto h-full"
/>
<div className="flex flex-col items-start py-0.5">
<span className="hidden text-sm text-chalkboard-110 dark:text-chalkboard-20 whitespace-nowrap lg:block">
{isTauri() && file?.name
? file.name.slice(file.name.lastIndexOf(sep) + 1)
: APP_NAME}
: 'KittyCAD Modeling App'}
</span>
{isTauri() && project?.name && (
<span className="hidden text-xs text-chalkboard-70 dark:text-chalkboard-40 whitespace-nowrap lg:block">
@ -82,13 +88,18 @@ const ProjectSidebarMenu = ({
{({ close }) => (
<>
<div className="flex items-center gap-4 px-4 py-3">
<Logo />
<img
src="/kitt-8bit-winking.svg"
alt="KittyCAD App"
className="w-auto h-9"
/>
<div>
<p
className="m-0 text-chalkboard-100 dark:text-energy-10 text-mono"
data-testid="projectName"
>
{project?.name ? project.name : APP_NAME}
{project?.name ? project.name : 'KittyCAD Modeling App'}
</p>
{project?.entrypointMetadata && (
<p

View File

@ -7,7 +7,7 @@ import {
} from 'react'
import { v4 as uuidv4 } from 'uuid'
import { useStore } from '../useStore'
import { getNormalisedCoordinates, throttle } from '../lib/utils'
import { getNormalisedCoordinates } from '../lib/utils'
import Loading from './Loading'
import { cameraMouseDragGuards } from 'lib/cameraControls'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
@ -115,9 +115,9 @@ export const Stream = ({ className = '' }) => {
setClickCoords({ x, y })
}
const fps = 60
const handleScroll: WheelEventHandler<HTMLVideoElement> = throttle((e) => {
const handleScroll: WheelEventHandler<HTMLVideoElement> = (e) => {
if (!cameraMouseDragGuards[cameraControls].zoom.scrollCallback(e)) return
engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
@ -126,7 +126,7 @@ export const Stream = ({ className = '' }) => {
},
cmd_id: uuidv4(),
})
}, Math.round(1000 / fps))
}
const handleMouseUp: MouseEventHandler<HTMLVideoElement> = ({
clientX,
@ -243,14 +243,15 @@ export const Stream = ({ className = '' }) => {
})
} else if (
!didDragInStream &&
(state.matches('Sketch.SketchIdle') || state.matches('idle'))
(state.matches('Sketch.SketchIdle') ||
state.matches('idle') ||
state.matches('awaiting selection'))
) {
command.cmd = {
type: 'select_with_point',
selected_at_window: { x, y },
selection_type: 'add',
}
engineCommandManager.sendSceneCommand(command)
} else if (!didDragInStream && state.matches('Sketch.Move Tool')) {
command.cmd = {

View File

@ -64,7 +64,7 @@ export const TextEditor = ({
const { settings: { context: { textWrapping } = {} } = {} } =
useGlobalStateContext()
const { commandBarSend } = useCommandsContext()
const { setCommandBarOpen } = useCommandsContext()
const { enable: convertEnabled, handleClick: convertCallback } =
useConvertToVariable()
@ -136,7 +136,7 @@ export const TextEditor = ({
{
key: 'Meta-k',
run: () => {
commandBarSend({ type: 'Open' })
setCommandBarOpen(true)
return false
},
},

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