Compare commits
1 Commits
path-to-no
...
achalmers/
Author | SHA1 | Date | |
---|---|---|---|
c9e27ffb95 |
@ -1,3 +1,3 @@
|
||||
[codespell]
|
||||
ignore-words-list: crate,everytime
|
||||
skip: **/target,node_modules,build,**/Cargo.lock
|
||||
skip: **/target,node_modules,build
|
||||
|
@ -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=
|
||||
|
@ -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
|
||||
|
11
.github/workflows/cargo-clippy.yml
vendored
@ -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 }}"
|
||||
|
10
.github/workflows/cargo-test.yml
vendored
@ -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: |-
|
||||
|
53
.github/workflows/ci.yml
vendored
@ -55,7 +55,7 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v4
|
||||
- name: Install codespell
|
||||
run: |
|
||||
python -m pip install codespell
|
||||
@ -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:
|
||||
@ -182,9 +181,6 @@ jobs:
|
||||
cd ../../
|
||||
cp src/wasm-lib/pkg/wasm_lib_bg.wasm public
|
||||
|
||||
- name: Run vite build (build:both)
|
||||
run: yarn vite build --mode ${{ env.BUILD_RELEASE == 'true' && 'production' || 'development' }}
|
||||
|
||||
- name: Fix format
|
||||
run: yarn fmt
|
||||
|
||||
@ -244,7 +240,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' }}
|
||||
@ -255,12 +250,10 @@ jobs:
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
run: |
|
||||
cargo install tauri-driver
|
||||
source .env.${{ env.BUILD_RELEASE == 'true' && 'production' || 'development' }}
|
||||
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"
|
||||
KITTYCAD_API_TOKEN: ${{ env.BUILD_RELEASE == 'true' && secrets.KITTYCAD_API_TOKEN || secrets.KITTYCAD_API_TOKEN_DEV }}
|
||||
E2E_APPLICATION: "./src-tauri/target/${{ env.BUILD_RELEASE == 'true' && 'release' || 'debug' }}/kittycad-modeling"
|
||||
KITTYCAD_API_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN }}
|
||||
|
||||
|
||||
publish-apps-release:
|
||||
@ -273,24 +266,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 +299,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 +313,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 +329,9 @@ jobs:
|
||||
"dmg-universal": {
|
||||
"url": $darwin_url
|
||||
},
|
||||
"appimage-x86_64": {
|
||||
"url": $linux_url
|
||||
},
|
||||
"msi-x86_64": {
|
||||
"url": $windows_url
|
||||
}
|
||||
@ -342,26 +345,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 +373,4 @@ jobs:
|
||||
if: ${{ github.event_name == 'release' }}
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: 'artifact/*/Zoo*'
|
||||
files: artifact/*/*itty*
|
||||
|
2
.github/workflows/playwright.yml
vendored
@ -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
@ -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/
|
||||
|
2
LICENSE
@ -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
|
||||
|
21
README.md
@ -1,17 +1,17 @@
|
||||

|
||||

|
||||
|
||||
## 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
|
||||
|
BIN
app-icon.png
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 207 KiB |
@ -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": {
|
||||
|
@ -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
|
||||
|
||||
|
BIN
e2e/playwright/export-snapshots/gltf-binary.gltf
Normal file
Before Width: | Height: | Size: 80 KiB |
2459
e2e/playwright/export-snapshots/gltf-embedded.gltf
Normal file
Before Width: | Height: | Size: 80 KiB |
189
e2e/playwright/export-snapshots/obj-.obj
Normal 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
|
Before Width: | Height: | Size: 56 KiB |
282
e2e/playwright/export-snapshots/ply-ascii.ply
Normal 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
|
Before Width: | Height: | Size: 56 KiB |
BIN
e2e/playwright/export-snapshots/ply-binary_big_endian.ply
Normal file
Before Width: | Height: | Size: 56 KiB |
BIN
e2e/playwright/export-snapshots/ply-binary_little_endian.ply
Normal file
Before Width: | Height: | Size: 56 KiB |
Before Width: | Height: | Size: 80 KiB |
@ -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.);
|
||||
|
Before Width: | Height: | Size: 57 KiB |
Before Width: | Height: | Size: 57 KiB |
@ -4,7 +4,8 @@ import { EngineCommand } from '../../src/lang/std/engineConnection'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { getUtils } from './test-utils'
|
||||
import waitOn from 'wait-on'
|
||||
import { Themes } from '../../src/lib/theme'
|
||||
import { Models } from '@kittycad/lib'
|
||||
import fsp from 'fs/promises'
|
||||
|
||||
/*
|
||||
debug helper: unfortunately we do rely on exact coord mouse clicks in a few places
|
||||
@ -85,26 +86,26 @@ test('Basic sketch', async ({ page }) => {
|
||||
|
||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
||||
|
||||
const startAt = '[18.26, -24.63]'
|
||||
const num = '18.43'
|
||||
const startAt = '[10.97, -14.79]'
|
||||
const tenish = '11.07'
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt(${startAt}, %)
|
||||
|> line([${num}, 0], %)`)
|
||||
|> line([${tenish}, 0], %)`)
|
||||
|
||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt(${startAt}, %)
|
||||
|> line([${num}, 0], %)
|
||||
|> line([0, ${num}], %)`)
|
||||
|> line([${tenish}, 0], %)
|
||||
|> line([0, ${tenish}], %)`)
|
||||
await page.mouse.click(startXPx, 500 - PUR * 20)
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt(${startAt}, %)
|
||||
|> line([${num}, 0], %)
|
||||
|> line([0, ${num}], %)
|
||||
|> line([-36.69, 0], %)`)
|
||||
|> line([${tenish}, 0], %)
|
||||
|> line([0, ${tenish}], %)
|
||||
|> line([-22.04, 0], %)`)
|
||||
|
||||
// deselect line tool
|
||||
await u.doAndWaitForCmd(
|
||||
@ -132,8 +133,8 @@ test('Basic sketch', async ({ page }) => {
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt(${startAt}, %)
|
||||
|> line({ to: [${num}, 0], tag: 'seg01' }, %)
|
||||
|> line([0, ${num}], %)
|
||||
|> line({ to: [${tenish}, 0], tag: 'seg01' }, %)
|
||||
|> line([0, ${tenish}], %)
|
||||
|> angledLine([180, segLen('seg01', %)], %)`)
|
||||
})
|
||||
|
||||
@ -182,26 +183,6 @@ test('if you write invalid kcl you get inlined errors', async ({ page }) => {
|
||||
|
||||
// wait for .cm-lint-marker-error not to be visible
|
||||
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
|
||||
|
||||
// let's check we get an error when defining the same variable twice
|
||||
await page.getByText('const bottomAng = 25').click()
|
||||
await page.keyboard.press('Enter')
|
||||
await page.keyboard.type("// Let's define the same thing twice")
|
||||
await page.keyboard.press('Enter')
|
||||
await page.keyboard.type('const topAng = 42')
|
||||
|
||||
await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
|
||||
await expect(page.locator('.cm-lintRange.cm-lintRange-error')).toBeVisible()
|
||||
|
||||
await page.locator('.cm-lintRange.cm-lintRange-error').hover()
|
||||
await expect(page.locator('.cm-diagnosticText')).toBeVisible()
|
||||
await expect(page.getByText('Cannot redefine topAng')).toBeVisible()
|
||||
|
||||
const secondTopAng = await page.getByText('topAng').first()
|
||||
await secondTopAng?.dblclick()
|
||||
await page.keyboard.type('otherAng')
|
||||
|
||||
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('executes on load', async ({ page, context }) => {
|
||||
@ -507,27 +488,27 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|
||||
|
||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
||||
|
||||
const startAt = '[18.26, -24.63]'
|
||||
const num = '18.43'
|
||||
const num2 = '36.69'
|
||||
const startAt = '[10.97, -14.79]'
|
||||
const tenish = '11.07'
|
||||
const twentyish = '22.04'
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt(${startAt}, %)
|
||||
|> line([${num}, 0], %)`)
|
||||
|> line([${tenish}, 0], %)`)
|
||||
|
||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt(${startAt}, %)
|
||||
|> line([${num}, 0], %)
|
||||
|> line([0, ${num}], %)`)
|
||||
|> line([${tenish}, 0], %)
|
||||
|> line([0, ${tenish}], %)`)
|
||||
await page.mouse.click(startXPx, 500 - PUR * 20)
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt(${startAt}, %)
|
||||
|> line([${num}, 0], %)
|
||||
|> line([0, ${num}], %)
|
||||
|> line([-${num2}, 0], %)`)
|
||||
|> line([${tenish}, 0], %)
|
||||
|> line([0, ${tenish}], %)
|
||||
|> line([-${twentyish}, 0], %)`)
|
||||
|
||||
// deselect line tool
|
||||
await u.doAndWaitForCmd(
|
||||
@ -580,7 +561,7 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|
||||
|
||||
// check the same selection again by putting cursor in code first then selecting axis
|
||||
await u.doAndWaitForCmd(
|
||||
() => page.getByText(` |> line([-${num2}, 0], %)`).click(),
|
||||
() => page.getByText(` |> line([-${twentyish}, 0], %)`).click(),
|
||||
'select_clear',
|
||||
false
|
||||
)
|
||||
@ -595,7 +576,7 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|
||||
|
||||
// select segment in editor than another segment in scene and check there are two cursors
|
||||
await u.doAndWaitForCmd(
|
||||
() => page.getByText(` |> line([-${num2}, 0], %)`).click(),
|
||||
() => page.getByText(` |> line([-${twentyish}, 0], %)`).click(),
|
||||
'select_clear',
|
||||
false
|
||||
)
|
||||
@ -632,106 +613,3 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|
||||
// hover again and check it works
|
||||
await selectionSequence()
|
||||
})
|
||||
|
||||
test('Command bar works and can change a setting', async ({ page }) => {
|
||||
// Brief boilerplate
|
||||
const u = getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
await page.goto('/')
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
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 expect(cmdSearchBar).toBeVisible()
|
||||
await page.keyboard.press('Escape')
|
||||
await expect(cmdSearchBar).not.toBeVisible()
|
||||
|
||||
// Now try the same, but with the keyboard shortcut, check focus
|
||||
await page.keyboard.press('Meta+K')
|
||||
await expect(cmdSearchBar).toBeVisible()
|
||||
await expect(cmdSearchBar).toBeFocused()
|
||||
|
||||
// Try typing in the command bar
|
||||
await page.keyboard.type('theme')
|
||||
const themeOption = page.getByRole('option', { name: 'Set Theme' })
|
||||
await expect(themeOption).toBeVisible()
|
||||
await themeOption.click()
|
||||
const themeInput = page.getByPlaceholder('Select an option')
|
||||
await expect(themeInput).toBeVisible()
|
||||
await expect(themeInput).toBeFocused()
|
||||
// Select dark theme
|
||||
await page.keyboard.press('ArrowDown')
|
||||
await page.keyboard.press('ArrowDown')
|
||||
await expect(page.getByRole('option', { name: Themes.Dark })).toHaveAttribute(
|
||||
'data-headlessui-state',
|
||||
'active'
|
||||
)
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
// Check the toast appeared
|
||||
await expect(page.getByText(`Set Theme to "${Themes.Dark}"`)).toBeVisible()
|
||||
// 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, %)`
|
||||
)
|
||||
})
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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) => {
|
||||
@ -47,7 +45,7 @@ test('change camera, show planes', async ({ page, context }) => {
|
||||
type: 'default_camera_look_at',
|
||||
center: { x: 0, y: 0, z: 0 },
|
||||
up: { x: 0, y: 0, z: 1 },
|
||||
vantage: { x: 0, y: 85, z: 85 },
|
||||
vantage: { x: 0, y: 50, z: 50 },
|
||||
},
|
||||
}
|
||||
|
||||
@ -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',
|
||||
})
|
||||
})
|
||||
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 118 KiB |
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 80 KiB |
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 53 KiB |
@ -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
|
||||
@ -36,14 +29,13 @@ describe('ZMA (Tauri, Linux)', () => {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
const apiBaseUrl = process.env.VITE_KC_API_BASE_URL
|
||||
const verifyUrl = `${apiBaseUrl}/oauth2/device/verify?user_code=${userCode}`
|
||||
const verifyUrl = `https://api.kittycad.io/oauth2/device/verify?user_code=${userCode}`
|
||||
console.log(`GET ${verifyUrl}`)
|
||||
const vr = await fetch(verifyUrl, { headers })
|
||||
console.log(vr.status)
|
||||
|
||||
// Device flow: confirm
|
||||
const confirmUrl = `${apiBaseUrl}/oauth2/device/confirm`
|
||||
const confirmUrl = 'https://api.kittycad.io/oauth2/device/confirm'
|
||||
const data = JSON.stringify({ user_code: userCode })
|
||||
console.log(`POST ${confirmUrl} ${data}`)
|
||||
const cr = await fetch(confirmUrl, {
|
||||
@ -56,51 +48,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')
|
||||
})
|
||||
|
11
index.html
@ -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>
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "untitled-app",
|
||||
"version": "0.14.0",
|
||||
"version": "0.12.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",
|
||||
@ -135,8 +134,8 @@
|
||||
"postcss": "^8.4.31",
|
||||
"prettier": "^2.8.0",
|
||||
"setimmediate": "^1.0.5",
|
||||
"tailwindcss": "^3.3.6",
|
||||
"vite": "^4.5.2",
|
||||
"tailwindcss": "^3.3.5",
|
||||
"vite": "^4.5.0",
|
||||
"vite-plugin-eslint": "^1.8.1",
|
||||
"vite-tsconfig-paths": "^4.2.1",
|
||||
"wait-on": "^7.2.0",
|
||||
|
3
public/Icon/Icon/Projects/Create File.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M4 3H4.5H11H11.2071L11.3536 3.14645L15.8536 7.64646L16 7.7929V8.00001V11.3773C15.6992 11.1362 15.3628 10.9376 15 10.7908V8.50001H11H10.5V8.00001V4H5V16H9.79076C9.93763 16.3628 10.1362 16.6992 10.3773 17H4.5H4V16.5V3.5V3ZM11.5 4.70711L14.2929 7.50001H11.5V4.70711ZM13 12V14H11V15H13V17H14V15H16V14H14V12H13Z" fill="black"/>
|
||||
</svg>
|
After Width: | Height: | Size: 475 B |
3
public/Icon/Icon/Projects/Create Folder.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.5 3.5H4H7H7.16667L7.3 3.6L9.16667 5H16H16.5V5.5V7.5V10.3773C16.1992 10.1362 15.8628 9.93763 15.5 9.79076V8H4.5V15.5H10.5351C10.7529 15.8764 11.0302 16.2141 11.3542 16.5H4H3.5V16V7.5V4V3.5ZM4.5 4.5V7H15.5V6H9H8.83333L8.7 5.9L6.83333 4.5H4.5ZM13.5 11V13H11.5V14H13.5V16H14.5V14H16.5V13H14.5V11H13.5Z" fill="black"/>
|
||||
</svg>
|
After Width: | Height: | Size: 469 B |
3
public/Icon/Icon/Projects/File.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11 3.5H4.5V16.5H15.5V8.00001M11 3.5L15.5 8.00001M11 3.5V8.00001H15.5" stroke="black"/>
|
||||
</svg>
|
After Width: | Height: | Size: 200 B |
@ -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 it’s not a duplicate or it’s 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:
|
||||
|
Before Width: | Height: | Size: 11 KiB |
@ -1,6 +1,6 @@
|
||||
{
|
||||
"short_name": "ZMA",
|
||||
"name": "Zoo Modeling App",
|
||||
"short_name": "KCMA",
|
||||
"name": "KittyCAD Modeling App",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
|
@ -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.
|
||||
|
||||
|
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 13 KiB |
@ -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
@ -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",
|
||||
|
@ -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"
|
||||
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 55 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 62 KiB |
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 9.2 KiB |
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 5.4 KiB |
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 68 KiB |
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 122 KiB |
@ -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()))?;
|
||||
|
@ -1,13 +1,14 @@
|
||||
{
|
||||
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
||||
"build": {
|
||||
"beforeBuildCommand": "yarn build:both",
|
||||
"beforeDevCommand": "yarn start",
|
||||
"devPath": "http://localhost:3000",
|
||||
"distDir": "../build"
|
||||
},
|
||||
"package": {
|
||||
"productName": "zoo-modeling-app",
|
||||
"version": "0.14.0"
|
||||
"productName": "kittycad-modeling",
|
||||
"version": "0.12.0"
|
||||
},
|
||||
"tauri": {
|
||||
"allowlist": {
|
||||
@ -84,7 +85,7 @@
|
||||
"fullscreen": false,
|
||||
"height": 1200,
|
||||
"resizable": true,
|
||||
"title": "Zoo Modeling App",
|
||||
"title": "KittyCAD Modeling",
|
||||
"width": 1800
|
||||
}
|
||||
]
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
||||
"package": {
|
||||
"productName": "Zoo Modeling App"
|
||||
"productName": "KittyCAD Modeling"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
||||
"package": {
|
||||
"productName": "Zoo Modeling App"
|
||||
"productName": "KittyCAD Modeling"
|
||||
}
|
||||
}
|
||||
|
@ -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'
|
||||
|
||||
|
17
src/App.tsx
@ -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}
|
||||
|
@ -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}</>
|
||||
|
@ -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: [
|
||||
|
106
src/Toolbar.module.css
Normal file
@ -0,0 +1,106 @@
|
||||
.toolbarWrapper {
|
||||
@apply relative;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
@apply flex gap-4 items-center rounded-full;
|
||||
@apply border border-cool-20/30 bg-cool-10/50;
|
||||
}
|
||||
|
||||
:global(.dark) .toolbar {
|
||||
@apply border-cool-100/50 bg-cool-120/50;
|
||||
}
|
||||
|
||||
:global(.sketch) .toolbar {
|
||||
@apply border-fern-20/20 bg-fern-10/20;
|
||||
}
|
||||
|
||||
:global(.dark .sketch) .toolbar {
|
||||
@apply border-fern-120/50 bg-fern-100/30;
|
||||
}
|
||||
|
||||
.toolbarCap {
|
||||
@apply text-sm font-bold;
|
||||
@apply bg-cool-20/50 text-cool-100;
|
||||
}
|
||||
|
||||
:global(.dark) .toolbarCap {
|
||||
@apply bg-cool-90/50 text-cool-30;
|
||||
}
|
||||
|
||||
:global(.sketch) .toolbarCap {
|
||||
@apply bg-fern-20/50 text-fern-100;
|
||||
}
|
||||
|
||||
:global(.dark .sketch) .toolbarCap {
|
||||
@apply bg-fern-90/50 text-fern-30;
|
||||
}
|
||||
|
||||
.label {
|
||||
@apply self-stretch flex items-center px-4 py-1;
|
||||
@apply rounded-l-full;
|
||||
}
|
||||
|
||||
.popoverToggle {
|
||||
@apply self-stretch m-0 flex items-center px-4 py-1;
|
||||
@apply rounded-r-full border-none;
|
||||
@apply hover:bg-cool-20;
|
||||
}
|
||||
|
||||
.toolbarButtons::-webkit-scrollbar {
|
||||
@apply h-0.5;
|
||||
}
|
||||
|
||||
.toolbarButtons {
|
||||
@apply flex items-center overflow-x-auto;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.toolbarButtons button {
|
||||
@apply text-chalkboard-90 bg-chalkboard-10/50 border-chalkboard-50 whitespace-nowrap;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@apply gap-1.5 p-0.5 pr-1;
|
||||
@apply rounded-sm;
|
||||
}
|
||||
:global(.dark) .toolbarButtons button {
|
||||
@apply text-chalkboard-30 bg-chalkboard-90/50 border-chalkboard-50;
|
||||
}
|
||||
.toolbarButtons button:hover {
|
||||
@apply text-cool-90 bg-cool-10;
|
||||
}
|
||||
:global(.sketch) .toolbarButtons button:hover {
|
||||
@apply text-fern-90 bg-fern-10;
|
||||
}
|
||||
.toolbarButtons button:disabled {
|
||||
@apply text-chalkboard-70 bg-chalkboard-30;
|
||||
}
|
||||
.toolbarButtons button:disabled:hover {
|
||||
@apply !bg-inherit !text-inherit cursor-not-allowed;
|
||||
}
|
||||
|
||||
:global(.dark) .toolbarButtons button {
|
||||
@apply text-chalkboard-20 border-chalkboard-50;
|
||||
}
|
||||
:global(.dark) .toolbarButtons button:hover {
|
||||
@apply text-cool-10 border-chalkboard-50 bg-cool-90;
|
||||
}
|
||||
:global(.dark .sketch) .toolbarButtons button:hover {
|
||||
@apply text-fern-10 border-chalkboard-50 bg-fern-90;
|
||||
}
|
||||
:global(.dark) .toolbarButtons button:disabled {
|
||||
@apply text-chalkboard-40 bg-chalkboard-80;
|
||||
}
|
||||
|
||||
:global(.dark) .popoverToggle {
|
||||
@apply hover:bg-cool-90;
|
||||
}
|
||||
|
||||
:global(.sketch) .popoverToggle {
|
||||
@apply hover:bg-fern-20;
|
||||
}
|
||||
|
||||
:global(.dark .sketch) .popoverToggle {
|
||||
@apply hover:bg-fern-90;
|
||||
}
|
305
src/Toolbar.tsx
@ -1,18 +1,22 @@
|
||||
import { WheelEvent, useRef, useMemo } from 'react'
|
||||
import { Fragment, WheelEvent, useRef, useMemo } from 'react'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faSearch, faX } from '@fortawesome/free-solid-svg-icons'
|
||||
import { Popover, Transition } from '@headlessui/react'
|
||||
import styles from './Toolbar.module.css'
|
||||
import { isCursorInSketchCommandRange } from 'lang/util'
|
||||
import { ActionIcon } from 'components/ActionIcon'
|
||||
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 sketchButtonClassnames = {
|
||||
background:
|
||||
'bg-chalkboard-100 group-hover:bg-chalkboard-90 hover:bg-chalkboard-90 dark:bg-fern-20 dark:group-hover:bg-fern-10 dark:hover:bg-fern-10 group-disabled:bg-chalkboard-50 dark:group-disabled:bg-chalkboard-60 group-hover:group-disabled:bg-chalkboard-50 dark:group-hover:group-disabled:bg-chalkboard-50',
|
||||
icon: 'text-fern-20 h-auto group-hover:text-fern-10 hover:text-fern-10 dark:text-chalkboard-100 dark:group-hover:text-chalkboard-100 dark:hover:text-chalkboard-100 group-disabled:bg-chalkboard-60 hover:group-disabled:text-inherit',
|
||||
}
|
||||
|
||||
export const Toolbar = () => {
|
||||
const platform = usePlatform()
|
||||
const { commandBarSend } = useCommandsContext()
|
||||
const { state, send, context } = useModelingContext()
|
||||
const toolbarButtonsRef = useRef<HTMLUListElement>(null)
|
||||
const bgClassName =
|
||||
'group-enabled:group-hover:bg-energy-10 group-pressed:bg-energy-10 dark:group-enabled:group-hover:bg-chalkboard-80 dark:group-pressed:bg-chalkboard-80'
|
||||
const toolbarButtonsRef = useRef<HTMLSpanElement>(null)
|
||||
const pathId = useMemo(
|
||||
() =>
|
||||
isCursorInSketchCommandRange(
|
||||
@ -31,102 +35,72 @@ export const Toolbar = () => {
|
||||
span.scrollLeft = span.scrollLeft += ev.deltaY
|
||||
}
|
||||
|
||||
function ToolbarButtons({
|
||||
className = '',
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLElement>) {
|
||||
function ToolbarButtons({ className }: React.HTMLAttributes<HTMLElement>) {
|
||||
return (
|
||||
<ul
|
||||
{...props}
|
||||
<span
|
||||
ref={toolbarButtonsRef}
|
||||
onWheel={handleToolbarButtonsWheelEvent}
|
||||
className={
|
||||
'm-0 py-1 rounded-l-sm flex gap-2 items-center overflow-x-auto ' +
|
||||
className
|
||||
}
|
||||
style={{ scrollbarWidth: 'thin' }}
|
||||
className={styles.toolbarButtons + ' ' + className}
|
||||
>
|
||||
{state.nextEvents.includes('Enter sketch') && (
|
||||
<li className="contents">
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={() => send({ type: 'Enter sketch' })}
|
||||
icon={{
|
||||
icon: 'sketch',
|
||||
bgClassName,
|
||||
}}
|
||||
>
|
||||
<span data-testid="start-sketch">Start Sketch</span>
|
||||
</ActionButton>
|
||||
</li>
|
||||
<button
|
||||
onClick={() => send({ type: 'Enter sketch' })}
|
||||
className="group"
|
||||
>
|
||||
<ActionIcon icon="sketch" className="!p-0.5" size="md" />
|
||||
<span data-testid="start-sketch">Start Sketch</span>
|
||||
</button>
|
||||
)}
|
||||
{state.nextEvents.includes('Enter sketch') && pathId && (
|
||||
<li className="contents">
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={() => send({ type: 'Enter sketch' })}
|
||||
icon={{
|
||||
icon: 'sketch',
|
||||
bgClassName,
|
||||
}}
|
||||
>
|
||||
Edit Sketch
|
||||
</ActionButton>
|
||||
</li>
|
||||
<button
|
||||
onClick={() => send({ type: 'Enter sketch' })}
|
||||
className="group"
|
||||
>
|
||||
<ActionIcon icon="sketch" className="!p-0.5" size="md" />
|
||||
Edit Sketch
|
||||
</button>
|
||||
)}
|
||||
{state.nextEvents.includes('Cancel') && !state.matches('idle') && (
|
||||
<li className="contents">
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={() => send({ type: 'Cancel' })}
|
||||
icon={{
|
||||
icon: 'arrowLeft',
|
||||
bgClassName,
|
||||
}}
|
||||
>
|
||||
Exit Sketch
|
||||
</ActionButton>
|
||||
</li>
|
||||
<button onClick={() => send({ type: 'Cancel' })} className="group">
|
||||
<ActionIcon icon="exit" className="!p-0.5" size="md" />
|
||||
Exit Sketch
|
||||
</button>
|
||||
)}
|
||||
{state.matches('Sketch') && !state.matches('idle') && (
|
||||
<li className="contents">
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={() =>
|
||||
state.matches('Sketch.Line Tool')
|
||||
? send('CancelSketch')
|
||||
: send('Equip tool')
|
||||
}
|
||||
aria-pressed={state.matches('Sketch.Line Tool')}
|
||||
className="pressed:bg-energy-10/20 dark:pressed:bg-energy-80"
|
||||
icon={{
|
||||
icon: 'line',
|
||||
bgClassName,
|
||||
}}
|
||||
>
|
||||
Line
|
||||
</ActionButton>
|
||||
</li>
|
||||
<button
|
||||
onClick={() =>
|
||||
state.matches('Sketch.Line Tool')
|
||||
? send('CancelSketch')
|
||||
: send('Equip tool')
|
||||
}
|
||||
className={
|
||||
'group ' +
|
||||
(state.matches('Sketch.Line Tool')
|
||||
? '!text-fern-70 !bg-fern-10 !dark:text-fern-20 !border-fern-50'
|
||||
: '')
|
||||
}
|
||||
>
|
||||
<ActionIcon icon="line" className="!p-0.5" size="md" />
|
||||
Line
|
||||
</button>
|
||||
)}
|
||||
{state.matches('Sketch') && (
|
||||
<li className="contents">
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={() =>
|
||||
state.matches('Sketch.Move Tool')
|
||||
? send('CancelSketch')
|
||||
: send('Equip move tool')
|
||||
}
|
||||
aria-pressed={state.matches('Sketch.Move Tool')}
|
||||
className="pressed:bg-energy-10/20 dark:pressed:bg-energy-80"
|
||||
icon={{
|
||||
icon: 'move',
|
||||
bgClassName,
|
||||
}}
|
||||
>
|
||||
Move
|
||||
</ActionButton>
|
||||
</li>
|
||||
<button
|
||||
onClick={() =>
|
||||
state.matches('Sketch.Move Tool')
|
||||
? send('CancelSketch')
|
||||
: send('Equip move tool')
|
||||
}
|
||||
className={
|
||||
'group ' +
|
||||
(state.matches('Sketch.Move Tool')
|
||||
? '!text-fern-70 !bg-fern-10 !dark:text-fern-20 !border-fern-50'
|
||||
: '')
|
||||
}
|
||||
>
|
||||
<ActionIcon icon="move" className="!p-0.5" size="md" />
|
||||
Move
|
||||
</button>
|
||||
)}
|
||||
{state.matches('Sketch.SketchIdle') &&
|
||||
state.nextEvents
|
||||
@ -151,71 +125,102 @@ export const Toolbar = () => {
|
||||
return 0
|
||||
})
|
||||
.map((eventName) => (
|
||||
<li className="contents">
|
||||
<ActionButton
|
||||
Element="button"
|
||||
className="text-sm"
|
||||
key={eventName}
|
||||
onClick={() => send(eventName)}
|
||||
disabled={
|
||||
!state.nextEvents
|
||||
.filter((event) => state.can(event as any))
|
||||
.includes(eventName)
|
||||
}
|
||||
title={eventName}
|
||||
icon={{
|
||||
icon: 'line',
|
||||
bgClassName,
|
||||
}}
|
||||
>
|
||||
{eventName
|
||||
.replace('Make segment ', '')
|
||||
.replace('Constrain ', '')}
|
||||
</ActionButton>
|
||||
</li>
|
||||
<button
|
||||
key={eventName}
|
||||
onClick={() => send(eventName)}
|
||||
className="group"
|
||||
disabled={
|
||||
!state.nextEvents
|
||||
.filter((event) => state.can(event as any))
|
||||
.includes(eventName)
|
||||
}
|
||||
title={eventName}
|
||||
>
|
||||
<ActionIcon
|
||||
icon={'line'} // TODO
|
||||
bgClassName={sketchButtonClassnames.background}
|
||||
iconClassName={sketchButtonClassnames.icon}
|
||||
size="md"
|
||||
/>
|
||||
{eventName
|
||||
.replace('Make segment ', '')
|
||||
.replace('Constrain ', '')}
|
||||
</button>
|
||||
))}
|
||||
{state.matches('idle') && (
|
||||
<li className="contents">
|
||||
<ActionButton
|
||||
Element="button"
|
||||
className="text-sm"
|
||||
onClick={() =>
|
||||
commandBarSend({
|
||||
type: 'Find and select command',
|
||||
data: { name: 'Extrude', ownerMachine: 'modeling' },
|
||||
})
|
||||
}
|
||||
disabled={!state.can('Extrude')}
|
||||
title={
|
||||
state.can('Extrude')
|
||||
? 'extrude'
|
||||
: 'sketches need to be closed, or not already extruded'
|
||||
}
|
||||
icon={{
|
||||
icon: 'extrude',
|
||||
bgClassName,
|
||||
}}
|
||||
>
|
||||
Extrude
|
||||
</ActionButton>
|
||||
</li>
|
||||
<button
|
||||
onClick={() => send('extrude intent')}
|
||||
disabled={!state.can('extrude intent')}
|
||||
className="group"
|
||||
title={
|
||||
state.can('extrude intent')
|
||||
? 'extrude'
|
||||
: 'sketches need to be closed, or not already extruded'
|
||||
}
|
||||
>
|
||||
<ActionIcon icon="extrude" className="!p-0.5" size="md" />
|
||||
Extrude
|
||||
</button>
|
||||
)}
|
||||
</ul>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-full flex items-stretch rounded-l-sm rounded-r-full bg-chalkboard-10 dark:bg-chalkboard-100 relative">
|
||||
<menu className="flex-1 pl-1 pr-2 py-0 overflow-hidden rounded-l-sm whitespace-nowrap bg-chalkboard-10 dark:bg-chalkboard-100 border-solid border border-energy-10 dark:border-chalkboard-90 border-r-0">
|
||||
<ToolbarButtons />
|
||||
</menu>
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={() => commandBarSend({ type: 'Open' })}
|
||||
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"
|
||||
<Popover
|
||||
className={
|
||||
styles.toolbarWrapper + state.matches('Sketch') ? ' sketch' : ''
|
||||
}
|
||||
>
|
||||
<div className={styles.toolbar}>
|
||||
<span className={styles.toolbarCap + ' ' + styles.label}>
|
||||
{state.matches('Sketch') ? '2D' : '3D'}
|
||||
</span>
|
||||
<menu className="flex-1 gap-2 py-0.5 overflow-hidden whitespace-nowrap">
|
||||
<ToolbarButtons />
|
||||
</menu>
|
||||
<Popover.Button
|
||||
className={styles.toolbarCap + ' ' + styles.popoverToggle}
|
||||
>
|
||||
<FontAwesomeIcon icon={faSearch} />
|
||||
</Popover.Button>
|
||||
</div>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-200"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="transition ease-out duration-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
{platform === 'darwin' ? '⌘K' : 'Ctrl+/'}
|
||||
</ActionButton>
|
||||
</div>
|
||||
<Popover.Overlay className="fixed inset-0 bg-chalkboard-110/20 dark:bg-chalkboard-110/50" />
|
||||
</Transition>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="opacity-0 translate-y-1 scale-95"
|
||||
enterTo="opacity-100 translate-y-0 scale-100"
|
||||
leave="transition ease-out duration-75"
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 translate-y-2"
|
||||
>
|
||||
<Popover.Panel className="absolute top-0 w-screen max-w-xl left-1/2 -translate-x-1/2 flex flex-col gap-8 bg-chalkboard-10 dark:bg-chalkboard-100 p-5 rounded border border-chalkboard-20/30 dark:border-chalkboard-70/50">
|
||||
<section className="flex justify-between items-center">
|
||||
<p
|
||||
className={`${styles.toolbarCap} ${styles.label} !self-center rounded-r-full w-fit`}
|
||||
>
|
||||
You're in {state.matches('Sketch') ? '2D' : '3D'}
|
||||
</p>
|
||||
<Popover.Button className="p-2 flex items-center justify-center rounded-sm bg-chalkboard-20 text-chalkboard-110 dark:bg-chalkboard-70 dark:text-chalkboard-20 border-none hover:bg-chalkboard-30 dark:hover:bg-chalkboard-60">
|
||||
<FontAwesomeIcon icon={faX} className="w-4 h-4" />
|
||||
</Popover.Button>
|
||||
</section>
|
||||
<section>
|
||||
<ToolbarButtons className="flex-wrap" />
|
||||
</section>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
@ -39,16 +39,16 @@ type ActionButtonProps =
|
||||
| ActionButtonAsElement
|
||||
|
||||
export const ActionButton = (props: ActionButtonProps) => {
|
||||
const classNames = `action-button m-0 group mono text-sm flex items-center gap-2 rounded-sm border-solid border border-chalkboard-30 hover:border-chalkboard-40 dark:border-chalkboard-70 dark:hover:border-chalkboard-60 dark:bg-chalkboard-90/50 p-[3px] text-chalkboard-100 dark:text-chalkboard-10 ${
|
||||
const classNames = `group mono text-base flex items-center gap-2 rounded-sm border border-chalkboard-40 dark:border-chalkboard-60 hover:border-liquid-40 dark:hover:bg-chalkboard-90 p-[3px] text-chalkboard-110 dark:text-chalkboard-10 hover:text-chalkboard-110 hover:dark:text-chalkboard-10 ${
|
||||
props.icon ? 'pr-2' : 'px-2'
|
||||
} ${props.className ? props.className : ''}`
|
||||
} ${props.className || ''}`
|
||||
|
||||
switch (props.Element) {
|
||||
case 'button': {
|
||||
// Note we have to destructure 'className' and 'Element' out of props
|
||||
// because we don't want to pass them to the button element;
|
||||
// the same is true for the other cases below.
|
||||
const { Element, icon, children, className: _className, ...rest } = props
|
||||
const { Element, icon, children, className, ...rest } = props
|
||||
return (
|
||||
<button className={classNames} {...rest}>
|
||||
{props.icon && <ActionIcon {...icon} />}
|
||||
@ -57,14 +57,7 @@ export const ActionButton = (props: ActionButtonProps) => {
|
||||
)
|
||||
}
|
||||
case 'link': {
|
||||
const {
|
||||
Element,
|
||||
to,
|
||||
icon,
|
||||
children,
|
||||
className: _className,
|
||||
...rest
|
||||
} = props
|
||||
const { Element, to, icon, children, className, ...rest } = props
|
||||
return (
|
||||
<Link to={to || paths.INDEX} className={classNames} {...rest}>
|
||||
{icon && <ActionIcon {...icon} />}
|
||||
@ -73,14 +66,7 @@ export const ActionButton = (props: ActionButtonProps) => {
|
||||
)
|
||||
}
|
||||
case 'externalLink': {
|
||||
const {
|
||||
Element,
|
||||
to,
|
||||
icon,
|
||||
children,
|
||||
className: _className,
|
||||
...rest
|
||||
} = props
|
||||
const { Element, to, icon, children, className, ...rest } = props
|
||||
return (
|
||||
<Link
|
||||
to={to || paths.INDEX}
|
||||
@ -94,7 +80,7 @@ export const ActionButton = (props: ActionButtonProps) => {
|
||||
)
|
||||
}
|
||||
default: {
|
||||
const { Element, icon, children, className: _className, ...rest } = props
|
||||
const { Element, icon, children, className, ...rest } = props
|
||||
if (!Element) throw new Error('Element is required')
|
||||
|
||||
return (
|
||||
|
@ -7,10 +7,10 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { CustomIcon, CustomIconName } from './CustomIcon'
|
||||
|
||||
const iconSizes = {
|
||||
xs: 12,
|
||||
sm: 14,
|
||||
md: 20,
|
||||
lg: 24,
|
||||
sm: 12,
|
||||
md: 14.4,
|
||||
lg: 20,
|
||||
xl: 28,
|
||||
}
|
||||
|
||||
export interface ActionIconProps extends React.PropsWithChildren {
|
||||
@ -30,14 +30,20 @@ export const ActionIcon = ({
|
||||
children,
|
||||
}: ActionIconProps) => {
|
||||
// By default, we reverse the icon color and background color in dark mode
|
||||
const computedIconClassName = `h-auto dark:text-energy-10 !group-disabled:text-chalkboard-60 !group-disabled:text-chalkboard-60 ${iconClassName}`
|
||||
const computedIconClassName =
|
||||
iconClassName ||
|
||||
`text-liquid-20 h-auto group-hover:text-liquid-10 hover:text-liquid-10 dark:text-chalkboard-100 dark:group-hover:text-chalkboard-100 dark:hover:text-chalkboard-100 group-disabled:bg-chalkboard-50 dark:group-disabled:bg-chalkboard-60 group-hover:group-disabled:bg-chalkboard-50 dark:group-hover:group-disabled:bg-chalkboard-50`
|
||||
|
||||
const computedBgClassName = `bg-chalkboard-20 dark:bg-chalkboard-90 !group-disabled:bg-chalkboard-30 !dark:group-disabled:bg-chalkboard-80 ${bgClassName}`
|
||||
const computedBgClassName =
|
||||
bgClassName ||
|
||||
`bg-chalkboard-100 group-hover:bg-chalkboard-90 hover:bg-chalkboard-90 dark:bg-liquid-20 dark:group-hover:bg-liquid-10 dark:hover:bg-liquid-10 group-disabled:bg-chalkboard-80 dark:group-disabled:bg-chalkboard-80`
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
`w-fit inline-grid place-content-center ${className} ` +
|
||||
`p-${
|
||||
size === 'xl' ? '2' : '1'
|
||||
} w-fit inline-grid place-content-center ${className} ` +
|
||||
computedBgClassName
|
||||
}
|
||||
>
|
||||
|
@ -5,9 +5,6 @@ import ProjectSidebarMenu from './ProjectSidebarMenu'
|
||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||
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,17 +20,15 @@ export const AppHeader = ({
|
||||
className = '',
|
||||
enableMenu = false,
|
||||
}: AppHeaderProps) => {
|
||||
const platform = usePlatform()
|
||||
const { commandBarSend } = useCommandsContext()
|
||||
const { auth } = useGlobalStateContext()
|
||||
const user = auth?.context?.user
|
||||
|
||||
return (
|
||||
<header
|
||||
className={
|
||||
'w-full grid ' +
|
||||
(showToolbar ? 'w-full grid ' : 'flex justify-between ') +
|
||||
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
|
||||
}
|
||||
>
|
||||
@ -43,31 +38,18 @@ export const AppHeader = ({
|
||||
file={project?.file}
|
||||
/>
|
||||
{/* Toolbar if the context deems it */}
|
||||
<div className="flex-grow flex justify-center max-w-lg md:max-w-xl lg:max-w-2xl xl:max-w-4xl 2xl:max-w-5xl">
|
||||
{showToolbar ? (
|
||||
{showToolbar && (
|
||||
<div className="max-w-lg md:max-w-xl lg:max-w-2xl xl:max-w-4xl 2xl:max-w-5xl">
|
||||
<Toolbar />
|
||||
) : (
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={() => commandBarSend({ type: 'Open' })}
|
||||
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+/'}
|
||||
</kbd>
|
||||
</ActionButton>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1 ml-auto">
|
||||
{/* If there are children, show them, otherwise show User menu */}
|
||||
{children || (
|
||||
<>
|
||||
<NetworkHealthIndicator />
|
||||
<UserSidebarMenu user={user} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* If there are children, show them, otherwise show User menu */}
|
||||
{children || (
|
||||
<div className="flex items-center gap-1 ml-auto">
|
||||
<NetworkHealthIndicator />
|
||||
<UserSidebarMenu user={user} />
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
@ -1,13 +1,13 @@
|
||||
.button {
|
||||
@apply flex justify-between items-center gap-2 px-2 py-1 text-left border-none rounded-sm;
|
||||
@apply font-mono text-xs font-bold select-none text-chalkboard-90;
|
||||
@apply ui-active:bg-energy-10/50 ui-active:text-inherit;
|
||||
@apply ui-active:bg-liquid-10/50 ui-active:text-liquid-90;
|
||||
@apply transition-colors ease-out;
|
||||
}
|
||||
|
||||
:global(.dark) .button {
|
||||
@apply text-chalkboard-30;
|
||||
@apply ui-active:bg-chalkboard-80 ui-active:text-energy-10;
|
||||
@apply ui-active:bg-chalkboard-80 ui-active:text-liquid-10;
|
||||
}
|
||||
|
||||
.button small {
|
||||
|
@ -30,10 +30,8 @@ export const CodeMenu = ({ children }: PropsWithChildren) => {
|
||||
<Menu.Button className="p-0 border-none relative">
|
||||
<ActionIcon
|
||||
icon={faEllipsis}
|
||||
className="p-1"
|
||||
size="sm"
|
||||
bgClassName={
|
||||
'bg-chalkboard-20 dark:bg-chalkboard-110 hover:bg-energy-10/50 hover:dark:bg-chalkboard-90 ui-active:bg-chalkboard-80 ui-active:dark:bg-chalkboard-90 rounded-sm'
|
||||
'bg-chalkboard-20 dark:bg-chalkboard-110 hover:bg-liquid-10/50 hover:dark:bg-chalkboard-90 ui-active:bg-chalkboard-80 ui-active:dark:bg-chalkboard-90 rounded'
|
||||
}
|
||||
iconClassName={'text-chalkboard-90 dark:text-chalkboard-40'}
|
||||
/>
|
||||
|
@ -24,17 +24,16 @@ 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 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 ' +
|
||||
'bg-chalkboard-30 dark:bg-chalkboard-90 group-open:bg-chalkboard-80 rounded ' +
|
||||
(iconClassNames?.bg || '')
|
||||
}
|
||||
iconClassName={
|
||||
'group-open:text-energy-10 ' + (iconClassNames?.icon || '')
|
||||
'text-chalkboard-90 dark:text-chalkboard-40 group-open:text-liquid-10 ' +
|
||||
(iconClassNames?.icon || '')
|
||||
}
|
||||
/>
|
||||
{title}
|
||||
@ -60,9 +59,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}
|
||||
|
290
src/components/CommandBar.tsx
Normal file
@ -0,0 +1,290 @@
|
||||
import { Combobox, Dialog, Transition } from '@headlessui/react'
|
||||
import {
|
||||
Dispatch,
|
||||
Fragment,
|
||||
SetStateAction,
|
||||
createContext,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import { ActionIcon } from './ActionIcon'
|
||||
import { faSearch } from '@fortawesome/free-solid-svg-icons'
|
||||
import Fuse from 'fuse.js'
|
||||
import { Command, SubCommand } from '../lib/commands'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
|
||||
export type SortedCommand = {
|
||||
item: Partial<Command | SubCommand> & { name: string }
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
const addCommands = (newCommands: Command[]) => {
|
||||
internalSetCommands((prevCommands) => [...newCommands, ...prevCommands])
|
||||
}
|
||||
const removeCommands = (newCommands: Command[]) => {
|
||||
internalSetCommands((prevCommands) =>
|
||||
prevCommands.filter((command) => !newCommands.includes(command))
|
||||
)
|
||||
}
|
||||
|
||||
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<SortedCommand | null>(
|
||||
null
|
||||
)
|
||||
// keep track of the current subcommand index
|
||||
const [subCommandIndex, setSubCommandIndex] = useState<number>()
|
||||
const [subCommandData, setSubCommandData] = useState<{
|
||||
[key: string]: string
|
||||
}>({})
|
||||
|
||||
// if the subcommand index is null, we're not in a subcommand
|
||||
const inSubCommand =
|
||||
selectedCommand &&
|
||||
'meta' in selectedCommand.item &&
|
||||
selectedCommand.item.meta?.args !== undefined &&
|
||||
subCommandIndex !== undefined
|
||||
const currentSubCommand =
|
||||
inSubCommand && 'meta' in selectedCommand.item
|
||||
? selectedCommand.item.meta?.args[subCommandIndex]
|
||||
: undefined
|
||||
|
||||
const [query, setQuery] = useState('')
|
||||
|
||||
const availableCommands =
|
||||
inSubCommand && currentSubCommand
|
||||
? currentSubCommand.type === 'string'
|
||||
? query
|
||||
? [{ name: query }]
|
||||
: currentSubCommand.options
|
||||
: currentSubCommand.options
|
||||
: commands
|
||||
|
||||
const fuse = new Fuse(availableCommands || [], {
|
||||
keys: ['name', 'description'],
|
||||
})
|
||||
|
||||
const filteredCommands = query
|
||||
? fuse.search(query)
|
||||
: availableCommands?.map((c) => ({ item: c } as SortedCommand))
|
||||
|
||||
function clearState() {
|
||||
setQuery('')
|
||||
setCommandBarOpen(false)
|
||||
setSelectedCommand(null)
|
||||
setSubCommandIndex(undefined)
|
||||
setSubCommandData({})
|
||||
}
|
||||
|
||||
function handleCommandSelection(entry: SortedCommand) {
|
||||
// If we have subcommands and have not yet gathered all the
|
||||
// data required from them, set the selected command to the
|
||||
// current command and increment the subcommand index
|
||||
if (selectedCommand === null && 'meta' in entry.item && entry.item.meta) {
|
||||
setSelectedCommand(entry)
|
||||
setSubCommandIndex(0)
|
||||
setQuery('')
|
||||
return
|
||||
}
|
||||
|
||||
const { item } = entry
|
||||
// If we have just selected a command with no subcommands, run it
|
||||
const isCommandWithoutSubcommands =
|
||||
'callback' in item && !('meta' in item && item.meta)
|
||||
if (isCommandWithoutSubcommands) {
|
||||
if (item.callback === undefined) return
|
||||
item.callback()
|
||||
setCommandBarOpen(false)
|
||||
return
|
||||
}
|
||||
|
||||
// If we have subcommands and have not yet gathered all the
|
||||
// data required from them, set the selected command to the
|
||||
// current command and increment the subcommand index
|
||||
if (
|
||||
selectedCommand &&
|
||||
subCommandIndex !== undefined &&
|
||||
'meta' in selectedCommand.item
|
||||
) {
|
||||
const subCommand = selectedCommand.item.meta?.args[subCommandIndex]
|
||||
|
||||
if (subCommand) {
|
||||
const newSubCommandData = {
|
||||
...subCommandData,
|
||||
[subCommand.name]: item.name,
|
||||
}
|
||||
const newSubCommandIndex = subCommandIndex + 1
|
||||
|
||||
// If we have subcommands and have gathered all the data required
|
||||
// from them, run the command with the gathered data
|
||||
if (
|
||||
selectedCommand.item.callback &&
|
||||
selectedCommand.item.meta?.args.length === newSubCommandIndex
|
||||
) {
|
||||
selectedCommand.item.callback(newSubCommandData)
|
||||
setCommandBarOpen(false)
|
||||
} else {
|
||||
// Otherwise, set the subcommand data and increment the subcommand index
|
||||
setSubCommandData(newSubCommandData)
|
||||
setSubCommandIndex(newSubCommandIndex)
|
||||
setQuery('')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getDisplayValue(command: Command) {
|
||||
if (command.meta?.displayValue === undefined || !command.meta.args)
|
||||
return command.name
|
||||
return command.meta?.displayValue(
|
||||
command.meta.args.map((c) =>
|
||||
subCommandData[c.name] ? subCommandData[c.name] : `<${c.name}>`
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Transition.Root
|
||||
show={
|
||||
commandBarOpen &&
|
||||
availableCommands?.length !== undefined &&
|
||||
availableCommands.length > 0
|
||||
}
|
||||
as={Fragment}
|
||||
afterLeave={() => clearState()}
|
||||
>
|
||||
<Dialog
|
||||
onClose={() => {
|
||||
setCommandBarOpen(false)
|
||||
clearState()
|
||||
}}
|
||||
className="fixed inset-0 z-40 overflow-y-auto p-4 pt-[25vh]"
|
||||
>
|
||||
<Transition.Child
|
||||
enter="duration-100 ease-out"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="duration-75 ease-in"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
as={Fragment}
|
||||
>
|
||||
<Dialog.Overlay className="fixed inset-0 bg-chalkboard-10/70 dark:bg-chalkboard-110/50" />
|
||||
</Transition.Child>
|
||||
<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"
|
||||
as={Fragment}
|
||||
>
|
||||
<Combobox
|
||||
value={selectedCommand}
|
||||
onChange={handleCommandSelection}
|
||||
className="relative w-full max-w-xl p-2 mx-auto border rounded shadow-lg bg-chalkboard-10 dark:bg-chalkboard-100 dark:border-chalkboard-70"
|
||||
as="div"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<ActionIcon icon={faSearch} size="xl" className="rounded-sm" />
|
||||
<div>
|
||||
{inSubCommand && (
|
||||
<p className="text-liquid-70 dark:text-liquid-30">
|
||||
{selectedCommand.item &&
|
||||
getDisplayValue(selectedCommand.item as Command)}
|
||||
</p>
|
||||
)}
|
||||
<Combobox.Input
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
className="w-full bg-transparent focus:outline-none"
|
||||
onKeyDown={(event) => {
|
||||
if (event.metaKey && event.key === 'k')
|
||||
setCommandBarOpen(false)
|
||||
if (
|
||||
inSubCommand &&
|
||||
event.key === 'Backspace' &&
|
||||
!event.currentTarget.value
|
||||
) {
|
||||
setSubCommandIndex(subCommandIndex - 1)
|
||||
setSelectedCommand(null)
|
||||
}
|
||||
}}
|
||||
displayValue={(command: SortedCommand) =>
|
||||
command !== null ? command.item.name : ''
|
||||
}
|
||||
placeholder={
|
||||
inSubCommand
|
||||
? `Enter <${currentSubCommand?.name}>`
|
||||
: 'Search for a command'
|
||||
}
|
||||
value={query}
|
||||
autoCapitalize="off"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
spellCheck="false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Combobox.Options static className="overflow-y-auto max-h-96">
|
||||
{filteredCommands?.map((commandResult) => (
|
||||
<Combobox.Option
|
||||
key={commandResult.item.name}
|
||||
value={commandResult}
|
||||
className="px-2 py-1 my-2 first:mt-4 last:mb-4 ui-active:bg-liquid-10 dark:ui-active:bg-liquid-90"
|
||||
>
|
||||
<p>{commandResult.item.name}</p>
|
||||
{(commandResult.item as SubCommand).description && (
|
||||
<p className="mt-0.5 text-liquid-70 dark:text-liquid-30 text-sm">
|
||||
{(commandResult.item as SubCommand).description}
|
||||
</p>
|
||||
)}
|
||||
</Combobox.Option>
|
||||
))}
|
||||
</Combobox.Options>
|
||||
</Combobox>
|
||||
</Transition.Child>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export default CommandBarProvider
|
@ -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
|
@ -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
|
@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -1,22 +1,14 @@
|
||||
export type CustomIconName =
|
||||
| 'arrowDown'
|
||||
| 'arrowLeft'
|
||||
| 'arrowRight'
|
||||
| 'arrowUp'
|
||||
| 'checkmark'
|
||||
| 'close'
|
||||
| 'createFile'
|
||||
| 'createFolder'
|
||||
| 'equal'
|
||||
| 'exit'
|
||||
| 'extrude'
|
||||
| 'file'
|
||||
| 'filePlus'
|
||||
| 'folder'
|
||||
| 'folderPlus'
|
||||
| 'gear'
|
||||
| 'horizontal'
|
||||
| 'line'
|
||||
| 'move'
|
||||
| 'parallel'
|
||||
| 'search'
|
||||
| 'sketch'
|
||||
| 'vertical'
|
||||
|
||||
@ -27,7 +19,7 @@ export const CustomIcon = ({
|
||||
name: CustomIconName
|
||||
} & React.SVGProps<SVGSVGElement>) => {
|
||||
switch (name) {
|
||||
case 'arrowDown':
|
||||
case 'createFile':
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
@ -38,12 +30,12 @@ export const CustomIcon = ({
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M10 17.7071L9.64648 17.3535L6.14648 13.8535L6.85359 13.1464L9.50004 15.7929V2.99997H10.5V15.7929L13.1465 13.1464L13.8536 13.8535L10.3536 17.3535L10 17.7071Z"
|
||||
d="M4 3H4.5H11H11.2071L11.3536 3.14645L15.8536 7.64646L16 7.7929V8.00001V11.3773C15.6992 11.1362 15.3628 10.9376 15 10.7908V8.50001H11H10.5V8.00001V4H5V16H9.79076C9.93763 16.3628 10.1362 16.6992 10.3773 17H4.5H4V16.5V3.5V3ZM11.5 4.70711L14.2929 7.50001H11.5V4.70711ZM13 12V14H11V15H13V17H14V15H16V14H14V12H13Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
case 'arrowLeft':
|
||||
case 'createFolder':
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
@ -54,71 +46,7 @@ export const CustomIcon = ({
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M2.29291 10L2.64646 9.64645L6.14646 6.14645L6.85357 6.85356L4.20712 9.50001L17 9.50001V10.5L4.20712 10.5L6.85357 13.1465L6.14646 13.8536L2.64646 10.3536L2.29291 10Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
case 'arrowRight':
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M17.7071 10L17.3536 10.3536L13.8536 13.8536L13.1464 13.1465L15.7929 10.5H3V9.50001H15.7929L13.1464 6.85356L13.8536 6.14645L17.3536 9.64645L17.7071 10Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
case 'arrowUp':
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M10 2.29288L10.3536 2.64643L13.8536 6.14643L13.1465 6.85354L10.5 4.20709V17H9.50004V4.20709L6.85359 6.85354L6.14648 6.14643L9.64648 2.64643L10 2.29288Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</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
|
||||
{...props}
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M9.2929 10L6.46448 7.17158L7.17158 6.46448L10 9.2929L12.8284 6.46448L13.5355 7.17158L10.7071 10L13.5355 12.8284L12.8284 13.5355L10 10.7071L7.17158 13.5355L6.46448 12.8284L9.2929 10Z"
|
||||
d="M3.5 3.5H4H7H7.16667L7.3 3.6L9.16667 5H16H16.5V5.5V7.5V10.3773C16.1992 10.1362 15.8628 9.93763 15.5 9.79076V8H4.5V15.5H10.5351C10.7529 15.8764 11.0302 16.2141 11.3542 16.5H4H3.5V16V7.5V4V3.5ZM4.5 4.5V7H15.5V6H9H8.83333L8.7 5.9L6.83333 4.5H4.5ZM13.5 11V13H11.5V14H13.5V16H14.5V14H16.5V13H14.5V11H13.5Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
@ -137,6 +65,21 @@ export const CustomIcon = ({
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
case 'exit':
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M17 10L3 10M3 10L6.5 6.5M3 10L6.5 13.5"
|
||||
stroke="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
case 'extrude':
|
||||
return (
|
||||
<svg
|
||||
@ -162,74 +105,8 @@ export const CustomIcon = ({
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M4 3H4.5H11H11.2071L11.3536 3.14645L15.8536 7.64646L16 7.7929V8.00001V16.5V17H15.5H4.5H4V16.5V3.5V3ZM5 4V16H15V8.50001H11H10.5V8.00001V4H5ZM11.5 4.70711L14.2929 7.50001H11.5V4.70711Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
case 'filePlus':
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M4 3H4.5H11H11.2071L11.3536 3.14645L15.8536 7.64646L16 7.7929V8.00001V11.3773C15.6992 11.1362 15.3628 10.9376 15 10.7908V8.50001H11H10.5V8.00001V4H5V16H9.79076C9.93763 16.3628 10.1362 16.6992 10.3773 17H4.5H4V16.5V3.5V3ZM11.5 4.70711L14.2929 7.50001H11.5V4.70711ZM13 12V14H11V15H13V17H14V15H16V14H14V12H13Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
case 'folder':
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M3.5 3.5H4H7H7.16667L7.3 3.6L9.16667 5H16H16.5V5.5V7.5V16V16.5H16H4H3.5V16V7.5V4V3.5ZM4.5 4.5V7H15.5V6H9H8.83333L8.7 5.9L6.83333 4.5H4.5ZM15.5 8H4.5V15.5H15.5V8Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
case 'folderPlus':
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M3.5 3.5H4H7H7.16667L7.3 3.6L9.16667 5H16H16.5V5.5V7.5V10.3773C16.1992 10.1362 15.8628 9.93763 15.5 9.79076V8H4.5V15.5H10.5351C10.7529 15.8764 11.0302 16.2141 11.3542 16.5H4H3.5V16V7.5V4V3.5ZM4.5 4.5V7H15.5V6H9H8.83333L8.7 5.9L6.83333 4.5H4.5ZM13.5 11V13H11.5V14H13.5V16H14.5V14H16.5V13H14.5V11H13.5Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
case 'gear':
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M8.61477 3.0884L5.87402 4.67077L6.50004 5.75505L5.25004 7.92011H4.0047V11.07H5.25004L6.50004 13.2351L5.86973 14.3268L8.62776 15.9191L9.24503 14.85H11.745L12.3647 15.9234L15.1416 14.3202L14.5151 13.2351L15.7651 11.07H16.9951V7.92011H15.7651L14.5151 5.75505L15.1373 4.67741L12.3778 3.08423L11.7451 4.18012H9.24508L8.61477 3.0884ZM10.4999 13C12.4329 13 13.9999 11.433 13.9999 9.50003C13.9999 7.56703 12.4329 6.00003 10.4999 6.00003C8.56687 6.00003 6.99986 7.56703 6.99986 9.50003C6.99986 11.433 8.56687 13 10.4999 13Z"
|
||||
fill="currentColor"
|
||||
d="M11 3.5H4.5V16.5H15.5V8.00001M11 3.5L15.5 8.00001M11 3.5V8.00001H15.5"
|
||||
stroke="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
@ -297,22 +174,6 @@ export const CustomIcon = ({
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
case 'search':
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M14.016 9.00482C14.016 10.662 12.6731 12.0048 11.0172 12.0048C9.3613 12.0048 8.01841 10.662 8.01841 9.00482C8.01841 7.34768 9.3613 6.00482 11.0172 6.00482C12.6731 6.00482 14.016 7.34768 14.016 9.00482ZM15.016 9.00482C15.016 11.214 13.2257 13.0048 11.0172 13.0048C10.082 13.0048 9.22178 12.6837 8.54074 12.1456L5.6912 14.9952L4.98409 14.2881L7.83921 11.433C7.32431 10.7597 7.01841 9.91799 7.01841 9.00482C7.01841 6.79568 8.80873 5.00482 11.0172 5.00482C13.2257 5.00482 15.016 6.79568 15.016 9.00482Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
case 'sketch':
|
||||
return (
|
||||
<svg
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { Dialog } from '@headlessui/react'
|
||||
import { useStore } from '../useStore'
|
||||
import { ActionButton } from './ActionButton'
|
||||
import { faX } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
const DownloadAppBanner = () => {
|
||||
const { isBannerDismissed, setBannerDismissed } = useStore((s) => ({
|
||||
@ -10,48 +11,44 @@ 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: faX,
|
||||
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>
|
||||
)
|
||||
|
@ -118,8 +118,6 @@ export const ExportButton = ({ children, className }: ExportButtonProps) => {
|
||||
Element="button"
|
||||
icon={{
|
||||
icon: faFileExport,
|
||||
className: 'p-1',
|
||||
size: 'sm',
|
||||
iconClassName: className?.icon,
|
||||
bgClassName: className?.bg,
|
||||
}}
|
||||
@ -214,7 +212,6 @@ export const ExportButton = ({ children, className }: ExportButtonProps) => {
|
||||
onClick={closeModal}
|
||||
icon={{
|
||||
icon: faXmark,
|
||||
className: 'p-1',
|
||||
bgClassName: 'bg-destroy-80',
|
||||
iconClassName:
|
||||
'text-destroy-20 group-hover:text-destroy-10 hover:text-destroy-10',
|
||||
@ -226,7 +223,7 @@ export const ExportButton = ({ children, className }: ExportButtonProps) => {
|
||||
<ActionButton
|
||||
Element="button"
|
||||
type="submit"
|
||||
icon={{ icon: faFileExport, className: 'p-1' }}
|
||||
icon={{ icon: faFileExport }}
|
||||
>
|
||||
Export
|
||||
</ActionButton>
|
||||
|
@ -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
|
||||
|
@ -325,17 +325,16 @@ export const FileTree = ({
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className="flex items-center gap-1 px-4 py-1 bg-chalkboard-20/50 dark:bg-chalkboard-80/50 border-b border-b-chalkboard-30 dark:border-b-chalkboard-80">
|
||||
<div className="flex items-center gap-1 px-4 py-1 bg-chalkboard-30/50 dark:bg-chalkboard-70/50">
|
||||
<h2 className="flex-1 m-0 p-0 text-sm mono">Files</h2>
|
||||
<ActionButton
|
||||
Element="button"
|
||||
icon={{
|
||||
icon: 'filePlus',
|
||||
icon: 'createFile',
|
||||
iconClassName: '!text-energy-80 dark:!text-energy-20',
|
||||
bgClassName:
|
||||
'bg-chalkboard-20/50 hover:bg-energy-10/50 dark:hover:bg-transparent',
|
||||
bgClassName: 'hover:bg-energy-10/50 dark:hover:bg-transparent',
|
||||
}}
|
||||
className="!p-0 bg-transparent !outline-none"
|
||||
className="!p-0 border-none bg-transparent !outline-none"
|
||||
onClick={createFile}
|
||||
>
|
||||
<Tooltip position="inlineStart" delay={750}>
|
||||
@ -346,12 +345,11 @@ export const FileTree = ({
|
||||
<ActionButton
|
||||
Element="button"
|
||||
icon={{
|
||||
icon: 'folderPlus',
|
||||
icon: 'createFolder',
|
||||
iconClassName: '!text-energy-80 dark:!text-energy-20',
|
||||
bgClassName:
|
||||
'bg-chalkboard-20/50 hover:bg-energy-10/50 dark:hover:bg-transparent',
|
||||
bgClassName: 'hover:bg-energy-10/50 dark:hover:bg-transparent',
|
||||
}}
|
||||
className="!p-0 bg-transparent !outline-none"
|
||||
className="!p-0 border-none bg-transparent !outline-none"
|
||||
onClick={createFolder}
|
||||
>
|
||||
<Tooltip position="inlineStart" delay={750}>
|
||||
|
@ -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 {
|
||||
authCommandBarMeta,
|
||||
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,
|
||||
settingsCommandBarMeta,
|
||||
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,10 +81,11 @@ export const GlobalStateProvider = ({
|
||||
})
|
||||
|
||||
useStateMachineCommands({
|
||||
machineId: 'settings',
|
||||
state: settingsState,
|
||||
send: settingsSend,
|
||||
commandBarConfig: settingsCommandBarConfig,
|
||||
commands,
|
||||
owner: 'settings',
|
||||
commandBarMeta: settingsCommandBarMeta,
|
||||
})
|
||||
|
||||
// Listen for changes to the system theme and update the app theme accordingly
|
||||
@ -112,10 +121,11 @@ export const GlobalStateProvider = ({
|
||||
})
|
||||
|
||||
useStateMachineCommands({
|
||||
machineId: 'auth',
|
||||
state: authState,
|
||||
send: authSend,
|
||||
commandBarConfig: authCommandBarConfig,
|
||||
commands,
|
||||
commandBarMeta: authCommandBarMeta,
|
||||
owner: 'auth',
|
||||
})
|
||||
|
||||
return (
|
||||
|