Merge branch 'main' into pierremtb/issue1217
16
.github/workflows/ci.yml
vendored
@ -107,7 +107,7 @@ jobs:
|
|||||||
echo "$(jq --arg url 'https://dl.kittycad.io/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
|
'.tauri.updater.endpoints[]=$url' src-tauri/tauri.release.conf.json --indent 2)" > src-tauri/tauri.release.conf.json
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v4
|
||||||
if: github.event_name == 'schedule'
|
if: github.event_name == 'schedule'
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
@ -128,7 +128,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: actions/download-artifact@v3
|
- uses: actions/download-artifact@v4
|
||||||
if: github.event_name == 'schedule'
|
if: github.event_name == 'schedule'
|
||||||
|
|
||||||
- name: Copy updated .json files
|
- name: Copy updated .json files
|
||||||
@ -243,7 +243,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
args: "${{ matrix.os == 'macos-latest' && '--target universal-apple-darwin' || '' }} ${{ env.TAURI_CONF_ARGS }}"
|
args: "${{ matrix.os == 'macos-latest' && '--target universal-apple-darwin' || '' }} ${{ env.TAURI_CONF_ARGS }}"
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v4
|
||||||
env:
|
env:
|
||||||
PREFIX: ${{ matrix.os == 'macos-latest' && 'src-tauri/target/universal-apple-darwin' || 'src-tauri/target' }}
|
PREFIX: ${{ matrix.os == 'macos-latest' && 'src-tauri/target/universal-apple-darwin' || 'src-tauri/target' }}
|
||||||
MODE: ${{ env.BUILD_RELEASE == 'true' && 'release' || 'debug' }}
|
MODE: ${{ env.BUILD_RELEASE == 'true' && 'release' || 'debug' }}
|
||||||
@ -273,7 +273,7 @@ jobs:
|
|||||||
NOTES: ${{ github.event_name == 'release' && github.event.release.body || format('Nightly build, commit {0}', github.sha) }}
|
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' }}
|
BUCKET_DIR: ${{ github.event_name == 'release' && 'dl.kittycad.io/releases/modeling-app' || 'dl.kittycad.io/releases/modeling-app/nightly' }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/download-artifact@v3
|
- uses: actions/download-artifact@v4
|
||||||
|
|
||||||
- name: Generate the update static endpoint
|
- name: Generate the update static endpoint
|
||||||
run: |
|
run: |
|
||||||
@ -351,12 +351,12 @@ jobs:
|
|||||||
credentials_json: '${{ secrets.GOOGLE_CLOUD_DL_SA }}'
|
credentials_json: '${{ secrets.GOOGLE_CLOUD_DL_SA }}'
|
||||||
|
|
||||||
- name: Set up Google Cloud SDK
|
- name: Set up Google Cloud SDK
|
||||||
uses: google-github-actions/setup-gcloud@v1.1.1
|
uses: google-github-actions/setup-gcloud@v2.0.0
|
||||||
with:
|
with:
|
||||||
project_id: kittycadapi
|
project_id: kittycadapi
|
||||||
|
|
||||||
- name: Upload release files to public bucket
|
- name: Upload release files to public bucket
|
||||||
uses: google-github-actions/upload-cloud-storage@v1.0.3
|
uses: google-github-actions/upload-cloud-storage@v2.0.0
|
||||||
with:
|
with:
|
||||||
path: artifact
|
path: artifact
|
||||||
glob: '*/*itty*'
|
glob: '*/*itty*'
|
||||||
@ -364,13 +364,13 @@ jobs:
|
|||||||
destination: ${{ env.BUCKET_DIR }}/${{ env.VERSION }}
|
destination: ${{ env.BUCKET_DIR }}/${{ env.VERSION }}
|
||||||
|
|
||||||
- name: Upload update endpoint to public bucket
|
- name: Upload update endpoint to public bucket
|
||||||
uses: google-github-actions/upload-cloud-storage@v1.0.3
|
uses: google-github-actions/upload-cloud-storage@v2.0.0
|
||||||
with:
|
with:
|
||||||
path: last_update.json
|
path: last_update.json
|
||||||
destination: ${{ env.BUCKET_DIR }}
|
destination: ${{ env.BUCKET_DIR }}
|
||||||
|
|
||||||
- name: Upload download endpoint to public bucket
|
- name: Upload download endpoint to public bucket
|
||||||
uses: google-github-actions/upload-cloud-storage@v1.0.3
|
uses: google-github-actions/upload-cloud-storage@v2.0.0
|
||||||
with:
|
with:
|
||||||
path: last_download.json
|
path: last_download.json
|
||||||
destination: ${{ env.BUCKET_DIR }}
|
destination: ${{ env.BUCKET_DIR }}
|
||||||
|
|||||||
6
.github/workflows/playwright.yml
vendored
@ -33,7 +33,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
CI: true
|
CI: true
|
||||||
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
|
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v4
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
name: playwright-report
|
name: playwright-report
|
||||||
@ -68,7 +68,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
CI: true
|
CI: true
|
||||||
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
|
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v4
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
name: playwright-report
|
name: playwright-report
|
||||||
@ -106,7 +106,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
CI: true
|
CI: true
|
||||||
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
|
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v4
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
name: playwright-report
|
name: playwright-report
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||

|

|
||||||
|
|
||||||
## KittyCAD Modeling App
|
## KittyCAD Modeling App
|
||||||
|
|
||||||
|
|||||||
@ -8,275 +8,275 @@ property float z
|
|||||||
element face 68
|
element face 68
|
||||||
property list uchar uint vertex_indices
|
property list uchar uint vertex_indices
|
||||||
end_header
|
end_header
|
||||||
0 0 4
|
0 0 4
|
||||||
0 0 0
|
0 0 0
|
||||||
0 -1 4
|
0 -1 4
|
||||||
0 -1 4
|
0 -1 4
|
||||||
0 0 0
|
0 0 0
|
||||||
0 -1 0
|
0 -1 0
|
||||||
0 -1 4
|
0 -1 4
|
||||||
0 -1 0
|
0 -1 0
|
||||||
3.0950184 -1 4
|
3.0950184 -1 4
|
||||||
3.0950184 -1 4
|
3.0950184 -1 4
|
||||||
0 -1 0
|
0 -1 0
|
||||||
3.0950184 -1 0
|
3.0950184 -1 0
|
||||||
3.0950184 -1 4
|
3.0950184 -1 4
|
||||||
3.0950184 -1 0
|
3.0950184 -1 0
|
||||||
5.9513144 -3 4
|
5.9513144 -3 4
|
||||||
5.9513144 -3 4
|
5.9513144 -3 4
|
||||||
3.0950184 -1 0
|
3.0950184 -1 0
|
||||||
5.9513144 -3 0
|
5.9513144 -3 0
|
||||||
5.9513144 -3 4
|
5.9513144 -3 4
|
||||||
5.9513144 -3 0
|
5.9513144 -3 0
|
||||||
9.5 -3 4
|
9.5 -3 4
|
||||||
9.5 -3 4
|
9.5 -3 4
|
||||||
5.9513144 -3 0
|
5.9513144 -3 0
|
||||||
9.5 -3 0
|
9.5 -3 0
|
||||||
9.5 -3 4
|
9.5 -3 4
|
||||||
9.5 -3 0
|
9.5 -3 0
|
||||||
9.5 -2.5 4
|
9.5 -2.5 4
|
||||||
9.5 -2.5 4
|
9.5 -2.5 4
|
||||||
9.5 -3 0
|
9.5 -3 0
|
||||||
9.5 -2.5 0
|
9.5 -2.5 0
|
||||||
9.5 -2.5 4
|
9.5 -2.5 4
|
||||||
9.5 -2.5 0
|
9.5 -2.5 0
|
||||||
6.108964 -2.5 4
|
6.108964 -2.5 4
|
||||||
6.108964 -2.5 4
|
6.108964 -2.5 4
|
||||||
9.5 -2.5 0
|
9.5 -2.5 0
|
||||||
6.108964 -2.5 0
|
6.108964 -2.5 0
|
||||||
3.4311862 -0.625 4
|
3.4311862 -0.625 4
|
||||||
4.323779 -1.25 4
|
4.323779 -1.25 4
|
||||||
4.323779 -1.25 0
|
4.323779 -1.25 0
|
||||||
4.323779 -1.25 4
|
4.323779 -1.25 4
|
||||||
6.108964 -2.5 4
|
6.108964 -2.5 4
|
||||||
6.108964 -2.5 0
|
6.108964 -2.5 0
|
||||||
3.4311862 -0.625 0
|
3.4311862 -0.625 0
|
||||||
2.5385938 0 0
|
2.5385938 0 0
|
||||||
2.5385938 0 4
|
2.5385938 0 4
|
||||||
3.4311862 -0.625 4
|
3.4311862 -0.625 4
|
||||||
3.4311862 -0.625 0
|
3.4311862 -0.625 0
|
||||||
2.5385938 0 4
|
2.5385938 0 4
|
||||||
4.323779 -1.25 4
|
4.323779 -1.25 4
|
||||||
6.108964 -2.5 0
|
6.108964 -2.5 0
|
||||||
4.323779 -1.25 0
|
4.323779 -1.25 0
|
||||||
3.4311862 -0.625 0
|
3.4311862 -0.625 0
|
||||||
3.4311862 -0.625 4
|
3.4311862 -0.625 4
|
||||||
4.323779 -1.25 0
|
4.323779 -1.25 0
|
||||||
3.342784 0.375 4
|
3.342784 0.375 4
|
||||||
2.5385938 0 4
|
2.5385938 0 4
|
||||||
2.5385938 0 0
|
2.5385938 0 0
|
||||||
4.146974 0.75 4
|
4.146974 0.75 4
|
||||||
3.342784 0.375 4
|
3.342784 0.375 4
|
||||||
3.342784 0.375 0
|
3.342784 0.375 0
|
||||||
3.342784 0.375 0
|
3.342784 0.375 0
|
||||||
4.146974 0.75 0
|
4.146974 0.75 0
|
||||||
4.146974 0.75 4
|
4.146974 0.75 4
|
||||||
4.146974 0.75 0
|
4.146974 0.75 0
|
||||||
5.755354 1.5 0
|
5.755354 1.5 0
|
||||||
5.755354 1.5 4
|
5.755354 1.5 4
|
||||||
3.342784 0.375 4
|
3.342784 0.375 4
|
||||||
2.5385938 0 0
|
2.5385938 0 0
|
||||||
3.342784 0.375 0
|
3.342784 0.375 0
|
||||||
5.755354 1.5 4
|
5.755354 1.5 4
|
||||||
4.146974 0.75 4
|
4.146974 0.75 4
|
||||||
4.146974 0.75 0
|
4.146974 0.75 0
|
||||||
5.755354 1.5 4
|
5.755354 1.5 4
|
||||||
5.755354 1.5 0
|
5.755354 1.5 0
|
||||||
9.5 1.5 4
|
9.5 1.5 4
|
||||||
9.5 1.5 4
|
9.5 1.5 4
|
||||||
5.755354 1.5 0
|
5.755354 1.5 0
|
||||||
9.5 1.5 0
|
9.5 1.5 0
|
||||||
9.5 1.5 4
|
9.5 1.5 4
|
||||||
9.5 1.5 0
|
9.5 1.5 0
|
||||||
9.5 2 4
|
9.5 2 4
|
||||||
9.5 2 4
|
9.5 2 4
|
||||||
9.5 1.5 0
|
9.5 1.5 0
|
||||||
9.5 2 0
|
9.5 2 0
|
||||||
9.5 2 4
|
9.5 2 4
|
||||||
9.5 2 0
|
9.5 2 0
|
||||||
5.644507 2 4
|
5.644507 2 4
|
||||||
5.644507 2 4
|
5.644507 2 4
|
||||||
9.5 2 0
|
9.5 2 0
|
||||||
5.644507 2 0
|
5.644507 2 0
|
||||||
5.644507 2 4
|
5.644507 2 4
|
||||||
5.644507 2 0
|
5.644507 2 0
|
||||||
3.5 1 4
|
3.5 1 4
|
||||||
3.5 1 4
|
3.5 1 4
|
||||||
5.644507 2 0
|
5.644507 2 0
|
||||||
3.5 1 0
|
3.5 1 0
|
||||||
3.5 1 4
|
3.5 1 4
|
||||||
3.5 1 0
|
3.5 1 0
|
||||||
0 1 4
|
0 1 4
|
||||||
0 1 4
|
0 1 4
|
||||||
3.5 1 0
|
3.5 1 0
|
||||||
0 1 0
|
0 1 0
|
||||||
0 1 4
|
0 1 4
|
||||||
0 1 0
|
0 1 0
|
||||||
0 0 4
|
0 0 4
|
||||||
0 0 4
|
0 0 4
|
||||||
0 1 0
|
0 1 0
|
||||||
0 0 0
|
0 0 0
|
||||||
3.342784 0.375 0
|
3.342784 0.375 0
|
||||||
2.5385938 0 0
|
2.5385938 0 0
|
||||||
3.5 1 0
|
3.5 1 0
|
||||||
3.4311862 -0.625 0
|
3.4311862 -0.625 0
|
||||||
4.323779 -1.25 0
|
4.323779 -1.25 0
|
||||||
3.0950184 -1 0
|
3.0950184 -1 0
|
||||||
3.342784 0.375 0
|
3.342784 0.375 0
|
||||||
3.5 1 0
|
3.5 1 0
|
||||||
4.146974 0.75 0
|
4.146974 0.75 0
|
||||||
4.323779 -1.25 0
|
4.323779 -1.25 0
|
||||||
5.9513144 -3 0
|
5.9513144 -3 0
|
||||||
3.0950184 -1 0
|
3.0950184 -1 0
|
||||||
0 -1 0
|
0 -1 0
|
||||||
2.5385938 0 0
|
2.5385938 0 0
|
||||||
3.0950184 -1 0
|
3.0950184 -1 0
|
||||||
0 -1 0
|
0 -1 0
|
||||||
0 0 0
|
0 0 0
|
||||||
2.5385938 0 0
|
2.5385938 0 0
|
||||||
9.5 -3 0
|
9.5 -3 0
|
||||||
6.108964 -2.5 0
|
6.108964 -2.5 0
|
||||||
9.5 -2.5 0
|
9.5 -2.5 0
|
||||||
9.5 -3 0
|
9.5 -3 0
|
||||||
5.9513144 -3 0
|
5.9513144 -3 0
|
||||||
6.108964 -2.5 0
|
6.108964 -2.5 0
|
||||||
5.9513144 -3 0
|
5.9513144 -3 0
|
||||||
4.323779 -1.25 0
|
4.323779 -1.25 0
|
||||||
6.108964 -2.5 0
|
6.108964 -2.5 0
|
||||||
5.644507 2 0
|
5.644507 2 0
|
||||||
5.755354 1.5 0
|
5.755354 1.5 0
|
||||||
4.146974 0.75 0
|
4.146974 0.75 0
|
||||||
3.0950184 -1 0
|
3.0950184 -1 0
|
||||||
2.5385938 0 0
|
2.5385938 0 0
|
||||||
3.4311862 -0.625 0
|
3.4311862 -0.625 0
|
||||||
4.146974 0.75 0
|
4.146974 0.75 0
|
||||||
3.5 1 0
|
3.5 1 0
|
||||||
5.644507 2 0
|
5.644507 2 0
|
||||||
9.5 1.5 0
|
9.5 1.5 0
|
||||||
5.755354 1.5 0
|
5.755354 1.5 0
|
||||||
9.5 2 0
|
9.5 2 0
|
||||||
5.755354 1.5 0
|
5.755354 1.5 0
|
||||||
5.644507 2 0
|
5.644507 2 0
|
||||||
9.5 2 0
|
9.5 2 0
|
||||||
2.5385938 0 0
|
2.5385938 0 0
|
||||||
0 0 0
|
0 0 0
|
||||||
0 1 0
|
0 1 0
|
||||||
3.5 1 0
|
3.5 1 0
|
||||||
2.5385938 0 0
|
2.5385938 0 0
|
||||||
0 1 0
|
0 1 0
|
||||||
3.342784 0.375 4
|
3.342784 0.375 4
|
||||||
3.5 1 4
|
3.5 1 4
|
||||||
2.5385938 0 4
|
2.5385938 0 4
|
||||||
4.146974 0.75 4
|
4.146974 0.75 4
|
||||||
3.5 1 4
|
3.5 1 4
|
||||||
3.342784 0.375 4
|
3.342784 0.375 4
|
||||||
3.4311862 -0.625 4
|
3.4311862 -0.625 4
|
||||||
3.0950184 -1 4
|
3.0950184 -1 4
|
||||||
4.323779 -1.25 4
|
4.323779 -1.25 4
|
||||||
4.146974 0.75 4
|
4.146974 0.75 4
|
||||||
5.755354 1.5 4
|
5.755354 1.5 4
|
||||||
5.644507 2 4
|
5.644507 2 4
|
||||||
0 1 4
|
0 1 4
|
||||||
2.5385938 0 4
|
2.5385938 0 4
|
||||||
3.5 1 4
|
3.5 1 4
|
||||||
0 1 4
|
0 1 4
|
||||||
0 0 4
|
0 0 4
|
||||||
2.5385938 0 4
|
2.5385938 0 4
|
||||||
5.644507 2 4
|
5.644507 2 4
|
||||||
5.755354 1.5 4
|
5.755354 1.5 4
|
||||||
9.5 2 4
|
9.5 2 4
|
||||||
9.5 2 4
|
9.5 2 4
|
||||||
5.755354 1.5 4
|
5.755354 1.5 4
|
||||||
9.5 1.5 4
|
9.5 1.5 4
|
||||||
4.146974 0.75 4
|
4.146974 0.75 4
|
||||||
5.644507 2 4
|
5.644507 2 4
|
||||||
3.5 1 4
|
3.5 1 4
|
||||||
2.5385938 0 4
|
2.5385938 0 4
|
||||||
3.0950184 -1 4
|
3.0950184 -1 4
|
||||||
3.4311862 -0.625 4
|
3.4311862 -0.625 4
|
||||||
4.323779 -1.25 4
|
4.323779 -1.25 4
|
||||||
3.0950184 -1 4
|
3.0950184 -1 4
|
||||||
5.9513144 -3 4
|
5.9513144 -3 4
|
||||||
6.108964 -2.5 4
|
6.108964 -2.5 4
|
||||||
4.323779 -1.25 4
|
4.323779 -1.25 4
|
||||||
5.9513144 -3 4
|
5.9513144 -3 4
|
||||||
9.5 -2.5 4
|
9.5 -2.5 4
|
||||||
6.108964 -2.5 4
|
6.108964 -2.5 4
|
||||||
9.5 -3 4
|
9.5 -3 4
|
||||||
6.108964 -2.5 4
|
6.108964 -2.5 4
|
||||||
5.9513144 -3 4
|
5.9513144 -3 4
|
||||||
9.5 -3 4
|
9.5 -3 4
|
||||||
2.5385938 0 4
|
2.5385938 0 4
|
||||||
0 -1 4
|
0 -1 4
|
||||||
3.0950184 -1 4
|
3.0950184 -1 4
|
||||||
0 -1 4
|
0 -1 4
|
||||||
2.5385938 0 4
|
2.5385938 0 4
|
||||||
0 0 4
|
0 0 4
|
||||||
3 0 1 2
|
3 0 1 2
|
||||||
3 3 4 5
|
3 3 4 5
|
||||||
3 6 7 8
|
3 6 7 8
|
||||||
3 9 10 11
|
3 9 10 11
|
||||||
3 12 13 14
|
3 12 13 14
|
||||||
3 15 16 17
|
3 15 16 17
|
||||||
3 18 19 20
|
3 18 19 20
|
||||||
3 21 22 23
|
3 21 22 23
|
||||||
3 24 25 26
|
3 24 25 26
|
||||||
3 27 28 29
|
3 27 28 29
|
||||||
3 30 31 32
|
3 30 31 32
|
||||||
3 33 34 35
|
3 33 34 35
|
||||||
3 36 37 38
|
3 36 37 38
|
||||||
3 39 40 41
|
3 39 40 41
|
||||||
3 42 43 44
|
3 42 43 44
|
||||||
3 45 46 47
|
3 45 46 47
|
||||||
3 48 49 50
|
3 48 49 50
|
||||||
3 51 52 53
|
3 51 52 53
|
||||||
3 54 55 56
|
3 54 55 56
|
||||||
3 57 58 59
|
3 57 58 59
|
||||||
3 60 61 62
|
3 60 61 62
|
||||||
3 63 64 65
|
3 63 64 65
|
||||||
3 66 67 68
|
3 66 67 68
|
||||||
3 69 70 71
|
3 69 70 71
|
||||||
3 72 73 74
|
3 72 73 74
|
||||||
3 75 76 77
|
3 75 76 77
|
||||||
3 78 79 80
|
3 78 79 80
|
||||||
3 81 82 83
|
3 81 82 83
|
||||||
3 84 85 86
|
3 84 85 86
|
||||||
3 87 88 89
|
3 87 88 89
|
||||||
3 90 91 92
|
3 90 91 92
|
||||||
3 93 94 95
|
3 93 94 95
|
||||||
3 96 97 98
|
3 96 97 98
|
||||||
3 99 100 101
|
3 99 100 101
|
||||||
3 102 103 104
|
3 102 103 104
|
||||||
3 105 106 107
|
3 105 106 107
|
||||||
3 108 109 110
|
3 108 109 110
|
||||||
3 111 112 113
|
3 111 112 113
|
||||||
3 114 115 116
|
3 114 115 116
|
||||||
3 117 118 119
|
3 117 118 119
|
||||||
3 120 121 122
|
3 120 121 122
|
||||||
3 123 124 125
|
3 123 124 125
|
||||||
3 126 127 128
|
3 126 127 128
|
||||||
3 129 130 131
|
3 129 130 131
|
||||||
3 132 133 134
|
3 132 133 134
|
||||||
3 135 136 137
|
3 135 136 137
|
||||||
3 138 139 140
|
3 138 139 140
|
||||||
3 141 142 143
|
3 141 142 143
|
||||||
3 144 145 146
|
3 144 145 146
|
||||||
3 147 148 149
|
3 147 148 149
|
||||||
3 150 151 152
|
3 150 151 152
|
||||||
3 153 154 155
|
3 153 154 155
|
||||||
3 156 157 158
|
3 156 157 158
|
||||||
3 159 160 161
|
3 159 160 161
|
||||||
3 162 163 164
|
3 162 163 164
|
||||||
3 165 166 167
|
3 165 166 167
|
||||||
3 168 169 170
|
3 168 169 170
|
||||||
3 171 172 173
|
3 171 172 173
|
||||||
3 174 175 176
|
3 174 175 176
|
||||||
3 177 178 179
|
3 177 178 179
|
||||||
3 180 181 182
|
3 180 181 182
|
||||||
3 183 184 185
|
3 183 184 185
|
||||||
3 186 187 188
|
3 186 187 188
|
||||||
3 189 190 191
|
3 189 190 191
|
||||||
3 192 193 194
|
3 192 193 194
|
||||||
3 195 196 197
|
3 195 196 197
|
||||||
3 198 199 200
|
3 198 199 200
|
||||||
3 201 202 203
|
3 201 202 203
|
||||||
|
|||||||
@ -643,7 +643,11 @@ test('Command bar works and can change a setting', async ({ page }) => {
|
|||||||
let cmdSearchBar = page.getByPlaceholder('Search commands')
|
let cmdSearchBar = page.getByPlaceholder('Search commands')
|
||||||
|
|
||||||
// First try opening the command bar and closing it
|
// First try opening the command bar and closing it
|
||||||
await page.getByRole('button', { name: '⌘K' }).click()
|
// 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 expect(cmdSearchBar).toBeVisible()
|
||||||
await page.keyboard.press('Escape')
|
await page.keyboard.press('Escape')
|
||||||
await expect(cmdSearchBar).not.toBeVisible()
|
await expect(cmdSearchBar).not.toBeVisible()
|
||||||
@ -658,12 +662,12 @@ test('Command bar works and can change a setting', async ({ page }) => {
|
|||||||
const themeOption = page.getByRole('option', { name: 'Set Theme' })
|
const themeOption = page.getByRole('option', { name: 'Set Theme' })
|
||||||
await expect(themeOption).toBeVisible()
|
await expect(themeOption).toBeVisible()
|
||||||
await themeOption.click()
|
await themeOption.click()
|
||||||
const themeInput = page.getByPlaceholder(Themes.System)
|
const themeInput = page.getByPlaceholder('Select an option')
|
||||||
await expect(themeInput).toBeVisible()
|
await expect(themeInput).toBeVisible()
|
||||||
await expect(themeInput).toBeFocused()
|
await expect(themeInput).toBeFocused()
|
||||||
// Select dark theme
|
// Select dark theme
|
||||||
await page.keyboard.press('ArrowDown')
|
await page.keyboard.press('ArrowDown')
|
||||||
await page.keyboard.press('ArrowUp')
|
await page.keyboard.press('ArrowDown')
|
||||||
await expect(page.getByRole('option', { name: Themes.Dark })).toHaveAttribute(
|
await expect(page.getByRole('option', { name: Themes.Dark })).toHaveAttribute(
|
||||||
'data-headlessui-state',
|
'data-headlessui-state',
|
||||||
'active'
|
'active'
|
||||||
@ -675,3 +679,59 @@ test('Command bar works and can change a setting', async ({ page }) => {
|
|||||||
// Check that the theme changed
|
// Check that the theme changed
|
||||||
await expect(page.locator('body')).toHaveClass(`body-bg ${Themes.Dark}`)
|
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, %)`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 113 KiB |
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 54 KiB |
@ -60,6 +60,7 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "vite",
|
"start": "vite",
|
||||||
|
"start:prod": "vite preview --port=3000",
|
||||||
"serve": "vite serve --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": "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",
|
"build:local": "vite build",
|
||||||
|
|||||||
BIN
public/kcma-logomark-outlined.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
@ -8,7 +8,7 @@ import {
|
|||||||
createRoutesFromElements,
|
createRoutesFromElements,
|
||||||
} from 'react-router-dom'
|
} from 'react-router-dom'
|
||||||
import { GlobalStateProvider } from './components/GlobalStateProvider'
|
import { GlobalStateProvider } from './components/GlobalStateProvider'
|
||||||
import CommandBarProvider from 'components/CommandBar'
|
import CommandBarProvider from 'components/CommandBar/CommandBar'
|
||||||
import ModelingMachineProvider from 'components/ModelingMachineProvider'
|
import ModelingMachineProvider from 'components/ModelingMachineProvider'
|
||||||
import { BROWSER_FILE_NAME } from 'Router'
|
import { BROWSER_FILE_NAME } from 'Router'
|
||||||
|
|
||||||
|
|||||||
17
src/App.tsx
@ -172,11 +172,8 @@ export function App() {
|
|||||||
<ModalContainer />
|
<ModalContainer />
|
||||||
<Resizable
|
<Resizable
|
||||||
className={
|
className={
|
||||||
'h-full flex flex-col flex-1 z-10 my-5 ml-5 pr-1 transition-opacity transition-duration-75 ' +
|
'pointer-events-none h-full flex flex-col flex-1 z-10 my-5 ml-5 pr-1 transition-opacity transition-duration-75 ' +
|
||||||
(buttonDownInStream || onboardingStatus === 'camera'
|
+paneOpacity
|
||||||
? ' pointer-events-none '
|
|
||||||
: ' ') +
|
|
||||||
paneOpacity
|
|
||||||
}
|
}
|
||||||
defaultSize={{
|
defaultSize={{
|
||||||
width: '550px',
|
width: '550px',
|
||||||
@ -188,10 +185,16 @@ export function App() {
|
|||||||
maxHeight={'auto'}
|
maxHeight={'auto'}
|
||||||
handleClasses={{
|
handleClasses={{
|
||||||
right:
|
right:
|
||||||
'hover:bg-liquid-30/40 dark:hover:bg-liquid-10/40 bg-transparent transition-colors duration-100 transition-ease-out delay-100',
|
'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'),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div id="code-pane" className="h-full flex flex-col justify-between">
|
<div
|
||||||
|
id="code-pane"
|
||||||
|
className="h-full flex flex-col justify-between pointer-events-none"
|
||||||
|
>
|
||||||
<CollapsiblePanel
|
<CollapsiblePanel
|
||||||
title="Code"
|
title="Code"
|
||||||
icon={faCode}
|
icon={faCode}
|
||||||
|
|||||||
@ -38,7 +38,7 @@ import {
|
|||||||
settingsMachine,
|
settingsMachine,
|
||||||
} from './machines/settingsMachine'
|
} from './machines/settingsMachine'
|
||||||
import { ContextFrom } from 'xstate'
|
import { ContextFrom } from 'xstate'
|
||||||
import CommandBarProvider from 'components/CommandBar'
|
import CommandBarProvider from 'components/CommandBar/CommandBar'
|
||||||
import { TEST, VITE_KC_SENTRY_DSN } from './env'
|
import { TEST, VITE_KC_SENTRY_DSN } from './env'
|
||||||
import * as Sentry from '@sentry/react'
|
import * as Sentry from '@sentry/react'
|
||||||
import ModelingMachineProvider from 'components/ModelingMachineProvider'
|
import ModelingMachineProvider from 'components/ModelingMachineProvider'
|
||||||
|
|||||||
@ -4,9 +4,11 @@ import { engineCommandManager } from './lang/std/engineConnection'
|
|||||||
import { useModelingContext } from 'hooks/useModelingContext'
|
import { useModelingContext } from 'hooks/useModelingContext'
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||||
import { ActionButton } from 'components/ActionButton'
|
import { ActionButton } from 'components/ActionButton'
|
||||||
|
import usePlatform from 'hooks/usePlatform'
|
||||||
|
|
||||||
export const Toolbar = () => {
|
export const Toolbar = () => {
|
||||||
const { setCommandBarOpen } = useCommandsContext()
|
const platform = usePlatform()
|
||||||
|
const { commandBarSend } = useCommandsContext()
|
||||||
const { state, send, context } = useModelingContext()
|
const { state, send, context } = useModelingContext()
|
||||||
const toolbarButtonsRef = useRef<HTMLUListElement>(null)
|
const toolbarButtonsRef = useRef<HTMLUListElement>(null)
|
||||||
const bgClassName =
|
const bgClassName =
|
||||||
@ -177,10 +179,15 @@ export const Toolbar = () => {
|
|||||||
<ActionButton
|
<ActionButton
|
||||||
Element="button"
|
Element="button"
|
||||||
className="text-sm"
|
className="text-sm"
|
||||||
onClick={() => send('extrude intent')}
|
onClick={() =>
|
||||||
disabled={!state.can('extrude intent')}
|
commandBarSend({
|
||||||
|
type: 'Find and select command',
|
||||||
|
data: { name: 'Extrude', ownerMachine: 'modeling' },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
disabled={!state.can('Extrude')}
|
||||||
title={
|
title={
|
||||||
state.can('extrude intent')
|
state.can('Extrude')
|
||||||
? 'extrude'
|
? 'extrude'
|
||||||
: 'sketches need to be closed, or not already extruded'
|
: 'sketches need to be closed, or not already extruded'
|
||||||
}
|
}
|
||||||
@ -204,10 +211,10 @@ export const Toolbar = () => {
|
|||||||
</menu>
|
</menu>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
Element="button"
|
Element="button"
|
||||||
onClick={() => setCommandBarOpen(true)}
|
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"
|
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"
|
||||||
>
|
>
|
||||||
⌘K
|
{platform === 'darwin' ? '⌘K' : 'Ctrl+/'}
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import styles from './AppHeader.module.css'
|
|||||||
import { NetworkHealthIndicator } from './NetworkHealthIndicator'
|
import { NetworkHealthIndicator } from './NetworkHealthIndicator'
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||||
import { ActionButton } from './ActionButton'
|
import { ActionButton } from './ActionButton'
|
||||||
|
import usePlatform from 'hooks/usePlatform'
|
||||||
|
|
||||||
interface AppHeaderProps extends React.PropsWithChildren {
|
interface AppHeaderProps extends React.PropsWithChildren {
|
||||||
showToolbar?: boolean
|
showToolbar?: boolean
|
||||||
@ -22,7 +23,8 @@ export const AppHeader = ({
|
|||||||
className = '',
|
className = '',
|
||||||
enableMenu = false,
|
enableMenu = false,
|
||||||
}: AppHeaderProps) => {
|
}: AppHeaderProps) => {
|
||||||
const { setCommandBarOpen } = useCommandsContext()
|
const platform = usePlatform()
|
||||||
|
const { commandBarSend } = useCommandsContext()
|
||||||
const { auth } = useGlobalStateContext()
|
const { auth } = useGlobalStateContext()
|
||||||
const user = auth?.context?.user
|
const user = auth?.context?.user
|
||||||
|
|
||||||
@ -47,12 +49,12 @@ export const AppHeader = ({
|
|||||||
) : (
|
) : (
|
||||||
<ActionButton
|
<ActionButton
|
||||||
Element="button"
|
Element="button"
|
||||||
onClick={() => setCommandBarOpen(true)}
|
onClick={() => commandBarSend({ type: 'Open' })}
|
||||||
className="text-sm self-center flex items-center w-fit gap-3"
|
className="text-sm self-center flex items-center w-fit gap-3"
|
||||||
>
|
>
|
||||||
Command Palette{' '}
|
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">
|
<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">
|
||||||
⌘K
|
{platform === 'darwin' ? '⌘K' : 'Ctrl+/'}
|
||||||
</kbd>
|
</kbd>
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -24,13 +24,13 @@ export const PanelHeader = ({
|
|||||||
}: CollapsiblePanelProps) => {
|
}: CollapsiblePanelProps) => {
|
||||||
return (
|
return (
|
||||||
<summary className={styles.header}>
|
<summary className={styles.header}>
|
||||||
<div className="flex gap-2 align-center items-center flex-1">
|
<div className="flex gap-2 items-center flex-1">
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
icon={icon}
|
icon={icon}
|
||||||
className="p-1"
|
className="p-1"
|
||||||
size="sm"
|
size="sm"
|
||||||
bgClassName={
|
bgClassName={
|
||||||
'dark:!bg-chalkboard-100 group-open:bg-chalkboard-80 dark:group-open:!bg-chalkboard-90 group-open:border dark:group-open:border-chalkboard-60 rounded-sm ' +
|
'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 ' +
|
||||||
(iconClassNames?.bg || '')
|
(iconClassNames?.bg || '')
|
||||||
}
|
}
|
||||||
iconClassName={
|
iconClassName={
|
||||||
@ -60,7 +60,9 @@ export const CollapsiblePanel = ({
|
|||||||
<details
|
<details
|
||||||
{...props}
|
{...props}
|
||||||
data-testid={detailsTestId}
|
data-testid={detailsTestId}
|
||||||
className={styles.panel + ' group ' + (className || '')}
|
className={
|
||||||
|
styles.panel + ' pointer-events-auto group ' + (className || '')
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<PanelHeader
|
<PanelHeader
|
||||||
title={title}
|
title={title}
|
||||||
|
|||||||
@ -1,404 +0,0 @@
|
|||||||
import { Combobox, Dialog, Transition } from '@headlessui/react'
|
|
||||||
import {
|
|
||||||
Dispatch,
|
|
||||||
Fragment,
|
|
||||||
SetStateAction,
|
|
||||||
createContext,
|
|
||||||
useEffect,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from 'react'
|
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
|
||||||
import Fuse from 'fuse.js'
|
|
||||||
import {
|
|
||||||
Command,
|
|
||||||
CommandArgument,
|
|
||||||
CommandArgumentOption,
|
|
||||||
} from '../lib/commands'
|
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
|
||||||
import { CustomIcon } from './CustomIcon'
|
|
||||||
|
|
||||||
type ComboboxOption = Command | CommandArgumentOption
|
|
||||||
type CommandArgumentData = [string, any]
|
|
||||||
|
|
||||||
export const CommandsContext = createContext(
|
|
||||||
{} as {
|
|
||||||
commands: Command[]
|
|
||||||
addCommands: (commands: Command[]) => void
|
|
||||||
removeCommands: (commands: Command[]) => void
|
|
||||||
commandBarOpen: boolean
|
|
||||||
setCommandBarOpen: Dispatch<SetStateAction<boolean>>
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
export const CommandBarProvider = ({
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode
|
|
||||||
}) => {
|
|
||||||
const [commands, internalSetCommands] = useState([] as Command[])
|
|
||||||
const [commandBarOpen, setCommandBarOpen] = useState(false)
|
|
||||||
|
|
||||||
function sortCommands(a: Command, b: Command) {
|
|
||||||
if (b.owner === 'auth') return -1
|
|
||||||
if (a.owner === 'auth') return 1
|
|
||||||
return a.name.localeCompare(b.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => console.log('commands updated', commands), [commands])
|
|
||||||
|
|
||||||
const addCommands = (newCommands: Command[]) => {
|
|
||||||
internalSetCommands((prevCommands) =>
|
|
||||||
[...newCommands, ...prevCommands].sort(sortCommands)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const removeCommands = (newCommands: Command[]) => {
|
|
||||||
internalSetCommands((prevCommands) =>
|
|
||||||
prevCommands
|
|
||||||
.filter((command) => !newCommands.includes(command))
|
|
||||||
.sort(sortCommands)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CommandsContext.Provider
|
|
||||||
value={{
|
|
||||||
commands,
|
|
||||||
addCommands,
|
|
||||||
removeCommands,
|
|
||||||
commandBarOpen,
|
|
||||||
setCommandBarOpen,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<CommandBar />
|
|
||||||
</CommandsContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const CommandBar = () => {
|
|
||||||
const { commands, commandBarOpen, setCommandBarOpen } = useCommandsContext()
|
|
||||||
useHotkeys(['meta+k', 'meta+/'], () => {
|
|
||||||
if (commands?.length === 0) return
|
|
||||||
setCommandBarOpen(!commandBarOpen)
|
|
||||||
})
|
|
||||||
|
|
||||||
const [selectedCommand, setSelectedCommand] = useState<Command>()
|
|
||||||
const [commandArguments, setCommandArguments] = useState<CommandArgument[]>(
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
const [commandArgumentData, setCommandArgumentData] = useState<
|
|
||||||
CommandArgumentData[]
|
|
||||||
>([])
|
|
||||||
const [commandArgumentIndex, setCommandArgumentIndex] = useState<number>(0)
|
|
||||||
|
|
||||||
function clearState() {
|
|
||||||
setCommandBarOpen(false)
|
|
||||||
setSelectedCommand(undefined)
|
|
||||||
setCommandArguments([])
|
|
||||||
setCommandArgumentData([])
|
|
||||||
setCommandArgumentIndex(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectCommand(command: Command) {
|
|
||||||
console.log('selecting command', command)
|
|
||||||
if (!('args' in command && command.args?.length)) {
|
|
||||||
submitCommand({ command })
|
|
||||||
} else {
|
|
||||||
setCommandArguments(command.args)
|
|
||||||
setSelectedCommand(command)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function stepBack() {
|
|
||||||
if (!selectedCommand) {
|
|
||||||
clearState()
|
|
||||||
} else {
|
|
||||||
if (commandArgumentIndex === 0) {
|
|
||||||
setSelectedCommand(undefined)
|
|
||||||
} else {
|
|
||||||
setCommandArgumentIndex((prevIndex) => Math.max(0, prevIndex - 1))
|
|
||||||
}
|
|
||||||
if (commandArgumentData.length > 0) {
|
|
||||||
setCommandArgumentData((prevData) => prevData.slice(0, -1))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function appendCommandArgumentData(data: { name: any }) {
|
|
||||||
const transformedData = [
|
|
||||||
commandArguments[commandArgumentIndex].name,
|
|
||||||
data.name,
|
|
||||||
]
|
|
||||||
if (commandArgumentIndex + 1 === commandArguments.length) {
|
|
||||||
submitCommand({
|
|
||||||
dataArr: [
|
|
||||||
...commandArgumentData,
|
|
||||||
transformedData,
|
|
||||||
] as CommandArgumentData[],
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
setCommandArgumentData(
|
|
||||||
(prevData) => [...prevData, transformedData] as CommandArgumentData[]
|
|
||||||
)
|
|
||||||
setCommandArgumentIndex((prevIndex) => prevIndex + 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function submitCommand({
|
|
||||||
command = selectedCommand,
|
|
||||||
dataArr = commandArgumentData,
|
|
||||||
}) {
|
|
||||||
console.log('submitting command', command, dataArr)
|
|
||||||
if (dataArr.length === 0) {
|
|
||||||
command?.callback()
|
|
||||||
} else {
|
|
||||||
const data = Object.fromEntries(dataArr)
|
|
||||||
console.log('submitting data', data)
|
|
||||||
command?.callback(data)
|
|
||||||
}
|
|
||||||
setCommandBarOpen(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDisplayValue(command: Command) {
|
|
||||||
if (
|
|
||||||
'args' in command &&
|
|
||||||
command.args &&
|
|
||||||
command.args?.length > 0 &&
|
|
||||||
'formatFunction' in command &&
|
|
||||||
command.formatFunction
|
|
||||||
) {
|
|
||||||
command.formatFunction(
|
|
||||||
command.args.map((c, i) =>
|
|
||||||
commandArgumentData[i] ? commandArgumentData[i][0] : `<${c.name}>`
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return command.name
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Transition.Root
|
|
||||||
show={commandBarOpen || false}
|
|
||||||
afterLeave={() => clearState()}
|
|
||||||
as={Fragment}
|
|
||||||
>
|
|
||||||
<Dialog
|
|
||||||
onClose={() => {
|
|
||||||
setCommandBarOpen(false)
|
|
||||||
}}
|
|
||||||
className="fixed inset-0 z-40 overflow-y-auto pb-4 pt-1"
|
|
||||||
>
|
|
||||||
<Transition.Child
|
|
||||||
enter="duration-100 ease-out"
|
|
||||||
enterFrom="opacity-0 scale-95"
|
|
||||||
enterTo="opacity-100 scale-100"
|
|
||||||
leave="duration-75 ease-in"
|
|
||||||
leaveFrom="opacity-100 scale-100"
|
|
||||||
leaveTo="opacity-0 scale-95"
|
|
||||||
>
|
|
||||||
<Dialog.Panel
|
|
||||||
className="relative w-full max-w-xl py-2 mx-auto border rounded shadow-lg bg-chalkboard-10 dark:bg-chalkboard-100 dark:border-chalkboard-70"
|
|
||||||
as="div"
|
|
||||||
>
|
|
||||||
{!(
|
|
||||||
commandArguments &&
|
|
||||||
commandArguments.length &&
|
|
||||||
selectedCommand
|
|
||||||
) ? (
|
|
||||||
<CommandComboBox
|
|
||||||
options={commands}
|
|
||||||
handleSelection={selectCommand}
|
|
||||||
stepBack={stepBack}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className="px-4 text-sm flex flex-wrap gap-2">
|
|
||||||
<p className="pr-4 flex gap-2 items-center">
|
|
||||||
{selectedCommand &&
|
|
||||||
'icon' in selectedCommand &&
|
|
||||||
selectedCommand.icon && (
|
|
||||||
<CustomIcon
|
|
||||||
name={selectedCommand.icon}
|
|
||||||
className="w-5 h-5"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{getDisplayValue(selectedCommand)}
|
|
||||||
</p>
|
|
||||||
{commandArguments.map((arg, i) => (
|
|
||||||
<p
|
|
||||||
key={arg.name}
|
|
||||||
className={`w-fit px-2 py-1 rounded-sm flex gap-2 items-center border ${
|
|
||||||
i === commandArgumentIndex
|
|
||||||
? 'bg-energy-10/50 dark:bg-energy-10/20 border-energy-10 dark:border-energy-10'
|
|
||||||
: 'bg-chalkboard-20/50 dark:bg-chalkboard-80/50 border-chalkboard-20 dark:border-chalkboard-80'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{commandArgumentIndex >= i && commandArgumentData[i] ? (
|
|
||||||
commandArgumentData[i][1]
|
|
||||||
) : arg.defaultValue ? (
|
|
||||||
arg.defaultValue
|
|
||||||
) : (
|
|
||||||
<em>{arg.name}</em>
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="block w-full my-2 h-[1px] bg-chalkboard-20 dark:bg-chalkboard-80" />
|
|
||||||
<Argument
|
|
||||||
arg={commandArguments[commandArgumentIndex]}
|
|
||||||
appendCommandArgumentData={appendCommandArgumentData}
|
|
||||||
stepBack={stepBack}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Dialog.Panel>
|
|
||||||
</Transition.Child>
|
|
||||||
</Dialog>
|
|
||||||
</Transition.Root>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function Argument({
|
|
||||||
arg,
|
|
||||||
appendCommandArgumentData,
|
|
||||||
stepBack,
|
|
||||||
}: {
|
|
||||||
arg: CommandArgument
|
|
||||||
appendCommandArgumentData: Dispatch<SetStateAction<any>>
|
|
||||||
stepBack: () => void
|
|
||||||
}) {
|
|
||||||
const { setCommandBarOpen } = useCommandsContext()
|
|
||||||
const inputRef = useRef<HTMLInputElement>(null)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (inputRef.current) {
|
|
||||||
inputRef.current.focus()
|
|
||||||
inputRef.current.select()
|
|
||||||
}
|
|
||||||
}, [arg, inputRef])
|
|
||||||
|
|
||||||
return arg.type === 'select' ? (
|
|
||||||
<CommandComboBox
|
|
||||||
options={arg.options}
|
|
||||||
handleSelection={appendCommandArgumentData}
|
|
||||||
stepBack={stepBack}
|
|
||||||
placeholder="Select an option"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<form
|
|
||||||
onSubmit={(event) => {
|
|
||||||
event.preventDefault()
|
|
||||||
|
|
||||||
appendCommandArgumentData({ name: inputRef.current?.value })
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<label className="flex items-center mx-4 my-4">
|
|
||||||
<span className="px-2 py-1 rounded-l bg-chalkboard-100 dark:bg-chalkboard-80 text-chalkboard-10 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80">
|
|
||||||
{arg.name}
|
|
||||||
</span>
|
|
||||||
<input
|
|
||||||
ref={inputRef}
|
|
||||||
className="flex-grow px-2 py-1 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80 !bg-transparent focus:outline-none"
|
|
||||||
placeholder="Enter a value"
|
|
||||||
defaultValue={arg.defaultValue}
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
if (event.metaKey && event.key === 'k') setCommandBarOpen(false)
|
|
||||||
if (event.key === 'Backspace' && !event.currentTarget.value) {
|
|
||||||
stepBack()
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</form>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default CommandBarProvider
|
|
||||||
|
|
||||||
function CommandComboBox({
|
|
||||||
options,
|
|
||||||
handleSelection,
|
|
||||||
stepBack,
|
|
||||||
placeholder,
|
|
||||||
}: {
|
|
||||||
options: ComboboxOption[]
|
|
||||||
handleSelection: Dispatch<SetStateAction<any>>
|
|
||||||
stepBack: () => void
|
|
||||||
placeholder?: string
|
|
||||||
}) {
|
|
||||||
const { setCommandBarOpen } = useCommandsContext()
|
|
||||||
const [query, setQuery] = useState('')
|
|
||||||
const [filteredOptions, setFilteredOptions] = useState<ComboboxOption[]>()
|
|
||||||
|
|
||||||
const defaultOption =
|
|
||||||
options.find((o) => 'isCurrent' in o && o.isCurrent) || null
|
|
||||||
|
|
||||||
const fuse = new Fuse(options, {
|
|
||||||
keys: ['name', 'description'],
|
|
||||||
threshold: 0.3,
|
|
||||||
})
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const results = fuse.search(query).map((result) => result.item)
|
|
||||||
setFilteredOptions(query.length > 0 ? results : options)
|
|
||||||
}, [query])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Combobox defaultValue={defaultOption} onChange={handleSelection}>
|
|
||||||
<div className="flex items-center gap-2 px-4 pb-2 border-solid border-0 border-b border-b-chalkboard-20 dark:border-b-chalkboard-80">
|
|
||||||
<CustomIcon
|
|
||||||
name="search"
|
|
||||||
className="w-5 h-5 bg-energy-10/50 dark:bg-chalkboard-90 dark:text-energy-10"
|
|
||||||
/>
|
|
||||||
<Combobox.Input
|
|
||||||
onChange={(event) => setQuery(event.target.value)}
|
|
||||||
className="w-full bg-transparent focus:outline-none selection:bg-energy-10/50 dark:selection:bg-energy-10/20 dark:focus:outline-none"
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
if (event.metaKey && event.key === 'k') setCommandBarOpen(false)
|
|
||||||
if (event.key === 'Backspace' && !event.currentTarget.value) {
|
|
||||||
stepBack()
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
placeholder={
|
|
||||||
(defaultOption && defaultOption.name) ||
|
|
||||||
placeholder ||
|
|
||||||
'Search commands'
|
|
||||||
}
|
|
||||||
autoCapitalize="off"
|
|
||||||
autoComplete="off"
|
|
||||||
autoCorrect="off"
|
|
||||||
spellCheck="false"
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Combobox.Options
|
|
||||||
static
|
|
||||||
className="overflow-y-auto max-h-96 cursor-pointer"
|
|
||||||
>
|
|
||||||
{filteredOptions?.map((option) => (
|
|
||||||
<Combobox.Option
|
|
||||||
key={option.name}
|
|
||||||
value={option}
|
|
||||||
className="flex items-center gap-2 px-4 py-1 first:mt-2 last:mb-2 ui-active:bg-energy-10/50 dark:ui-active:bg-chalkboard-90"
|
|
||||||
>
|
|
||||||
{'icon' in option && option.icon && (
|
|
||||||
<CustomIcon
|
|
||||||
name={option.icon}
|
|
||||||
className="w-5 h-5 dark:text-energy-10"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<p className="flex-grow">{option.name} </p>
|
|
||||||
{'isCurrent' in option && option.isCurrent && (
|
|
||||||
<small className="text-chalkboard-70 dark:text-chalkboard-50">
|
|
||||||
current
|
|
||||||
</small>
|
|
||||||
)}
|
|
||||||
</Combobox.Option>
|
|
||||||
))}
|
|
||||||
</Combobox.Options>
|
|
||||||
</Combobox>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
114
src/components/CommandBar/CommandArgOptionInput.tsx
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
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
|
||||||
166
src/components/CommandBar/CommandBar.tsx
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
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
|
||||||
80
src/components/CommandBar/CommandBarArgument.tsx
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
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}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
66
src/components/CommandBar/CommandBarBasicInput.tsx
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
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
|
||||||
171
src/components/CommandBar/CommandBarHeader.tsx
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
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
|
||||||
81
src/components/CommandBar/CommandBarReview.tsx
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
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
|
||||||
114
src/components/CommandBar/CommandBarSelectionInput.tsx
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
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
|
||||||
90
src/components/CommandComboBox.tsx
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
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
|
||||||
@ -3,6 +3,7 @@ export type CustomIconName =
|
|||||||
| 'arrowLeft'
|
| 'arrowLeft'
|
||||||
| 'arrowRight'
|
| 'arrowRight'
|
||||||
| 'arrowUp'
|
| 'arrowUp'
|
||||||
|
| 'checkmark'
|
||||||
| 'close'
|
| 'close'
|
||||||
| 'equal'
|
| 'equal'
|
||||||
| 'extrude'
|
| 'extrude'
|
||||||
@ -90,6 +91,22 @@ export const CustomIcon = ({
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</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':
|
case 'close':
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
|
|||||||
@ -10,45 +10,48 @@ const DownloadAppBanner = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
className="fixed inset-0 top-auto z-50 bg-warn-20 text-warn-80 px-8 py-4"
|
className="fixed inset-0 z-50"
|
||||||
open={!isBannerDismissed}
|
open={!isBannerDismissed}
|
||||||
onClose={() => ({})}
|
onClose={() => ({})}
|
||||||
>
|
>
|
||||||
<Dialog.Panel className="max-w-3xl mx-auto">
|
<Dialog.Overlay className="fixed inset-0 bg-chalkboard-100/50" />
|
||||||
<div className="flex gap-2 justify-between items-start">
|
<Dialog.Panel className="absolute inset-0 top-auto bg-warn-20 text-warn-80 px-8 py-4">
|
||||||
<h2 className="text-xl font-bold mb-4">
|
<div className="max-w-3xl mx-auto">
|
||||||
KittyCAD Modeling App is better as a desktop app!
|
<div className="flex gap-2 justify-between items-start">
|
||||||
</h2>
|
<h2 className="text-xl font-bold mb-4">
|
||||||
<ActionButton
|
KittyCAD Modeling App is better as a desktop app!
|
||||||
Element="button"
|
</h2>
|
||||||
onClick={() => setBannerDismissed(true)}
|
<ActionButton
|
||||||
icon={{
|
Element="button"
|
||||||
icon: 'close',
|
onClick={() => setBannerDismissed(true)}
|
||||||
className: 'p-1',
|
icon={{
|
||||||
bgClassName:
|
icon: 'close',
|
||||||
'bg-warn-70 hover:bg-warn-80 dark:bg-warn-70 dark:hover:bg-warn-80',
|
className: 'p-1',
|
||||||
iconClassName:
|
bgClassName:
|
||||||
'text-warn-10 group-hover:text-warn-10 dark:text-warn-10 dark:group-hover:text-warn-10',
|
'bg-warn-70 hover:bg-warn-80 dark:bg-warn-70 dark:hover:bg-warn-80',
|
||||||
}}
|
iconClassName:
|
||||||
className="!p-0 !bg-transparent !border-transparent"
|
'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>
|
||||||
</div>
|
</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.Panel>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -40,7 +40,7 @@ export const FileMachineProvider = ({
|
|||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}) => {
|
}) => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { setCommandBarOpen } = useCommandsContext()
|
const { commandBarSend } = useCommandsContext()
|
||||||
const { project } = useRouteLoaderData(paths.FILE) as IndexLoaderData
|
const { project } = useRouteLoaderData(paths.FILE) as IndexLoaderData
|
||||||
|
|
||||||
const [state, send] = useMachine(fileMachine, {
|
const [state, send] = useMachine(fileMachine, {
|
||||||
@ -54,7 +54,7 @@ export const FileMachineProvider = ({
|
|||||||
event: EventFrom<typeof fileMachine>
|
event: EventFrom<typeof fileMachine>
|
||||||
) => {
|
) => {
|
||||||
if (event.data && 'name' in event.data) {
|
if (event.data && 'name' in event.data) {
|
||||||
setCommandBarOpen(false)
|
commandBarSend({ type: 'Close' })
|
||||||
navigate(
|
navigate(
|
||||||
`${paths.FILE}/${encodeURIComponent(
|
`${paths.FILE}/${encodeURIComponent(
|
||||||
context.selectedDirectory + sep + event.data.name
|
context.selectedDirectory + sep + event.data.name
|
||||||
|
|||||||
@ -1,19 +1,11 @@
|
|||||||
import { useMachine } from '@xstate/react'
|
import { useMachine } from '@xstate/react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { paths } from '../Router'
|
import { paths } from '../Router'
|
||||||
import {
|
import { authMachine, TOKEN_PERSIST_KEY } from '../machines/authMachine'
|
||||||
authCommandBarConfig,
|
|
||||||
authMachine,
|
|
||||||
TOKEN_PERSIST_KEY,
|
|
||||||
} from '../machines/authMachine'
|
|
||||||
import withBaseUrl from '../lib/withBaseURL'
|
import withBaseUrl from '../lib/withBaseURL'
|
||||||
import React, { createContext, useEffect, useRef } from 'react'
|
import React, { createContext, useEffect, useRef } from 'react'
|
||||||
import useStateMachineCommands from '../hooks/useStateMachineCommands'
|
import useStateMachineCommands from '../hooks/useStateMachineCommands'
|
||||||
import {
|
import { SETTINGS_PERSIST_KEY, settingsMachine } from 'machines/settingsMachine'
|
||||||
SETTINGS_PERSIST_KEY,
|
|
||||||
settingsCommandBarConfig,
|
|
||||||
settingsMachine,
|
|
||||||
} from 'machines/settingsMachine'
|
|
||||||
import { toast } from 'react-hot-toast'
|
import { toast } from 'react-hot-toast'
|
||||||
import { setThemeClass, Themes } from 'lib/theme'
|
import { setThemeClass, Themes } from 'lib/theme'
|
||||||
import {
|
import {
|
||||||
@ -23,8 +15,9 @@ import {
|
|||||||
Prop,
|
Prop,
|
||||||
StateFrom,
|
StateFrom,
|
||||||
} from 'xstate'
|
} from 'xstate'
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
|
||||||
import { isTauri } from 'lib/isTauri'
|
import { isTauri } from 'lib/isTauri'
|
||||||
|
import { settingsCommandBarConfig } from 'lib/commandBarConfigs/settingsCommandConfig'
|
||||||
|
import { authCommandBarConfig } from 'lib/commandBarConfigs/authCommandConfig'
|
||||||
|
|
||||||
type MachineContext<T extends AnyStateMachine> = {
|
type MachineContext<T extends AnyStateMachine> = {
|
||||||
state: StateFrom<T>
|
state: StateFrom<T>
|
||||||
@ -45,7 +38,6 @@ export const GlobalStateProvider = ({
|
|||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}) => {
|
}) => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { commands } = useCommandsContext()
|
|
||||||
|
|
||||||
// Settings machine setup
|
// Settings machine setup
|
||||||
const retrievedSettings = useRef(
|
const retrievedSettings = useRef(
|
||||||
@ -81,10 +73,9 @@ export const GlobalStateProvider = ({
|
|||||||
})
|
})
|
||||||
|
|
||||||
useStateMachineCommands({
|
useStateMachineCommands({
|
||||||
|
machineId: 'settings',
|
||||||
state: settingsState,
|
state: settingsState,
|
||||||
send: settingsSend,
|
send: settingsSend,
|
||||||
commands,
|
|
||||||
owner: 'settings',
|
|
||||||
commandBarConfig: settingsCommandBarConfig,
|
commandBarConfig: settingsCommandBarConfig,
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -121,11 +112,10 @@ export const GlobalStateProvider = ({
|
|||||||
})
|
})
|
||||||
|
|
||||||
useStateMachineCommands({
|
useStateMachineCommands({
|
||||||
|
machineId: 'auth',
|
||||||
state: authState,
|
state: authState,
|
||||||
send: authSend,
|
send: authSend,
|
||||||
commands,
|
|
||||||
commandBarConfig: authCommandBarConfig,
|
commandBarConfig: authCommandBarConfig,
|
||||||
owner: 'auth',
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -15,23 +15,23 @@ const Loading = ({ children }: React.PropsWithChildren) => {
|
|||||||
data-testid="loading"
|
data-testid="loading"
|
||||||
>
|
>
|
||||||
<svg viewBox="0 0 10 10" className="w-8 h-8">
|
<svg viewBox="0 0 10 10" className="w-8 h-8">
|
||||||
<circle cx="5" cy="5" r="4" stroke="var(--liquid-20)" fill="none" />
|
<circle cx="5" cy="5" r="4" stroke="var(--energy-50)" fill="none" />
|
||||||
<circle
|
<circle
|
||||||
cx="5"
|
cx="5"
|
||||||
cy="5"
|
cy="5"
|
||||||
r="4"
|
r="4"
|
||||||
stroke="var(--liquid-10)"
|
stroke="var(--energy-10)"
|
||||||
fill="none"
|
fill="none"
|
||||||
strokeDasharray="4, 4"
|
strokeDasharray="4, 4"
|
||||||
className="animate-spin origin-center"
|
className="animate-spin origin-center"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<p className="text-base mt-4 text-liquid-80 dark:text-liquid-20">
|
<p className="text-base mt-4 text-energy-80 dark:text-energy-30">
|
||||||
{children || 'Loading'}
|
{children || 'Loading'}
|
||||||
</p>
|
</p>
|
||||||
<p
|
<p
|
||||||
className={
|
className={
|
||||||
'text-sm mt-4 text-liquid-90 dark:text-liquid-10 transition-opacity duration-500' +
|
'text-sm mt-4 text-energy-70 dark:text-energy-50 transition-opacity duration-500' +
|
||||||
(hasLongLoadTime ? ' opacity-100' : ' opacity-0')
|
(hasLongLoadTime ? ' opacity-100' : ' opacity-0')
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -29,19 +29,26 @@ import {
|
|||||||
addNewSketchLn,
|
addNewSketchLn,
|
||||||
compareVec2Epsilon,
|
compareVec2Epsilon,
|
||||||
} from 'lang/std/sketch'
|
} from 'lang/std/sketch'
|
||||||
import { kclManager } from 'lang/KclSinglton'
|
import { kclManager, useKclContext } from 'lang/KclSinglton'
|
||||||
import { applyConstraintHorzVertDistance } from './Toolbar/SetHorzVertDistance'
|
import { applyConstraintHorzVertDistance } from './Toolbar/SetHorzVertDistance'
|
||||||
import {
|
import {
|
||||||
angleBetweenInfo,
|
angleBetweenInfo,
|
||||||
applyConstraintAngleBetween,
|
applyConstraintAngleBetween,
|
||||||
} from './Toolbar/SetAngleBetween'
|
} from './Toolbar/SetAngleBetween'
|
||||||
import { applyConstraintAngleLength } from './Toolbar/setAngleLength'
|
import { applyConstraintAngleLength } from './Toolbar/setAngleLength'
|
||||||
import { toast } from 'react-hot-toast'
|
|
||||||
import { pathMapToSelections } from 'lang/util'
|
import { pathMapToSelections } from 'lang/util'
|
||||||
import { useStore } from 'useStore'
|
import { useStore } from 'useStore'
|
||||||
import { handleSelectionBatch, handleSelectionWithShift } from 'lib/selections'
|
import {
|
||||||
|
canExtrudeSelection,
|
||||||
|
handleSelectionBatch,
|
||||||
|
handleSelectionWithShift,
|
||||||
|
isSelectionLastLine,
|
||||||
|
isSketchPipe,
|
||||||
|
} from 'lib/selections'
|
||||||
import { applyConstraintIntersect } from './Toolbar/Intersect'
|
import { applyConstraintIntersect } from './Toolbar/Intersect'
|
||||||
import { applyConstraintAbsDistance } from './Toolbar/SetAbsDistance'
|
import { applyConstraintAbsDistance } from './Toolbar/SetAbsDistance'
|
||||||
|
import useStateMachineCommands from 'hooks/useStateMachineCommands'
|
||||||
|
import { modelingMachineConfig } from 'lib/commandBarConfigs/modelingCommandConfig'
|
||||||
|
|
||||||
type MachineContext<T extends AnyStateMachine> = {
|
type MachineContext<T extends AnyStateMachine> = {
|
||||||
state: StateFrom<T>
|
state: StateFrom<T>
|
||||||
@ -59,6 +66,7 @@ export const ModelingMachineProvider = ({
|
|||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}) => {
|
}) => {
|
||||||
const { auth } = useGlobalStateContext()
|
const { auth } = useGlobalStateContext()
|
||||||
|
const { code } = useKclContext()
|
||||||
const token = auth?.context?.token
|
const token = auth?.context?.token
|
||||||
const streamRef = useRef<HTMLDivElement>(null)
|
const streamRef = useRef<HTMLDivElement>(null)
|
||||||
useSetupEngineManager(streamRef, token)
|
useSetupEngineManager(streamRef, token)
|
||||||
@ -68,8 +76,6 @@ export const ModelingMachineProvider = ({
|
|||||||
editorView: s.editorView,
|
editorView: s.editorView,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// const { commands } = useCommandsContext()
|
|
||||||
|
|
||||||
// Settings machine setup
|
// Settings machine setup
|
||||||
// const retrievedSettings = useRef(
|
// const retrievedSettings = useRef(
|
||||||
// localStorage?.getItem(MODELING_PERSIST_KEY) || '{}'
|
// localStorage?.getItem(MODELING_PERSIST_KEY) || '{}'
|
||||||
@ -83,148 +89,85 @@ export const ModelingMachineProvider = ({
|
|||||||
// >
|
// >
|
||||||
// )
|
// )
|
||||||
|
|
||||||
const [modelingState, modelingSend] = useMachine(modelingMachine, {
|
const [modelingState, modelingSend, modelingActor] = useMachine(
|
||||||
// context: persistedSettings,
|
modelingMachine,
|
||||||
actions: {
|
{
|
||||||
'Modify AST': () => {},
|
// context: persistedSettings,
|
||||||
'Update code selection cursors': () => {},
|
actions: {
|
||||||
'show default planes': () => {
|
'Modify AST': () => {},
|
||||||
kclManager.showPlanes()
|
'Update code selection cursors': () => {},
|
||||||
},
|
'show default planes': () => {
|
||||||
'create path': assign({
|
kclManager.showPlanes()
|
||||||
sketchEnginePathId: () => {
|
|
||||||
const sketchUuid = uuidv4()
|
|
||||||
engineCommandManager.sendSceneCommand({
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd_id: sketchUuid,
|
|
||||||
cmd: {
|
|
||||||
type: 'start_path',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
engineCommandManager.sendSceneCommand({
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
cmd: {
|
|
||||||
type: 'edit_mode_enter',
|
|
||||||
target: sketchUuid,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return sketchUuid
|
|
||||||
},
|
},
|
||||||
}),
|
'create path': assign({
|
||||||
'AST start new sketch': assign(
|
sketchEnginePathId: () => {
|
||||||
({ sketchEnginePathId }, { data: { coords, axis, segmentId } }) => {
|
const sketchUuid = uuidv4()
|
||||||
if (!axis) {
|
engineCommandManager.sendSceneCommand({
|
||||||
// Something really weird must have happened for this to happen.
|
type: 'modeling_cmd_req',
|
||||||
console.error('axis is undefined for starting a new sketch')
|
cmd_id: sketchUuid,
|
||||||
return {}
|
cmd: {
|
||||||
}
|
type: 'start_path',
|
||||||
if (!segmentId) {
|
},
|
||||||
// Something really weird must have happened for this to happen.
|
})
|
||||||
console.error('segmentId is undefined for starting a new sketch')
|
engineCommandManager.sendSceneCommand({
|
||||||
return {}
|
type: 'modeling_cmd_req',
|
||||||
}
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
const _addStartSketch = addStartSketch(
|
type: 'edit_mode_enter',
|
||||||
kclManager.ast,
|
target: sketchUuid,
|
||||||
axis,
|
},
|
||||||
[roundOff(coords[0].x), roundOff(coords[0].y)],
|
})
|
||||||
[
|
return sketchUuid
|
||||||
roundOff(coords[1].x - coords[0].x),
|
},
|
||||||
roundOff(coords[1].y - coords[0].y),
|
}),
|
||||||
]
|
'AST start new sketch': assign(
|
||||||
)
|
({ sketchEnginePathId }, { data: { coords, axis, segmentId } }) => {
|
||||||
const _modifiedAst = _addStartSketch.modifiedAst
|
if (!axis) {
|
||||||
const _pathToNode = _addStartSketch.pathToNode
|
// Something really weird must have happened for this to happen.
|
||||||
const newCode = recast(_modifiedAst)
|
console.error('axis is undefined for starting a new sketch')
|
||||||
const astWithUpdatedSource = parse(newCode)
|
return {}
|
||||||
const updatedPipeNode = getNodeFromPath<PipeExpression>(
|
|
||||||
astWithUpdatedSource,
|
|
||||||
_pathToNode
|
|
||||||
).node
|
|
||||||
const startProfileAtCallExp = updatedPipeNode.body.find(
|
|
||||||
(exp) =>
|
|
||||||
exp.type === 'CallExpression' &&
|
|
||||||
exp.callee.name === 'startProfileAt'
|
|
||||||
)
|
|
||||||
if (startProfileAtCallExp)
|
|
||||||
engineCommandManager.artifactMap[sketchEnginePathId] = {
|
|
||||||
type: 'result',
|
|
||||||
range: [startProfileAtCallExp.start, startProfileAtCallExp.end],
|
|
||||||
commandType: 'start_path',
|
|
||||||
data: null,
|
|
||||||
raw: {} as any,
|
|
||||||
}
|
}
|
||||||
const lineCallExp = updatedPipeNode.body.find(
|
if (!segmentId) {
|
||||||
(exp) => exp.type === 'CallExpression' && exp.callee.name === 'line'
|
// Something really weird must have happened for this to happen.
|
||||||
)
|
console.error('segmentId is undefined for starting a new sketch')
|
||||||
if (lineCallExp)
|
return {}
|
||||||
engineCommandManager.artifactMap[segmentId] = {
|
|
||||||
type: 'result',
|
|
||||||
range: [lineCallExp.start, lineCallExp.end],
|
|
||||||
commandType: 'extend_path',
|
|
||||||
parentId: sketchEnginePathId,
|
|
||||||
data: null,
|
|
||||||
raw: {} as any,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
kclManager.executeAstMock(astWithUpdatedSource, true)
|
const _addStartSketch = addStartSketch(
|
||||||
|
|
||||||
return {
|
|
||||||
sketchPathToNode: _pathToNode,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
),
|
|
||||||
'AST add line segment': async (
|
|
||||||
{ sketchPathToNode, sketchEnginePathId },
|
|
||||||
{ data: { coords, segmentId } }
|
|
||||||
) => {
|
|
||||||
if (!sketchPathToNode) return
|
|
||||||
const lastCoord = coords[coords.length - 1]
|
|
||||||
|
|
||||||
const pathInfo = await engineCommandManager.sendSceneCommand({
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
cmd: {
|
|
||||||
type: 'path_get_info',
|
|
||||||
path_id: sketchEnginePathId,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
const firstSegment = pathInfo?.data?.data?.segments.find(
|
|
||||||
(seg: any) => seg.command === 'line_to'
|
|
||||||
)
|
|
||||||
const firstSegCoords = await engineCommandManager.sendSceneCommand({
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
cmd: {
|
|
||||||
type: 'curve_get_control_points',
|
|
||||||
curve_id: firstSegment.command_id,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
const startPathCoord = firstSegCoords?.data?.data?.control_points[0]
|
|
||||||
|
|
||||||
const isClose = compareVec2Epsilon(
|
|
||||||
[startPathCoord.x, startPathCoord.y],
|
|
||||||
[lastCoord.x, lastCoord.y]
|
|
||||||
)
|
|
||||||
|
|
||||||
let _modifiedAst: Program
|
|
||||||
if (!isClose) {
|
|
||||||
const newSketchLn = addNewSketchLn({
|
|
||||||
node: kclManager.ast,
|
|
||||||
programMemory: kclManager.programMemory,
|
|
||||||
to: [lastCoord.x, lastCoord.y],
|
|
||||||
from: [coords[0].x, coords[0].y],
|
|
||||||
fnName: 'line',
|
|
||||||
pathToNode: sketchPathToNode,
|
|
||||||
})
|
|
||||||
const _modifiedAst = newSketchLn.modifiedAst
|
|
||||||
kclManager.executeAstMock(_modifiedAst, true).then(() => {
|
|
||||||
const lineCallExp = getNodeFromPath<CallExpression>(
|
|
||||||
kclManager.ast,
|
kclManager.ast,
|
||||||
newSketchLn.pathToNode
|
axis,
|
||||||
|
[roundOff(coords[0].x), roundOff(coords[0].y)],
|
||||||
|
[
|
||||||
|
roundOff(coords[1].x - coords[0].x),
|
||||||
|
roundOff(coords[1].y - coords[0].y),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
const _modifiedAst = _addStartSketch.modifiedAst
|
||||||
|
const _pathToNode = _addStartSketch.pathToNode
|
||||||
|
const newCode = recast(_modifiedAst)
|
||||||
|
const astWithUpdatedSource = parse(newCode)
|
||||||
|
const updatedPipeNode = getNodeFromPath<PipeExpression>(
|
||||||
|
astWithUpdatedSource,
|
||||||
|
_pathToNode
|
||||||
).node
|
).node
|
||||||
if (segmentId)
|
const startProfileAtCallExp = updatedPipeNode.body.find(
|
||||||
|
(exp) =>
|
||||||
|
exp.type === 'CallExpression' &&
|
||||||
|
exp.callee.name === 'startProfileAt'
|
||||||
|
)
|
||||||
|
if (startProfileAtCallExp)
|
||||||
|
engineCommandManager.artifactMap[sketchEnginePathId] = {
|
||||||
|
type: 'result',
|
||||||
|
range: [startProfileAtCallExp.start, startProfileAtCallExp.end],
|
||||||
|
commandType: 'start_path',
|
||||||
|
data: null,
|
||||||
|
raw: {} as any,
|
||||||
|
}
|
||||||
|
const lineCallExp = updatedPipeNode.body.find(
|
||||||
|
(exp) =>
|
||||||
|
exp.type === 'CallExpression' && exp.callee.name === 'line'
|
||||||
|
)
|
||||||
|
if (lineCallExp)
|
||||||
engineCommandManager.artifactMap[segmentId] = {
|
engineCommandManager.artifactMap[segmentId] = {
|
||||||
type: 'result',
|
type: 'result',
|
||||||
range: [lineCallExp.start, lineCallExp.end],
|
range: [lineCallExp.start, lineCallExp.end],
|
||||||
@ -233,120 +176,189 @@ export const ModelingMachineProvider = ({
|
|||||||
data: null,
|
data: null,
|
||||||
raw: {} as any,
|
raw: {} as any,
|
||||||
}
|
}
|
||||||
})
|
|
||||||
} else {
|
|
||||||
_modifiedAst = addCloseToPipe({
|
|
||||||
node: kclManager.ast,
|
|
||||||
programMemory: kclManager.programMemory,
|
|
||||||
pathToNode: sketchPathToNode,
|
|
||||||
})
|
|
||||||
engineCommandManager.sendSceneCommand({
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
cmd: { type: 'edit_mode_exit' },
|
|
||||||
})
|
|
||||||
engineCommandManager.sendSceneCommand({
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
cmd: { type: 'default_camera_disable_sketch_mode' },
|
|
||||||
})
|
|
||||||
kclManager.executeAstMock(_modifiedAst, true)
|
|
||||||
// updateAst(_modifiedAst, true)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'sketch exit execute': () => {
|
|
||||||
kclManager.executeAst()
|
|
||||||
},
|
|
||||||
'set tool': () => {}, // TODO
|
|
||||||
'toast extrude failed': () => {
|
|
||||||
toast.error(
|
|
||||||
'Extrude failed, sketches need to be closed, or not already extruded'
|
|
||||||
)
|
|
||||||
},
|
|
||||||
'Set selection': assign(({ selectionRanges }, event) => {
|
|
||||||
if (event.type !== 'Set selection') return {} // this was needed for ts after adding 'Set selection' action to on done modal events
|
|
||||||
const setSelections = event.data
|
|
||||||
if (!editorView) return {}
|
|
||||||
if (setSelections.selectionType === 'mirrorCodeMirrorSelections')
|
|
||||||
return { selectionRanges: setSelections.selection }
|
|
||||||
else if (setSelections.selectionType === 'otherSelection') {
|
|
||||||
// TODO KittyCAD/engine/issues/1620: send axis highlight when it's working (if that's what we settle on)
|
|
||||||
// const axisAddCmd: EngineCommand = {
|
|
||||||
// type: 'modeling_cmd_req',
|
|
||||||
// cmd: {
|
|
||||||
// type: 'highlight_set_entities',
|
|
||||||
// entities: [
|
|
||||||
// setSelections.selection === 'x-axis'
|
|
||||||
// ? X_AXIS_UUID
|
|
||||||
// : Y_AXIS_UUID,
|
|
||||||
// ],
|
|
||||||
// },
|
|
||||||
// cmd_id: uuidv4(),
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (!isShiftDown) {
|
kclManager.executeAstMock(astWithUpdatedSource, true)
|
||||||
// engineCommandManager
|
|
||||||
// .sendSceneCommand({
|
|
||||||
// type: 'modeling_cmd_req',
|
|
||||||
// cmd: {
|
|
||||||
// type: 'select_clear',
|
|
||||||
// },
|
|
||||||
// cmd_id: uuidv4(),
|
|
||||||
// })
|
|
||||||
// .then(() => {
|
|
||||||
// engineCommandManager.sendSceneCommand(axisAddCmd)
|
|
||||||
// })
|
|
||||||
// } else {
|
|
||||||
// engineCommandManager.sendSceneCommand(axisAddCmd)
|
|
||||||
// }
|
|
||||||
|
|
||||||
const {
|
return {
|
||||||
codeMirrorSelection,
|
sketchPathToNode: _pathToNode,
|
||||||
selectionRangeTypeMap,
|
}
|
||||||
otherSelections,
|
|
||||||
} = handleSelectionWithShift({
|
|
||||||
otherSelection: setSelections.selection,
|
|
||||||
currentSelections: selectionRanges,
|
|
||||||
isShiftDown,
|
|
||||||
})
|
|
||||||
setTimeout(() => {
|
|
||||||
editorView.dispatch({
|
|
||||||
selection: codeMirrorSelection,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
return {
|
|
||||||
selectionRangeTypeMap,
|
|
||||||
selectionRanges: {
|
|
||||||
codeBasedSelections: selectionRanges.codeBasedSelections,
|
|
||||||
otherSelections,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
} else if (setSelections.selectionType === 'singleCodeCursor') {
|
),
|
||||||
// This DOES NOT set the `selectionRanges` in xstate context
|
'AST add line segment': async (
|
||||||
// instead it updates/dispatches to the editor, which in turn updates the xstate context
|
{ sketchPathToNode, sketchEnginePathId },
|
||||||
// I've found this the best way to deal with the editor without causing an infinite loop
|
{ data: { coords, segmentId } }
|
||||||
// and really we want the editor to be in charge of cursor positions and for `selectionRanges` mirror it
|
) => {
|
||||||
// because we want to respect the user manually placing the cursor too.
|
if (!sketchPathToNode) return
|
||||||
|
const lastCoord = coords[coords.length - 1]
|
||||||
|
|
||||||
// for more details on how selections see `src/lib/selections.ts`.
|
const pathInfo = await engineCommandManager.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
const {
|
cmd_id: uuidv4(),
|
||||||
codeMirrorSelection,
|
cmd: {
|
||||||
selectionRangeTypeMap,
|
type: 'path_get_info',
|
||||||
otherSelections,
|
path_id: sketchEnginePathId,
|
||||||
} = handleSelectionWithShift({
|
},
|
||||||
codeSelection: setSelections.selection,
|
|
||||||
currentSelections: selectionRanges,
|
|
||||||
isShiftDown,
|
|
||||||
})
|
})
|
||||||
if (codeMirrorSelection) {
|
const firstSegment = pathInfo?.data?.data?.segments.find(
|
||||||
|
(seg: any) => seg.command === 'line_to'
|
||||||
|
)
|
||||||
|
const firstSegCoords = await engineCommandManager.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'curve_get_control_points',
|
||||||
|
curve_id: firstSegment.command_id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const startPathCoord = firstSegCoords?.data?.data?.control_points[0]
|
||||||
|
|
||||||
|
const isClose = compareVec2Epsilon(
|
||||||
|
[startPathCoord.x, startPathCoord.y],
|
||||||
|
[lastCoord.x, lastCoord.y]
|
||||||
|
)
|
||||||
|
|
||||||
|
let _modifiedAst: Program
|
||||||
|
if (!isClose) {
|
||||||
|
const newSketchLn = addNewSketchLn({
|
||||||
|
node: kclManager.ast,
|
||||||
|
programMemory: kclManager.programMemory,
|
||||||
|
to: [lastCoord.x, lastCoord.y],
|
||||||
|
from: [coords[0].x, coords[0].y],
|
||||||
|
fnName: 'line',
|
||||||
|
pathToNode: sketchPathToNode,
|
||||||
|
})
|
||||||
|
const _modifiedAst = newSketchLn.modifiedAst
|
||||||
|
kclManager.executeAstMock(_modifiedAst, true).then(() => {
|
||||||
|
const lineCallExp = getNodeFromPath<CallExpression>(
|
||||||
|
kclManager.ast,
|
||||||
|
newSketchLn.pathToNode
|
||||||
|
).node
|
||||||
|
if (segmentId)
|
||||||
|
engineCommandManager.artifactMap[segmentId] = {
|
||||||
|
type: 'result',
|
||||||
|
range: [lineCallExp.start, lineCallExp.end],
|
||||||
|
commandType: 'extend_path',
|
||||||
|
parentId: sketchEnginePathId,
|
||||||
|
data: null,
|
||||||
|
raw: {} as any,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
_modifiedAst = addCloseToPipe({
|
||||||
|
node: kclManager.ast,
|
||||||
|
programMemory: kclManager.programMemory,
|
||||||
|
pathToNode: sketchPathToNode,
|
||||||
|
})
|
||||||
|
engineCommandManager.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: { type: 'edit_mode_exit' },
|
||||||
|
})
|
||||||
|
engineCommandManager.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: { type: 'default_camera_disable_sketch_mode' },
|
||||||
|
})
|
||||||
|
kclManager.executeAstMock(_modifiedAst, true)
|
||||||
|
// updateAst(_modifiedAst, true)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'sketch exit execute': () => {
|
||||||
|
kclManager.executeAst()
|
||||||
|
},
|
||||||
|
'set tool': () => {}, // TODO
|
||||||
|
'Set selection': assign(({ selectionRanges }, event) => {
|
||||||
|
if (event.type !== 'Set selection') return {} // this was needed for ts after adding 'Set selection' action to on done modal events
|
||||||
|
const setSelections = event.data
|
||||||
|
if (!editorView) return {}
|
||||||
|
if (setSelections.selectionType === 'mirrorCodeMirrorSelections')
|
||||||
|
return { selectionRanges: setSelections.selection }
|
||||||
|
else if (setSelections.selectionType === 'otherSelection') {
|
||||||
|
// TODO KittyCAD/engine/issues/1620: send axis highlight when it's working (if that's what we settle on)
|
||||||
|
// const axisAddCmd: EngineCommand = {
|
||||||
|
// type: 'modeling_cmd_req',
|
||||||
|
// cmd: {
|
||||||
|
// type: 'highlight_set_entities',
|
||||||
|
// entities: [
|
||||||
|
// setSelections.selection === 'x-axis'
|
||||||
|
// ? X_AXIS_UUID
|
||||||
|
// : Y_AXIS_UUID,
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
// cmd_id: uuidv4(),
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (!isShiftDown) {
|
||||||
|
// engineCommandManager
|
||||||
|
// .sendSceneCommand({
|
||||||
|
// type: 'modeling_cmd_req',
|
||||||
|
// cmd: {
|
||||||
|
// type: 'select_clear',
|
||||||
|
// },
|
||||||
|
// cmd_id: uuidv4(),
|
||||||
|
// })
|
||||||
|
// .then(() => {
|
||||||
|
// engineCommandManager.sendSceneCommand(axisAddCmd)
|
||||||
|
// })
|
||||||
|
// } else {
|
||||||
|
// engineCommandManager.sendSceneCommand(axisAddCmd)
|
||||||
|
// }
|
||||||
|
|
||||||
|
const {
|
||||||
|
codeMirrorSelection,
|
||||||
|
selectionRangeTypeMap,
|
||||||
|
otherSelections,
|
||||||
|
} = handleSelectionWithShift({
|
||||||
|
otherSelection: setSelections.selection,
|
||||||
|
currentSelections: selectionRanges,
|
||||||
|
isShiftDown,
|
||||||
|
})
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
editorView.dispatch({
|
editorView.dispatch({
|
||||||
selection: codeMirrorSelection,
|
selection: codeMirrorSelection,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
return {
|
||||||
if (!setSelections.selection) {
|
selectionRangeTypeMap,
|
||||||
|
selectionRanges: {
|
||||||
|
codeBasedSelections: selectionRanges.codeBasedSelections,
|
||||||
|
otherSelections,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else if (setSelections.selectionType === 'singleCodeCursor') {
|
||||||
|
// This DOES NOT set the `selectionRanges` in xstate context
|
||||||
|
// instead it updates/dispatches to the editor, which in turn updates the xstate context
|
||||||
|
// I've found this the best way to deal with the editor without causing an infinite loop
|
||||||
|
// and really we want the editor to be in charge of cursor positions and for `selectionRanges` mirror it
|
||||||
|
// because we want to respect the user manually placing the cursor too.
|
||||||
|
|
||||||
|
// for more details on how selections see `src/lib/selections.ts`.
|
||||||
|
|
||||||
|
const {
|
||||||
|
codeMirrorSelection,
|
||||||
|
selectionRangeTypeMap,
|
||||||
|
otherSelections,
|
||||||
|
} = handleSelectionWithShift({
|
||||||
|
codeSelection: setSelections.selection,
|
||||||
|
currentSelections: selectionRanges,
|
||||||
|
isShiftDown,
|
||||||
|
})
|
||||||
|
if (codeMirrorSelection) {
|
||||||
|
setTimeout(() => {
|
||||||
|
editorView.dispatch({
|
||||||
|
selection: codeMirrorSelection,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (!setSelections.selection) {
|
||||||
|
return {
|
||||||
|
selectionRangeTypeMap,
|
||||||
|
selectionRanges: {
|
||||||
|
codeBasedSelections: selectionRanges.codeBasedSelections,
|
||||||
|
otherSelections,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
selectionRangeTypeMap,
|
selectionRangeTypeMap,
|
||||||
selectionRanges: {
|
selectionRanges: {
|
||||||
@ -355,171 +367,180 @@ export const ModelingMachineProvider = ({
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// This DOES NOT set the `selectionRanges` in xstate context
|
||||||
|
// same as comment above
|
||||||
|
const { codeMirrorSelection, selectionRangeTypeMap } =
|
||||||
|
handleSelectionBatch({
|
||||||
|
selections: setSelections.selection,
|
||||||
|
})
|
||||||
|
if (codeMirrorSelection) {
|
||||||
|
setTimeout(() => {
|
||||||
|
editorView.dispatch({
|
||||||
|
selection: codeMirrorSelection,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return { selectionRangeTypeMap }
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
guards: {
|
||||||
|
'Selection contains axis': () => true,
|
||||||
|
'Selection contains edge': () => true,
|
||||||
|
'Selection contains face': () => true,
|
||||||
|
'Selection contains line': () => true,
|
||||||
|
'Selection contains point': () => true,
|
||||||
|
'Selection is not empty': () => true,
|
||||||
|
'has valid extrude selection': ({ selectionRanges }) => {
|
||||||
|
// A user can begin extruding if they either have 1+ faces selected or nothing selected
|
||||||
|
// TODO: I believe this guard only allows for extruding a single face at a time
|
||||||
|
if (selectionRanges.codeBasedSelections.length < 1) return false
|
||||||
|
const isPipe = isSketchPipe(selectionRanges)
|
||||||
|
|
||||||
|
if (isSelectionLastLine(selectionRanges, code)) return true
|
||||||
|
if (!isPipe) return false
|
||||||
|
|
||||||
|
return canExtrudeSelection(selectionRanges)
|
||||||
|
},
|
||||||
|
'Selection is one face': ({ selectionRanges }) => {
|
||||||
|
return !!isCursorInSketchCommandRange(
|
||||||
|
engineCommandManager.artifactMap,
|
||||||
|
selectionRanges
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
services: {
|
||||||
|
'Get horizontal info': async ({
|
||||||
|
selectionRanges,
|
||||||
|
}): Promise<SetSelections> => {
|
||||||
|
const { modifiedAst, pathToNodeMap } =
|
||||||
|
await applyConstraintHorzVertDistance({
|
||||||
|
constraint: 'setHorzDistance',
|
||||||
|
selectionRanges,
|
||||||
|
})
|
||||||
|
await kclManager.updateAst(modifiedAst, true)
|
||||||
return {
|
return {
|
||||||
selectionRangeTypeMap,
|
selectionType: 'completeSelection',
|
||||||
selectionRanges: {
|
selection: pathMapToSelections(
|
||||||
codeBasedSelections: selectionRanges.codeBasedSelections,
|
kclManager.ast,
|
||||||
otherSelections,
|
selectionRanges,
|
||||||
},
|
pathToNodeMap
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
// This DOES NOT set the `selectionRanges` in xstate context
|
'Get vertical info': async ({
|
||||||
// same as comment above
|
|
||||||
const { codeMirrorSelection, selectionRangeTypeMap } =
|
|
||||||
handleSelectionBatch({
|
|
||||||
selections: setSelections.selection,
|
|
||||||
})
|
|
||||||
if (codeMirrorSelection) {
|
|
||||||
setTimeout(() => {
|
|
||||||
editorView.dispatch({
|
|
||||||
selection: codeMirrorSelection,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return { selectionRangeTypeMap }
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
guards: {
|
|
||||||
'Selection contains axis': () => true,
|
|
||||||
'Selection contains edge': () => true,
|
|
||||||
'Selection contains face': () => true,
|
|
||||||
'Selection contains line': () => true,
|
|
||||||
'Selection contains point': () => true,
|
|
||||||
'Selection is not empty': () => true,
|
|
||||||
'Selection is one face': ({ selectionRanges }) => {
|
|
||||||
return !!isCursorInSketchCommandRange(
|
|
||||||
engineCommandManager.artifactMap,
|
|
||||||
selectionRanges
|
|
||||||
)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
services: {
|
|
||||||
'Get horizontal info': async ({
|
|
||||||
selectionRanges,
|
|
||||||
}): Promise<SetSelections> => {
|
|
||||||
const { modifiedAst, pathToNodeMap } =
|
|
||||||
await applyConstraintHorzVertDistance({
|
|
||||||
constraint: 'setHorzDistance',
|
|
||||||
selectionRanges,
|
|
||||||
})
|
|
||||||
await kclManager.updateAst(modifiedAst, true)
|
|
||||||
return {
|
|
||||||
selectionType: 'completeSelection',
|
|
||||||
selection: pathMapToSelections(
|
|
||||||
kclManager.ast,
|
|
||||||
selectionRanges,
|
|
||||||
pathToNodeMap
|
|
||||||
),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'Get vertical info': async ({
|
|
||||||
selectionRanges,
|
|
||||||
}): Promise<SetSelections> => {
|
|
||||||
const { modifiedAst, pathToNodeMap } =
|
|
||||||
await applyConstraintHorzVertDistance({
|
|
||||||
constraint: 'setVertDistance',
|
|
||||||
selectionRanges,
|
|
||||||
})
|
|
||||||
await kclManager.updateAst(modifiedAst, true)
|
|
||||||
return {
|
|
||||||
selectionType: 'completeSelection',
|
|
||||||
selection: pathMapToSelections(
|
|
||||||
kclManager.ast,
|
|
||||||
selectionRanges,
|
|
||||||
pathToNodeMap
|
|
||||||
),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'Get angle info': async ({ selectionRanges }): Promise<SetSelections> => {
|
|
||||||
const { modifiedAst, pathToNodeMap } = await (angleBetweenInfo({
|
|
||||||
selectionRanges,
|
selectionRanges,
|
||||||
}).enabled
|
}): Promise<SetSelections> => {
|
||||||
? applyConstraintAngleBetween({
|
const { modifiedAst, pathToNodeMap } =
|
||||||
|
await applyConstraintHorzVertDistance({
|
||||||
|
constraint: 'setVertDistance',
|
||||||
selectionRanges,
|
selectionRanges,
|
||||||
})
|
})
|
||||||
: applyConstraintAngleLength({
|
await kclManager.updateAst(modifiedAst, true)
|
||||||
|
return {
|
||||||
|
selectionType: 'completeSelection',
|
||||||
|
selection: pathMapToSelections(
|
||||||
|
kclManager.ast,
|
||||||
selectionRanges,
|
selectionRanges,
|
||||||
angleOrLength: 'setAngle',
|
pathToNodeMap
|
||||||
}))
|
),
|
||||||
await kclManager.updateAst(modifiedAst, true)
|
}
|
||||||
return {
|
},
|
||||||
selectionType: 'completeSelection',
|
'Get angle info': async ({
|
||||||
selection: pathMapToSelections(
|
|
||||||
kclManager.ast,
|
|
||||||
selectionRanges,
|
|
||||||
pathToNodeMap
|
|
||||||
),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'Get length info': async ({
|
|
||||||
selectionRanges,
|
|
||||||
}): Promise<SetSelections> => {
|
|
||||||
const { modifiedAst, pathToNodeMap } = await applyConstraintAngleLength(
|
|
||||||
{ selectionRanges }
|
|
||||||
)
|
|
||||||
await kclManager.updateAst(modifiedAst, true)
|
|
||||||
return {
|
|
||||||
selectionType: 'completeSelection',
|
|
||||||
selection: pathMapToSelections(
|
|
||||||
kclManager.ast,
|
|
||||||
selectionRanges,
|
|
||||||
pathToNodeMap
|
|
||||||
),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'Get perpendicular distance info': async ({
|
|
||||||
selectionRanges,
|
|
||||||
}): Promise<SetSelections> => {
|
|
||||||
const { modifiedAst, pathToNodeMap } = await applyConstraintIntersect({
|
|
||||||
selectionRanges,
|
selectionRanges,
|
||||||
})
|
}): Promise<SetSelections> => {
|
||||||
await kclManager.updateAst(modifiedAst, true)
|
const { modifiedAst, pathToNodeMap } = await (angleBetweenInfo({
|
||||||
return {
|
|
||||||
selectionType: 'completeSelection',
|
|
||||||
selection: pathMapToSelections(
|
|
||||||
kclManager.ast,
|
|
||||||
selectionRanges,
|
|
||||||
pathToNodeMap
|
|
||||||
),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'Get ABS X info': async ({ selectionRanges }): Promise<SetSelections> => {
|
|
||||||
const { modifiedAst, pathToNodeMap } = await applyConstraintAbsDistance(
|
|
||||||
{
|
|
||||||
constraint: 'xAbs',
|
|
||||||
selectionRanges,
|
selectionRanges,
|
||||||
|
}).enabled
|
||||||
|
? applyConstraintAngleBetween({
|
||||||
|
selectionRanges,
|
||||||
|
})
|
||||||
|
: applyConstraintAngleLength({
|
||||||
|
selectionRanges,
|
||||||
|
angleOrLength: 'setAngle',
|
||||||
|
}))
|
||||||
|
await kclManager.updateAst(modifiedAst, true)
|
||||||
|
return {
|
||||||
|
selectionType: 'completeSelection',
|
||||||
|
selection: pathMapToSelections(
|
||||||
|
kclManager.ast,
|
||||||
|
selectionRanges,
|
||||||
|
pathToNodeMap
|
||||||
|
),
|
||||||
}
|
}
|
||||||
)
|
},
|
||||||
await kclManager.updateAst(modifiedAst, true)
|
'Get length info': async ({
|
||||||
return {
|
selectionRanges,
|
||||||
selectionType: 'completeSelection',
|
}): Promise<SetSelections> => {
|
||||||
selection: pathMapToSelections(
|
const { modifiedAst, pathToNodeMap } =
|
||||||
kclManager.ast,
|
await applyConstraintAngleLength({ selectionRanges })
|
||||||
selectionRanges,
|
await kclManager.updateAst(modifiedAst, true)
|
||||||
pathToNodeMap
|
return {
|
||||||
),
|
selectionType: 'completeSelection',
|
||||||
}
|
selection: pathMapToSelections(
|
||||||
},
|
kclManager.ast,
|
||||||
'Get ABS Y info': async ({ selectionRanges }): Promise<SetSelections> => {
|
selectionRanges,
|
||||||
const { modifiedAst, pathToNodeMap } = await applyConstraintAbsDistance(
|
pathToNodeMap
|
||||||
{
|
),
|
||||||
constraint: 'yAbs',
|
|
||||||
selectionRanges,
|
|
||||||
}
|
}
|
||||||
)
|
},
|
||||||
await kclManager.updateAst(modifiedAst, true)
|
'Get perpendicular distance info': async ({
|
||||||
return {
|
selectionRanges,
|
||||||
selectionType: 'completeSelection',
|
}): Promise<SetSelections> => {
|
||||||
selection: pathMapToSelections(
|
const { modifiedAst, pathToNodeMap } = await applyConstraintIntersect(
|
||||||
kclManager.ast,
|
{
|
||||||
selectionRanges,
|
selectionRanges,
|
||||||
pathToNodeMap
|
}
|
||||||
),
|
)
|
||||||
}
|
await kclManager.updateAst(modifiedAst, true)
|
||||||
|
return {
|
||||||
|
selectionType: 'completeSelection',
|
||||||
|
selection: pathMapToSelections(
|
||||||
|
kclManager.ast,
|
||||||
|
selectionRanges,
|
||||||
|
pathToNodeMap
|
||||||
|
),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'Get ABS X info': async ({
|
||||||
|
selectionRanges,
|
||||||
|
}): Promise<SetSelections> => {
|
||||||
|
const { modifiedAst, pathToNodeMap } =
|
||||||
|
await applyConstraintAbsDistance({
|
||||||
|
constraint: 'xAbs',
|
||||||
|
selectionRanges,
|
||||||
|
})
|
||||||
|
await kclManager.updateAst(modifiedAst, true)
|
||||||
|
return {
|
||||||
|
selectionType: 'completeSelection',
|
||||||
|
selection: pathMapToSelections(
|
||||||
|
kclManager.ast,
|
||||||
|
selectionRanges,
|
||||||
|
pathToNodeMap
|
||||||
|
),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'Get ABS Y info': async ({
|
||||||
|
selectionRanges,
|
||||||
|
}): Promise<SetSelections> => {
|
||||||
|
const { modifiedAst, pathToNodeMap } =
|
||||||
|
await applyConstraintAbsDistance({
|
||||||
|
constraint: 'yAbs',
|
||||||
|
selectionRanges,
|
||||||
|
})
|
||||||
|
await kclManager.updateAst(modifiedAst, true)
|
||||||
|
return {
|
||||||
|
selectionType: 'completeSelection',
|
||||||
|
selection: pathMapToSelections(
|
||||||
|
kclManager.ast,
|
||||||
|
selectionRanges,
|
||||||
|
pathToNodeMap
|
||||||
|
),
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
devTools: true,
|
||||||
devTools: true,
|
}
|
||||||
})
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
engineCommandManager.onPlaneSelected((plane_id: string) => {
|
engineCommandManager.onPlaneSelected((plane_id: string) => {
|
||||||
@ -538,13 +559,17 @@ export const ModelingMachineProvider = ({
|
|||||||
})
|
})
|
||||||
}, [modelingSend])
|
}, [modelingSend])
|
||||||
|
|
||||||
// useStateMachineCommands({
|
useStateMachineCommands({
|
||||||
// state: settingsState,
|
machineId: 'modeling',
|
||||||
// send: settingsSend,
|
state: modelingState,
|
||||||
// commands,
|
send: modelingSend,
|
||||||
// owner: 'settings',
|
actor: modelingActor,
|
||||||
// commandBarMeta: settingsCommandBarMeta,
|
commandBarConfig: modelingMachineConfig,
|
||||||
// })
|
onCancel: () => {
|
||||||
|
console.log('firing onCancel!!')
|
||||||
|
modelingSend({ type: 'Cancel' })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModelingMachineContext.Provider
|
<ModelingMachineContext.Provider
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { fireEvent, render, screen } from '@testing-library/react'
|
import { fireEvent, render, screen } from '@testing-library/react'
|
||||||
import { BrowserRouter } from 'react-router-dom'
|
import { BrowserRouter } from 'react-router-dom'
|
||||||
import { GlobalStateProvider } from './GlobalStateProvider'
|
import { GlobalStateProvider } from './GlobalStateProvider'
|
||||||
import CommandBarProvider from './CommandBar'
|
import CommandBarProvider from './CommandBar/CommandBar'
|
||||||
import {
|
import {
|
||||||
NETWORK_CONTENT,
|
NETWORK_CONTENT,
|
||||||
NetworkHealthIndicator,
|
NetworkHealthIndicator,
|
||||||
|
|||||||
@ -1,8 +1,4 @@
|
|||||||
import {
|
import { faExclamation, faWifi } from '@fortawesome/free-solid-svg-icons'
|
||||||
faCheck,
|
|
||||||
faExclamation,
|
|
||||||
faWifi,
|
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
|
||||||
import { Popover } from '@headlessui/react'
|
import { Popover } from '@headlessui/react'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { ActionIcon } from './ActionIcon'
|
import { ActionIcon } from './ActionIcon'
|
||||||
@ -77,8 +73,8 @@ export const NetworkHealthIndicator = () => {
|
|||||||
data-testid="network-good"
|
data-testid="network-good"
|
||||||
>
|
>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
icon={faCheck}
|
icon="checkmark"
|
||||||
bgClassName={'bg-succeed-10/50 dark:bg-succeed-80/50 rounded'}
|
bgClassName={'bg-succeed-10/50 dark:bg-succeed-80/50 rounded-sm'}
|
||||||
iconClassName={'text-succeed-80 dark:text-succeed-30'}
|
iconClassName={'text-succeed-80 dark:text-succeed-30'}
|
||||||
/>
|
/>
|
||||||
{NETWORK_CONTENT.good}
|
{NETWORK_CONTENT.good}
|
||||||
|
|||||||
@ -143,7 +143,7 @@ function ProjectCard({
|
|||||||
className: 'p-1',
|
className: 'p-1',
|
||||||
size: 'xs',
|
size: 'xs',
|
||||||
bgClassName: 'bg-destroy-80',
|
bgClassName: 'bg-destroy-80',
|
||||||
iconClassName: 'text-destroy-20 dark:text-destroy-40',
|
iconClassName: '!text-destroy-20 dark:!text-destroy-40',
|
||||||
}}
|
}}
|
||||||
className="!p-0 hover:border-destroy-40 dark:hover:border-destroy-40"
|
className="!p-0 hover:border-destroy-40 dark:hover:border-destroy-40"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
@ -185,8 +185,7 @@ function ProjectCard({
|
|||||||
bgClassName: 'bg-destroy-80',
|
bgClassName: 'bg-destroy-80',
|
||||||
className: 'p-1',
|
className: 'p-1',
|
||||||
size: 'sm',
|
size: 'sm',
|
||||||
iconClassName:
|
iconClassName: '!text-destroy-70 dark:!text-destroy-40',
|
||||||
'text-destroy-20 group-hover:text-destroy-10 hover:text-destroy-10 dark:text-destroy-20 dark:group-hover:text-destroy-10 dark:hover:text-destroy-10',
|
|
||||||
}}
|
}}
|
||||||
className="hover:border-destroy-40 dark:hover:border-destroy-40"
|
className="hover:border-destroy-40 dark:hover:border-destroy-40"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { BrowserRouter } from 'react-router-dom'
|
|||||||
import ProjectSidebarMenu from './ProjectSidebarMenu'
|
import ProjectSidebarMenu from './ProjectSidebarMenu'
|
||||||
import { ProjectWithEntryPointMetadata } from '../Router'
|
import { ProjectWithEntryPointMetadata } from '../Router'
|
||||||
import { GlobalStateProvider } from './GlobalStateProvider'
|
import { GlobalStateProvider } from './GlobalStateProvider'
|
||||||
import CommandBarProvider from './CommandBar'
|
import CommandBarProvider from './CommandBar/CommandBar'
|
||||||
|
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
const projectWellFormed = {
|
const projectWellFormed = {
|
||||||
|
|||||||
@ -243,15 +243,14 @@ export const Stream = ({ className = '' }) => {
|
|||||||
})
|
})
|
||||||
} else if (
|
} else if (
|
||||||
!didDragInStream &&
|
!didDragInStream &&
|
||||||
(state.matches('Sketch.SketchIdle') ||
|
(state.matches('Sketch.SketchIdle') || state.matches('idle'))
|
||||||
state.matches('idle') ||
|
|
||||||
state.matches('awaiting selection'))
|
|
||||||
) {
|
) {
|
||||||
command.cmd = {
|
command.cmd = {
|
||||||
type: 'select_with_point',
|
type: 'select_with_point',
|
||||||
selected_at_window: { x, y },
|
selected_at_window: { x, y },
|
||||||
selection_type: 'add',
|
selection_type: 'add',
|
||||||
}
|
}
|
||||||
|
|
||||||
engineCommandManager.sendSceneCommand(command)
|
engineCommandManager.sendSceneCommand(command)
|
||||||
} else if (!didDragInStream && state.matches('Sketch.Move Tool')) {
|
} else if (!didDragInStream && state.matches('Sketch.Move Tool')) {
|
||||||
command.cmd = {
|
command.cmd = {
|
||||||
|
|||||||
@ -64,7 +64,7 @@ export const TextEditor = ({
|
|||||||
|
|
||||||
const { settings: { context: { textWrapping } = {} } = {} } =
|
const { settings: { context: { textWrapping } = {} } = {} } =
|
||||||
useGlobalStateContext()
|
useGlobalStateContext()
|
||||||
const { setCommandBarOpen } = useCommandsContext()
|
const { commandBarSend } = useCommandsContext()
|
||||||
const { enable: convertEnabled, handleClick: convertCallback } =
|
const { enable: convertEnabled, handleClick: convertCallback } =
|
||||||
useConvertToVariable()
|
useConvertToVariable()
|
||||||
|
|
||||||
@ -136,7 +136,7 @@ export const TextEditor = ({
|
|||||||
{
|
{
|
||||||
key: 'Meta-k',
|
key: 'Meta-k',
|
||||||
run: () => {
|
run: () => {
|
||||||
setCommandBarOpen(true)
|
commandBarSend({ type: 'Open' })
|
||||||
return false
|
return false
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import {
|
|||||||
} from 'react-router-dom'
|
} from 'react-router-dom'
|
||||||
import { Models } from '@kittycad/lib'
|
import { Models } from '@kittycad/lib'
|
||||||
import { GlobalStateProvider } from './GlobalStateProvider'
|
import { GlobalStateProvider } from './GlobalStateProvider'
|
||||||
import CommandBarProvider from './CommandBar'
|
import CommandBarProvider from './CommandBar/CommandBar'
|
||||||
|
|
||||||
type User = Models['User_type']
|
type User = Models['User_type']
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { CommandsContext } from 'components/CommandBar'
|
import { CommandsContext } from 'components/CommandBar/CommandBar'
|
||||||
import { useContext } from 'react'
|
import { useContext } from 'react'
|
||||||
|
|
||||||
export const useCommandsContext = () => {
|
export const useCommandsContext = () => {
|
||||||
|
|||||||
27
src/hooks/usePlatform.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { Platform, platform } from '@tauri-apps/api/os'
|
||||||
|
import { isTauri } from 'lib/isTauri'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
export default function usePlatform() {
|
||||||
|
const [platformName, setPlatformName] = useState<Platform | ''>('')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function getPlatform() {
|
||||||
|
setPlatformName(await platform())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isTauri()) {
|
||||||
|
void getPlatform()
|
||||||
|
} else {
|
||||||
|
if (navigator.userAgent.indexOf('Mac') !== -1) {
|
||||||
|
setPlatformName('darwin')
|
||||||
|
} else if (navigator.userAgent.indexOf('Win') !== -1) {
|
||||||
|
setPlatformName('win32')
|
||||||
|
} else if (navigator.userAgent.indexOf('Linux') !== -1) {
|
||||||
|
setPlatformName('linux')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [setPlatformName])
|
||||||
|
|
||||||
|
return platformName
|
||||||
|
}
|
||||||
@ -1,46 +1,68 @@
|
|||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { AnyStateMachine, StateFrom } from 'xstate'
|
import { AnyStateMachine, InterpreterFrom, StateFrom } from 'xstate'
|
||||||
import {
|
import { createMachineCommand } from '../lib/createMachineCommand'
|
||||||
Command,
|
|
||||||
CommandBarConfig,
|
|
||||||
createMachineCommand,
|
|
||||||
} from '../lib/commands'
|
|
||||||
import { useCommandsContext } from './useCommandsContext'
|
import { useCommandsContext } from './useCommandsContext'
|
||||||
|
import { modelingMachine } from 'machines/modelingMachine'
|
||||||
|
import { authMachine } from 'machines/authMachine'
|
||||||
|
import { settingsMachine } from 'machines/settingsMachine'
|
||||||
|
import { homeMachine } from 'machines/homeMachine'
|
||||||
|
import { Command, CommandSetConfig, CommandSetSchema } from 'lib/commandTypes'
|
||||||
|
|
||||||
interface UseStateMachineCommandsArgs<T extends AnyStateMachine> {
|
// This might not be necessary, AnyStateMachine from xstate is working
|
||||||
|
export type AllMachines =
|
||||||
|
| typeof modelingMachine
|
||||||
|
| typeof settingsMachine
|
||||||
|
| typeof authMachine
|
||||||
|
| typeof homeMachine
|
||||||
|
|
||||||
|
interface UseStateMachineCommandsArgs<
|
||||||
|
T extends AllMachines,
|
||||||
|
S extends CommandSetSchema<T>
|
||||||
|
> {
|
||||||
|
machineId: T['id']
|
||||||
state: StateFrom<T>
|
state: StateFrom<T>
|
||||||
send: Function
|
send: Function
|
||||||
commandBarConfig?: CommandBarConfig<T>
|
actor?: InterpreterFrom<T>
|
||||||
commands: Command[]
|
commandBarConfig?: CommandSetConfig<T, S>
|
||||||
owner: string
|
onCancel?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function useStateMachineCommands<T extends AnyStateMachine>({
|
export default function useStateMachineCommands<
|
||||||
|
T extends AnyStateMachine,
|
||||||
|
S extends CommandSetSchema<T>
|
||||||
|
>({
|
||||||
|
machineId,
|
||||||
state,
|
state,
|
||||||
send,
|
send,
|
||||||
|
actor,
|
||||||
commandBarConfig,
|
commandBarConfig,
|
||||||
owner,
|
onCancel,
|
||||||
}: UseStateMachineCommandsArgs<T>) {
|
}: UseStateMachineCommandsArgs<T, S>) {
|
||||||
const { addCommands, removeCommands } = useCommandsContext()
|
const { commandBarSend } = useCommandsContext()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const newCommands = state.nextEvents
|
const newCommands = state.nextEvents
|
||||||
.filter((e) => !['done.', 'error.'].some((n) => e.includes(n)))
|
.filter((e) => !['done.', 'error.'].some((n) => e.includes(n)))
|
||||||
.map((type) =>
|
.map((type) =>
|
||||||
createMachineCommand<T>({
|
createMachineCommand<T, S>({
|
||||||
|
ownerMachine: machineId,
|
||||||
type,
|
type,
|
||||||
state,
|
state,
|
||||||
send,
|
send,
|
||||||
|
actor,
|
||||||
commandBarConfig,
|
commandBarConfig,
|
||||||
owner,
|
onCancel,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.filter((c) => c !== null) as Command[] // TS isn't smart enough to know this filter removes nulls
|
.filter((c) => c !== null) as Command[] // TS isn't smart enough to know this filter removes nulls
|
||||||
|
|
||||||
addCommands(newCommands)
|
commandBarSend({ type: 'Add commands', data: { commands: newCommands } })
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
removeCommands(newCommands)
|
commandBarSend({
|
||||||
|
type: 'Remove commands',
|
||||||
|
data: { commands: newCommands },
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}, [state])
|
}, [state])
|
||||||
}
|
}
|
||||||
|
|||||||
@ -57,7 +57,7 @@ select {
|
|||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
@apply border border-chalkboard-30 m-0.5 px-3 rounded text-xs;
|
@apply border border-chalkboard-30 m-0.5 px-3 rounded text-xs focus-visible:ring-energy-10;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:hover {
|
button:hover {
|
||||||
@ -65,7 +65,7 @@ button:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dark button {
|
.dark button {
|
||||||
@apply border-chalkboard-70;
|
@apply border-chalkboard-70 focus-visible:ring-energy-10/50;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark button:hover {
|
.dark button:hover {
|
||||||
@ -88,6 +88,14 @@ a:not(.action-button) {
|
|||||||
@apply text-chalkboard-20 hover:text-energy-10;
|
@apply text-chalkboard-20 hover:text-energy-10;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
@apply selection:bg-energy-10/50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark input {
|
||||||
|
@apply selection:bg-energy-10/40;
|
||||||
|
}
|
||||||
|
|
||||||
.mono {
|
.mono {
|
||||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||||
monospace;
|
monospace;
|
||||||
|
|||||||
@ -248,7 +248,8 @@ export function mutateObjExpProp(
|
|||||||
export function extrudeSketch(
|
export function extrudeSketch(
|
||||||
node: Program,
|
node: Program,
|
||||||
pathToNode: PathToNode,
|
pathToNode: PathToNode,
|
||||||
shouldPipe = true
|
shouldPipe = true,
|
||||||
|
distance = 4
|
||||||
): {
|
): {
|
||||||
modifiedAst: Program
|
modifiedAst: Program
|
||||||
pathToNode: PathToNode
|
pathToNode: PathToNode
|
||||||
@ -274,7 +275,7 @@ export function extrudeSketch(
|
|||||||
getNodeFromPath<VariableDeclarator>(_node, pathToNode, 'VariableDeclarator')
|
getNodeFromPath<VariableDeclarator>(_node, pathToNode, 'VariableDeclarator')
|
||||||
|
|
||||||
const extrudeCall = createCallExpressionStdLib('extrude', [
|
const extrudeCall = createCallExpressionStdLib('extrude', [
|
||||||
createLiteral(4),
|
createLiteral(distance),
|
||||||
shouldPipe
|
shouldPipe
|
||||||
? createPipeSubstitution()
|
? createPipeSubstitution()
|
||||||
: {
|
: {
|
||||||
|
|||||||
17
src/lib/commandBarConfigs/authCommandConfig.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { CommandSetConfig } from 'lib/commandTypes'
|
||||||
|
import { authMachine } from 'machines/authMachine'
|
||||||
|
|
||||||
|
type AuthCommandSchema = {}
|
||||||
|
|
||||||
|
export const authCommandBarConfig: CommandSetConfig<
|
||||||
|
typeof authMachine,
|
||||||
|
AuthCommandSchema
|
||||||
|
> = {
|
||||||
|
'Log in': {
|
||||||
|
hide: 'both',
|
||||||
|
},
|
||||||
|
'Log out': {
|
||||||
|
args: [],
|
||||||
|
icon: 'arrowLeft',
|
||||||
|
},
|
||||||
|
}
|
||||||
87
src/lib/commandBarConfigs/homeCommandConfig.ts
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import { CommandSetConfig } from 'lib/commandTypes'
|
||||||
|
import { homeMachine } from 'machines/homeMachine'
|
||||||
|
|
||||||
|
export type HomeCommandSchema = {
|
||||||
|
'Create project': {
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
'Open project': {
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
'Delete project': {
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
'Rename project': {
|
||||||
|
oldName: string
|
||||||
|
newName: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const homeCommandBarConfig: CommandSetConfig<
|
||||||
|
typeof homeMachine,
|
||||||
|
HomeCommandSchema
|
||||||
|
> = {
|
||||||
|
'Open project': {
|
||||||
|
icon: 'arrowRight',
|
||||||
|
description: 'Open a project',
|
||||||
|
args: {
|
||||||
|
name: {
|
||||||
|
inputType: 'options',
|
||||||
|
required: true,
|
||||||
|
options: (context) =>
|
||||||
|
context.projects.map((p) => ({
|
||||||
|
name: p.name!,
|
||||||
|
value: p.name!,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'Create project': {
|
||||||
|
icon: 'folderPlus',
|
||||||
|
description: 'Create a project',
|
||||||
|
args: {
|
||||||
|
name: {
|
||||||
|
inputType: 'string',
|
||||||
|
required: true,
|
||||||
|
defaultValue: (context) => context.defaultProjectName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'Delete project': {
|
||||||
|
icon: 'close',
|
||||||
|
description: 'Delete a project',
|
||||||
|
needsReview: true,
|
||||||
|
args: {
|
||||||
|
name: {
|
||||||
|
inputType: 'options',
|
||||||
|
required: true,
|
||||||
|
options: (context) =>
|
||||||
|
context.projects.map((p) => ({
|
||||||
|
name: p.name!,
|
||||||
|
value: p.name!,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'Rename project': {
|
||||||
|
icon: 'folder',
|
||||||
|
description: 'Rename a project',
|
||||||
|
needsReview: true,
|
||||||
|
args: {
|
||||||
|
oldName: {
|
||||||
|
inputType: 'options',
|
||||||
|
required: true,
|
||||||
|
options: (context) =>
|
||||||
|
context.projects.map((p) => ({
|
||||||
|
name: p.name!,
|
||||||
|
value: p.name!,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
newName: {
|
||||||
|
inputType: 'string',
|
||||||
|
required: true,
|
||||||
|
defaultValue: (context) => context.defaultProjectName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
57
src/lib/commandBarConfigs/modelingCommandConfig.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import { CommandSetConfig } from 'lib/commandTypes'
|
||||||
|
import { Selections } from 'lib/selections'
|
||||||
|
import { modelingMachine } from 'machines/modelingMachine'
|
||||||
|
|
||||||
|
export const EXTRUSION_RESULTS = [
|
||||||
|
'new',
|
||||||
|
'add',
|
||||||
|
'subtract',
|
||||||
|
'intersect',
|
||||||
|
] as const
|
||||||
|
|
||||||
|
export type ModelingCommandSchema = {
|
||||||
|
'Enter sketch': {}
|
||||||
|
Extrude: {
|
||||||
|
selection: Selections // & { type: 'face' } would be cool to lock that down
|
||||||
|
// result: (typeof EXTRUSION_RESULTS)[number]
|
||||||
|
distance: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const modelingMachineConfig: CommandSetConfig<
|
||||||
|
typeof modelingMachine,
|
||||||
|
ModelingCommandSchema
|
||||||
|
> = {
|
||||||
|
'Enter sketch': {
|
||||||
|
description: 'Enter sketch mode.',
|
||||||
|
icon: 'sketch',
|
||||||
|
},
|
||||||
|
Extrude: {
|
||||||
|
description: 'Pull a sketch into 3D along its normal or perpendicular.',
|
||||||
|
icon: 'extrude',
|
||||||
|
needsReview: true,
|
||||||
|
args: {
|
||||||
|
selection: {
|
||||||
|
inputType: 'selection',
|
||||||
|
selectionTypes: ['face'],
|
||||||
|
multiple: false, // TODO: multiple selection
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
// result: {
|
||||||
|
// inputType: 'options',
|
||||||
|
// payload: 'add',
|
||||||
|
// required: true,
|
||||||
|
// options: EXTRUSION_RESULTS.map((r) => ({
|
||||||
|
// name: r,
|
||||||
|
// isCurrent: r === 'add',
|
||||||
|
// value: r,
|
||||||
|
// })),
|
||||||
|
// },
|
||||||
|
distance: {
|
||||||
|
inputType: 'number',
|
||||||
|
defaultValue: 5,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
141
src/lib/commandBarConfigs/settingsCommandConfig.ts
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
import { CommandSetConfig } from '../commandTypes'
|
||||||
|
import {
|
||||||
|
BaseUnit,
|
||||||
|
Toggle,
|
||||||
|
UnitSystem,
|
||||||
|
baseUnitsUnion,
|
||||||
|
settingsMachine,
|
||||||
|
} from 'machines/settingsMachine'
|
||||||
|
import { CameraSystem, cameraSystems } from '../cameraControls'
|
||||||
|
import { Themes } from '../theme'
|
||||||
|
|
||||||
|
// SETTINGS MACHINE
|
||||||
|
export type SettingsCommandSchema = {
|
||||||
|
'Set Base Unit': {
|
||||||
|
baseUnit: BaseUnit
|
||||||
|
}
|
||||||
|
'Set Camera Controls': {
|
||||||
|
cameraControls: CameraSystem
|
||||||
|
}
|
||||||
|
'Set Default Project Name': {
|
||||||
|
defaultProjectName: string
|
||||||
|
}
|
||||||
|
'Set Text Wrapping': {
|
||||||
|
textWrapping: Toggle
|
||||||
|
}
|
||||||
|
'Set Theme': {
|
||||||
|
theme: Themes
|
||||||
|
}
|
||||||
|
'Set Unit System': {
|
||||||
|
unitSystem: UnitSystem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const settingsCommandBarConfig: CommandSetConfig<
|
||||||
|
typeof settingsMachine,
|
||||||
|
SettingsCommandSchema
|
||||||
|
> = {
|
||||||
|
'Set Base Unit': {
|
||||||
|
icon: 'gear',
|
||||||
|
args: {
|
||||||
|
baseUnit: {
|
||||||
|
inputType: 'options',
|
||||||
|
required: true,
|
||||||
|
defaultValue: (context) => context.baseUnit,
|
||||||
|
options: (context) =>
|
||||||
|
Object.values(baseUnitsUnion).map((v) => ({
|
||||||
|
name: v,
|
||||||
|
value: v,
|
||||||
|
isCurrent: v === context.baseUnit,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'Set Camera Controls': {
|
||||||
|
icon: 'gear',
|
||||||
|
args: {
|
||||||
|
cameraControls: {
|
||||||
|
inputType: 'options',
|
||||||
|
required: true,
|
||||||
|
defaultValue: (context) => context.cameraControls,
|
||||||
|
options: (context) =>
|
||||||
|
Object.values(cameraSystems).map((v) => ({
|
||||||
|
name: v,
|
||||||
|
value: v,
|
||||||
|
isCurrent: v === context.cameraControls,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'Set Default Project Name': {
|
||||||
|
icon: 'gear',
|
||||||
|
hide: 'web',
|
||||||
|
args: {
|
||||||
|
defaultProjectName: {
|
||||||
|
inputType: 'string',
|
||||||
|
required: true,
|
||||||
|
defaultValue: (context) => context.defaultProjectName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'Set Text Wrapping': {
|
||||||
|
icon: 'gear',
|
||||||
|
args: {
|
||||||
|
textWrapping: {
|
||||||
|
inputType: 'options',
|
||||||
|
required: true,
|
||||||
|
defaultValue: (context) => context.textWrapping,
|
||||||
|
options: (context) => [
|
||||||
|
{
|
||||||
|
name: 'On',
|
||||||
|
value: 'On' as Toggle,
|
||||||
|
isCurrent: context.textWrapping === 'On',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Off',
|
||||||
|
value: 'Off' as Toggle,
|
||||||
|
isCurrent: context.textWrapping === 'Off',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'Set Theme': {
|
||||||
|
icon: 'gear',
|
||||||
|
args: {
|
||||||
|
theme: {
|
||||||
|
inputType: 'options',
|
||||||
|
required: true,
|
||||||
|
defaultValue: (context) => context.theme,
|
||||||
|
options: (context) =>
|
||||||
|
Object.values(Themes).map((v) => ({
|
||||||
|
name: v,
|
||||||
|
value: v,
|
||||||
|
isCurrent: v === context.theme,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'Set Unit System': {
|
||||||
|
icon: 'gear',
|
||||||
|
args: {
|
||||||
|
unitSystem: {
|
||||||
|
inputType: 'options',
|
||||||
|
required: true,
|
||||||
|
defaultValue: (context) => context.unitSystem,
|
||||||
|
options: (context) => [
|
||||||
|
{
|
||||||
|
name: 'Imperial',
|
||||||
|
value: 'imperial' as UnitSystem,
|
||||||
|
isCurrent: context.unitSystem === 'imperial',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Metric',
|
||||||
|
value: 'metric' as UnitSystem,
|
||||||
|
isCurrent: context.unitSystem === 'metric',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
136
src/lib/commandTypes.ts
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
import { CustomIconName } from 'components/CustomIcon'
|
||||||
|
import { AllMachines } from 'hooks/useStateMachineCommands'
|
||||||
|
import {
|
||||||
|
AnyStateMachine,
|
||||||
|
ContextFrom,
|
||||||
|
EventFrom,
|
||||||
|
InterpreterFrom,
|
||||||
|
} from 'xstate'
|
||||||
|
import { Selection } from './selections'
|
||||||
|
|
||||||
|
type Icon = CustomIconName
|
||||||
|
const PLATFORMS = ['both', 'web', 'desktop'] as const
|
||||||
|
const INPUT_TYPES = ['options', 'string', 'number', 'selection'] as const
|
||||||
|
export type CommandInputType = (typeof INPUT_TYPES)[number]
|
||||||
|
|
||||||
|
export type CommandSetSchema<T extends AnyStateMachine> = Partial<{
|
||||||
|
[EventType in EventFrom<T>['type']]: Record<string, any>
|
||||||
|
}>
|
||||||
|
|
||||||
|
export type CommandSet<
|
||||||
|
T extends AllMachines,
|
||||||
|
Schema extends CommandSetSchema<T>
|
||||||
|
> = Partial<{
|
||||||
|
[EventType in EventFrom<T>['type']]: Command<
|
||||||
|
T,
|
||||||
|
EventFrom<T>['type'],
|
||||||
|
Schema[EventType]
|
||||||
|
>
|
||||||
|
}>
|
||||||
|
|
||||||
|
export type CommandSetConfig<
|
||||||
|
T extends AllMachines,
|
||||||
|
Schema extends CommandSetSchema<T>
|
||||||
|
> = Partial<{
|
||||||
|
[EventType in EventFrom<T>['type']]: CommandConfig<
|
||||||
|
T,
|
||||||
|
EventFrom<T>['type'],
|
||||||
|
Schema[EventType]
|
||||||
|
>
|
||||||
|
}>
|
||||||
|
|
||||||
|
export type Command<
|
||||||
|
T extends AnyStateMachine = AnyStateMachine,
|
||||||
|
CommandName extends EventFrom<T>['type'] = EventFrom<T>['type'],
|
||||||
|
CommandSchema extends CommandSetSchema<T>[CommandName] = CommandSetSchema<T>[CommandName]
|
||||||
|
> = {
|
||||||
|
name: CommandName
|
||||||
|
ownerMachine: T['id']
|
||||||
|
needsReview: boolean
|
||||||
|
onSubmit: (data?: CommandSchema) => void
|
||||||
|
onCancel?: () => void
|
||||||
|
args?: {
|
||||||
|
[ArgName in keyof CommandSchema]: CommandArgument<CommandSchema[ArgName], T>
|
||||||
|
}
|
||||||
|
description?: string
|
||||||
|
icon?: Icon
|
||||||
|
hide?: (typeof PLATFORMS)[number]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CommandConfig<
|
||||||
|
T extends AnyStateMachine = AnyStateMachine,
|
||||||
|
CommandName extends EventFrom<T>['type'] = EventFrom<T>['type'],
|
||||||
|
CommandSchema extends CommandSetSchema<T>[CommandName] = CommandSetSchema<T>[CommandName]
|
||||||
|
> = Omit<
|
||||||
|
Command<T, CommandName, CommandSchema>,
|
||||||
|
'name' | 'ownerMachine' | 'onSubmit' | 'onCancel' | 'args' | 'needsReview'
|
||||||
|
> & {
|
||||||
|
needsReview?: true
|
||||||
|
args?: {
|
||||||
|
[ArgName in keyof CommandSchema]: CommandArgumentConfig<
|
||||||
|
CommandSchema[ArgName],
|
||||||
|
T
|
||||||
|
>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CommandArgumentConfig<
|
||||||
|
OutputType,
|
||||||
|
T extends AnyStateMachine = AnyStateMachine
|
||||||
|
> =
|
||||||
|
| {
|
||||||
|
description?: string
|
||||||
|
required: boolean
|
||||||
|
skip?: true
|
||||||
|
defaultValue?: OutputType | ((context: ContextFrom<T>) => OutputType)
|
||||||
|
payload?: OutputType
|
||||||
|
} & (
|
||||||
|
| {
|
||||||
|
inputType: Extract<CommandInputType, 'options'>
|
||||||
|
options:
|
||||||
|
| CommandArgumentOption<OutputType>[]
|
||||||
|
| ((context: ContextFrom<T>) => CommandArgumentOption<OutputType>[])
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
inputType: Extract<CommandInputType, 'selection'>
|
||||||
|
selectionTypes: Selection['type'][]
|
||||||
|
multiple: boolean
|
||||||
|
}
|
||||||
|
| { inputType: Exclude<CommandInputType, 'options' | 'selection'> }
|
||||||
|
)
|
||||||
|
|
||||||
|
export type CommandArgument<
|
||||||
|
OutputType,
|
||||||
|
T extends AnyStateMachine = AnyStateMachine
|
||||||
|
> =
|
||||||
|
| {
|
||||||
|
description?: string
|
||||||
|
required: boolean
|
||||||
|
payload?: OutputType // Payload sets the initialized value and more importantly its type
|
||||||
|
defaultValue?: OutputType // Default value is used as the starting value for the input on this argument
|
||||||
|
} & (
|
||||||
|
| {
|
||||||
|
inputType: Extract<CommandInputType, 'options'>
|
||||||
|
options: CommandArgumentOption<OutputType>[]
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
inputType: Extract<CommandInputType, 'selection'>
|
||||||
|
selectionTypes: Selection['type'][]
|
||||||
|
actor: InterpreterFrom<T>
|
||||||
|
multiple: boolean
|
||||||
|
}
|
||||||
|
| { inputType: Exclude<CommandInputType, 'options' | 'selection'> }
|
||||||
|
)
|
||||||
|
|
||||||
|
export type CommandArgumentWithName<
|
||||||
|
OutputType,
|
||||||
|
T extends AnyStateMachine = AnyStateMachine
|
||||||
|
> = CommandArgument<OutputType, T> & {
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CommandArgumentOption<A> = {
|
||||||
|
name: string
|
||||||
|
isCurrent?: boolean
|
||||||
|
value: A
|
||||||
|
}
|
||||||
@ -1,177 +0,0 @@
|
|||||||
import { AnyStateMachine, ContextFrom, EventFrom, StateFrom } from 'xstate'
|
|
||||||
import { isTauri } from './isTauri'
|
|
||||||
import { CustomIconName } from 'components/CustomIcon'
|
|
||||||
|
|
||||||
type Icon = CustomIconName
|
|
||||||
type Platform = 'both' | 'web' | 'desktop'
|
|
||||||
type InputType = 'select' | 'string' | 'interaction'
|
|
||||||
export type CommandArgumentOption = { name: string; isCurrent?: boolean }
|
|
||||||
|
|
||||||
// Command arguments can either be defined manually
|
|
||||||
// or flagged as needing to be looked up from the context.
|
|
||||||
// This is useful for things like settings, where
|
|
||||||
// we want to show the current setting value as the default.
|
|
||||||
// The lookup is done in createMachineCommand.
|
|
||||||
type CommandArgumentConfig<T extends AnyStateMachine> = {
|
|
||||||
name: string // TODO: I would love for this to be strongly-typed so we could guarantee it's a valid data payload key on the event type.
|
|
||||||
type: InputType
|
|
||||||
description?: string
|
|
||||||
} & (
|
|
||||||
| {
|
|
||||||
type: 'select'
|
|
||||||
options?: CommandArgumentOption[]
|
|
||||||
getOptionsFromContext?: keyof ContextFrom<T>
|
|
||||||
defaultValue?: string
|
|
||||||
getDefaultValueFromContext?: keyof ContextFrom<T>
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: 'string'
|
|
||||||
defaultValue?: string
|
|
||||||
getDefaultValueFromContext?: keyof ContextFrom<T>
|
|
||||||
}
|
|
||||||
| { type: 'interaction' }
|
|
||||||
)
|
|
||||||
|
|
||||||
export type CommandBarConfig<T extends AnyStateMachine> = Partial<{
|
|
||||||
[EventType in EventFrom<T>['type']]:
|
|
||||||
| {
|
|
||||||
args: CommandArgumentConfig<T>[]
|
|
||||||
formatFunction?: (args: string[]) => string
|
|
||||||
icon?: Icon
|
|
||||||
hide?: Platform
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
hide?: Platform
|
|
||||||
}
|
|
||||||
}>
|
|
||||||
|
|
||||||
export type Command = {
|
|
||||||
owner: string
|
|
||||||
name: string
|
|
||||||
callback: Function
|
|
||||||
icon?: Icon
|
|
||||||
args?: CommandArgument[]
|
|
||||||
formatFunction?: (args: string[]) => string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type CommandArgument = {
|
|
||||||
name: string
|
|
||||||
defaultValue?: string
|
|
||||||
} & (
|
|
||||||
| {
|
|
||||||
type: Extract<InputType, 'select'>
|
|
||||||
options: CommandArgumentOption[]
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: Exclude<InputType, 'select'>
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
interface CreateMachineCommandProps<T extends AnyStateMachine> {
|
|
||||||
type: EventFrom<T>['type']
|
|
||||||
state: StateFrom<T>
|
|
||||||
commandBarConfig?: CommandBarConfig<T>
|
|
||||||
send: Function
|
|
||||||
owner: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Creates a command with subcommands, ready for use in the CommandBar component,
|
|
||||||
// from a more terse Command Bar Meta definition.
|
|
||||||
export function createMachineCommand<T extends AnyStateMachine>({
|
|
||||||
type,
|
|
||||||
state,
|
|
||||||
commandBarConfig,
|
|
||||||
send,
|
|
||||||
owner,
|
|
||||||
}: CreateMachineCommandProps<T>): Command | null {
|
|
||||||
const lookedUpMeta = commandBarConfig && commandBarConfig[type]
|
|
||||||
if (!lookedUpMeta) return null
|
|
||||||
|
|
||||||
// Hide commands based on platform by returning `null`
|
|
||||||
// so the consumer can filter them out
|
|
||||||
if ('hide' in lookedUpMeta) {
|
|
||||||
const { hide } = lookedUpMeta
|
|
||||||
if (hide === 'both') return null
|
|
||||||
else if (hide === 'desktop' && isTauri()) return null
|
|
||||||
else if (hide === 'web' && !isTauri()) return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const icon = ('icon' in lookedUpMeta && lookedUpMeta.icon) || undefined
|
|
||||||
const formatFunction =
|
|
||||||
('formatFunction' in lookedUpMeta && lookedUpMeta.formatFunction) ||
|
|
||||||
undefined
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: type,
|
|
||||||
owner,
|
|
||||||
icon,
|
|
||||||
callback: (data: EventFrom<T, typeof type>) => {
|
|
||||||
if (data !== undefined && data !== null) {
|
|
||||||
send(type, { data })
|
|
||||||
} else {
|
|
||||||
send(type)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
...('args' in lookedUpMeta
|
|
||||||
? {
|
|
||||||
args: getCommandArgumentValuesFromContext(state, lookedUpMeta.args),
|
|
||||||
formatFunction,
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCommandArgumentValuesFromContext<T extends AnyStateMachine>(
|
|
||||||
state: StateFrom<T>,
|
|
||||||
args: CommandArgumentConfig<T>[]
|
|
||||||
): CommandArgument[] {
|
|
||||||
function getDefaultValue(
|
|
||||||
arg: CommandArgumentConfig<T> & { type: 'string' | 'select' }
|
|
||||||
) {
|
|
||||||
if (
|
|
||||||
arg.type === 'select' ||
|
|
||||||
('getDefaultValueFromContext' in arg && arg.getDefaultValueFromContext)
|
|
||||||
) {
|
|
||||||
return state.context[arg.getDefaultValueFromContext]
|
|
||||||
} else {
|
|
||||||
return arg.defaultValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return args.map((arg) => {
|
|
||||||
switch (arg.type) {
|
|
||||||
case 'interaction':
|
|
||||||
return {
|
|
||||||
name: arg.name,
|
|
||||||
type: 'interaction',
|
|
||||||
}
|
|
||||||
case 'string':
|
|
||||||
return {
|
|
||||||
name: arg.name,
|
|
||||||
type: arg.type,
|
|
||||||
defaultValue: arg.getDefaultValueFromContext
|
|
||||||
? state.context[arg.getDefaultValueFromContext]
|
|
||||||
: arg.defaultValue,
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return {
|
|
||||||
name: arg.name,
|
|
||||||
type: arg.type,
|
|
||||||
defaultValue: getDefaultValue(arg),
|
|
||||||
options: arg.getOptionsFromContext
|
|
||||||
? state.context[arg.getOptionsFromContext].map(
|
|
||||||
(v: string | { name: string }) => ({
|
|
||||||
name: typeof v === 'string' ? v : v.name,
|
|
||||||
isCurrent: v === getDefaultValue(arg),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
: arg.getDefaultValueFromContext
|
|
||||||
? arg.options?.map((v) => ({
|
|
||||||
...v,
|
|
||||||
isCurrent: v.name === getDefaultValue(arg),
|
|
||||||
}))
|
|
||||||
: arg.options,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
158
src/lib/createMachineCommand.ts
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
import { AnyStateMachine, EventFrom, InterpreterFrom, StateFrom } from 'xstate'
|
||||||
|
import { isTauri } from './isTauri'
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandArgument,
|
||||||
|
CommandArgumentConfig,
|
||||||
|
CommandConfig,
|
||||||
|
CommandSetConfig,
|
||||||
|
CommandSetSchema,
|
||||||
|
} from './commandTypes'
|
||||||
|
|
||||||
|
interface CreateMachineCommandProps<
|
||||||
|
T extends AnyStateMachine,
|
||||||
|
S extends CommandSetSchema<T>
|
||||||
|
> {
|
||||||
|
type: EventFrom<T>['type']
|
||||||
|
ownerMachine: T['id']
|
||||||
|
state: StateFrom<T>
|
||||||
|
send: Function
|
||||||
|
actor?: InterpreterFrom<T>
|
||||||
|
commandBarConfig?: CommandSetConfig<T, S>
|
||||||
|
onCancel?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a command with subcommands, ready for use in the CommandBar component,
|
||||||
|
// from a more terse Command Bar Meta definition.
|
||||||
|
export function createMachineCommand<
|
||||||
|
T extends AnyStateMachine,
|
||||||
|
S extends CommandSetSchema<T>
|
||||||
|
>({
|
||||||
|
ownerMachine,
|
||||||
|
type,
|
||||||
|
state,
|
||||||
|
send,
|
||||||
|
actor,
|
||||||
|
commandBarConfig,
|
||||||
|
onCancel,
|
||||||
|
}: CreateMachineCommandProps<T, S>): Command<
|
||||||
|
T,
|
||||||
|
typeof type,
|
||||||
|
S[typeof type]
|
||||||
|
> | null {
|
||||||
|
const commandConfig = commandBarConfig && commandBarConfig[type]
|
||||||
|
if (!commandConfig) return null
|
||||||
|
|
||||||
|
// Hide commands based on platform by returning `null`
|
||||||
|
// so the consumer can filter them out
|
||||||
|
if ('hide' in commandConfig) {
|
||||||
|
const { hide } = commandConfig
|
||||||
|
if (hide === 'both') return null
|
||||||
|
else if (hide === 'desktop' && isTauri()) return null
|
||||||
|
else if (hide === 'web' && !isTauri()) return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const icon = ('icon' in commandConfig && commandConfig.icon) || undefined
|
||||||
|
|
||||||
|
const command: Command<T, typeof type, S[typeof type]> = {
|
||||||
|
name: type,
|
||||||
|
ownerMachine: ownerMachine,
|
||||||
|
icon,
|
||||||
|
needsReview: commandConfig.needsReview || false,
|
||||||
|
onSubmit: (data?: S[typeof type]) => {
|
||||||
|
if (data !== undefined && data !== null) {
|
||||||
|
send(type, { data })
|
||||||
|
} else {
|
||||||
|
send(type)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if (commandConfig.args) {
|
||||||
|
const newArgs = buildCommandArguments(state, commandConfig.args, actor)
|
||||||
|
|
||||||
|
command.args = newArgs
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onCancel) {
|
||||||
|
command.onCancel = onCancel
|
||||||
|
}
|
||||||
|
|
||||||
|
return command
|
||||||
|
}
|
||||||
|
|
||||||
|
// Takes the args from a CommandConfig and creates
|
||||||
|
// a finalized CommandArgument object for each one,
|
||||||
|
// bundled together into the args for a Command.
|
||||||
|
function buildCommandArguments<
|
||||||
|
T extends AnyStateMachine,
|
||||||
|
S extends CommandSetSchema<T>,
|
||||||
|
CommandName extends EventFrom<T>['type'] = EventFrom<T>['type']
|
||||||
|
>(
|
||||||
|
state: StateFrom<T>,
|
||||||
|
args: CommandConfig<T, CommandName, S>['args'],
|
||||||
|
actor?: InterpreterFrom<T>
|
||||||
|
): NonNullable<Command<T, CommandName, S>['args']> {
|
||||||
|
const newArgs = {} as NonNullable<Command<T, CommandName, S>['args']>
|
||||||
|
|
||||||
|
for (const arg in args) {
|
||||||
|
const argConfig = args[arg] as CommandArgumentConfig<S[typeof arg], T>
|
||||||
|
const newArg = buildCommandArgument(argConfig, state, actor)
|
||||||
|
newArgs[arg] = newArg
|
||||||
|
}
|
||||||
|
|
||||||
|
return newArgs
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCommandArgument<
|
||||||
|
O extends CommandSetSchema<T>,
|
||||||
|
T extends AnyStateMachine
|
||||||
|
>(
|
||||||
|
arg: CommandArgumentConfig<O, T>,
|
||||||
|
state: StateFrom<T>,
|
||||||
|
actor?: InterpreterFrom<T>
|
||||||
|
): CommandArgument<O, T> & { inputType: typeof arg.inputType } {
|
||||||
|
const baseCommandArgument = {
|
||||||
|
description: arg.description,
|
||||||
|
required: arg.required,
|
||||||
|
payload: arg.payload,
|
||||||
|
defaultValue:
|
||||||
|
arg.defaultValue instanceof Function
|
||||||
|
? arg.defaultValue(state.context)
|
||||||
|
: arg.defaultValue,
|
||||||
|
} satisfies Omit<CommandArgument<O, T>, 'inputType'>
|
||||||
|
|
||||||
|
if (arg.inputType === 'options') {
|
||||||
|
const options = arg.options
|
||||||
|
? arg.options instanceof Function
|
||||||
|
? arg.options(state.context)
|
||||||
|
: arg.options
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
if (!options) {
|
||||||
|
throw new Error('Options must be provided for options input type')
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
inputType: arg.inputType,
|
||||||
|
...baseCommandArgument,
|
||||||
|
options,
|
||||||
|
} satisfies CommandArgument<O, T> & { inputType: 'options' }
|
||||||
|
} else if (arg.inputType === 'selection') {
|
||||||
|
if (!actor)
|
||||||
|
throw new Error('Actor must be provided for selection input type')
|
||||||
|
|
||||||
|
return {
|
||||||
|
inputType: arg.inputType,
|
||||||
|
...baseCommandArgument,
|
||||||
|
multiple: arg.multiple,
|
||||||
|
selectionTypes: arg.selectionTypes,
|
||||||
|
actor,
|
||||||
|
} satisfies CommandArgument<O, T> & { inputType: 'selection' }
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
inputType: arg.inputType,
|
||||||
|
...baseCommandArgument,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -7,6 +7,10 @@ import { EditorSelection } from '@codemirror/state'
|
|||||||
import { kclManager } from 'lang/KclSinglton'
|
import { kclManager } from 'lang/KclSinglton'
|
||||||
import { SelectionRange } from '@uiw/react-codemirror'
|
import { SelectionRange } from '@uiw/react-codemirror'
|
||||||
import { isOverlap } from 'lib/utils'
|
import { isOverlap } from 'lib/utils'
|
||||||
|
import { isCursorInSketchCommandRange } from 'lang/util'
|
||||||
|
import { Program } from 'lang/wasm'
|
||||||
|
import { doesPipeHaveCallExp } from 'lang/queryAst'
|
||||||
|
import { CommandArgument } from './commandTypes'
|
||||||
|
|
||||||
export const X_AXIS_UUID = 'ad792545-7fd3-482a-a602-a93924e3055b'
|
export const X_AXIS_UUID = 'ad792545-7fd3-482a-a602-a93924e3055b'
|
||||||
export const Y_AXIS_UUID = '680fd157-266f-4b8a-984f-cdf46b8bdf01'
|
export const Y_AXIS_UUID = '680fd157-266f-4b8a-984f-cdf46b8bdf01'
|
||||||
@ -371,3 +375,128 @@ function resetAndSetEngineEntitySelectionCmds(
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isSketchPipe(selectionRanges: Selections) {
|
||||||
|
return isCursorInSketchCommandRange(
|
||||||
|
engineCommandManager.artifactMap,
|
||||||
|
selectionRanges
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isSelectionLastLine(
|
||||||
|
selectionRanges: Selections,
|
||||||
|
code: string,
|
||||||
|
i = 0
|
||||||
|
) {
|
||||||
|
return selectionRanges.codeBasedSelections[i].range[1] === code.length
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CommonASTNode = {
|
||||||
|
selection: Selection
|
||||||
|
ast: Program
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildCommonNodeFromSelection(
|
||||||
|
selectionRanges: Selections,
|
||||||
|
i: number
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
selection: selectionRanges.codeBasedSelections[i],
|
||||||
|
ast: kclManager.ast,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function nodeHasExtrude(node: CommonASTNode) {
|
||||||
|
return doesPipeHaveCallExp({
|
||||||
|
calleeName: 'extrude',
|
||||||
|
...node,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function nodeHasClose(node: CommonASTNode) {
|
||||||
|
return doesPipeHaveCallExp({
|
||||||
|
calleeName: 'close',
|
||||||
|
...node,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function canExtrudeSelection(selection: Selections) {
|
||||||
|
const commonNodes = selection.codeBasedSelections.map((_, i) =>
|
||||||
|
buildCommonNodeFromSelection(selection, i)
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
!!isSketchPipe(selection) &&
|
||||||
|
commonNodes.every((n) => nodeHasClose(n)) &&
|
||||||
|
commonNodes.every((n) => !nodeHasExtrude(n))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function canExtrudeSelectionItem(selection: Selections, i: number) {
|
||||||
|
const commonNode = buildCommonNodeFromSelection(selection, i)
|
||||||
|
|
||||||
|
return (
|
||||||
|
!!isSketchPipe(selection) &&
|
||||||
|
nodeHasClose(commonNode) &&
|
||||||
|
!nodeHasExtrude(commonNode)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This accounts for non-geometry selections under "other"
|
||||||
|
export type ResolvedSelectionType = [Selection['type'] | 'other', number]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In the future, I'd like this function to properly return the type of each selected entity based on
|
||||||
|
* its code source range, so that we can show something like "0 objects" or "1 face" or "1 line, 2 edges",
|
||||||
|
* and then validate the selection in CommandBarSelectionInput.tsx and show the proper label.
|
||||||
|
* @param selection
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function getSelectionType(
|
||||||
|
selection: Selections
|
||||||
|
): ResolvedSelectionType[] {
|
||||||
|
return selection.codeBasedSelections
|
||||||
|
.map((s, i) => {
|
||||||
|
if (canExtrudeSelectionItem(selection, i)) {
|
||||||
|
return ['face', 1] as ResolvedSelectionType // This is implicitly determining what a face is, which is bad
|
||||||
|
} else {
|
||||||
|
return ['other', 1] as ResolvedSelectionType
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.reduce((acc, [type, count]) => {
|
||||||
|
const foundIndex = acc.findIndex((item) => item && item[0] === type)
|
||||||
|
|
||||||
|
if (foundIndex === -1) {
|
||||||
|
return [...acc, [type, count]]
|
||||||
|
} else {
|
||||||
|
const temp = [...acc]
|
||||||
|
temp[foundIndex][1] += count
|
||||||
|
return temp
|
||||||
|
}
|
||||||
|
}, [] as ResolvedSelectionType[])
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSelectionTypeDisplayText(
|
||||||
|
selection: Selections
|
||||||
|
): string | null {
|
||||||
|
const selectionsByType = getSelectionType(selection)
|
||||||
|
|
||||||
|
return (selectionsByType as Exclude<typeof selectionsByType, 'none'>)
|
||||||
|
.map(([type, count]) => `${count} ${type}${count > 1 ? 's' : ''}`)
|
||||||
|
.join(', ')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function canSubmitSelectionArg(
|
||||||
|
selectionsByType: 'none' | ResolvedSelectionType[],
|
||||||
|
argument: CommandArgument<unknown> & { inputType: 'selection' }
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
selectionsByType !== 'none' &&
|
||||||
|
selectionsByType.every(([type, count]) => {
|
||||||
|
const foundIndex = argument.selectionTypes.findIndex((s) => s === type)
|
||||||
|
return (
|
||||||
|
foundIndex !== -1 &&
|
||||||
|
(!argument.multiple ? count < 2 && count > 0 : count > 0)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { createMachine, assign } from 'xstate'
|
import { createMachine, assign } from 'xstate'
|
||||||
import { Models } from '@kittycad/lib'
|
import { Models } from '@kittycad/lib'
|
||||||
import withBaseURL from '../lib/withBaseURL'
|
import withBaseURL from '../lib/withBaseURL'
|
||||||
import { CommandBarConfig } from '../lib/commands'
|
|
||||||
import { isTauri } from 'lib/isTauri'
|
import { isTauri } from 'lib/isTauri'
|
||||||
import { invoke } from '@tauri-apps/api'
|
import { invoke } from '@tauri-apps/api'
|
||||||
import { VITE_KC_API_BASE_URL } from 'env'
|
import { VITE_KC_API_BASE_URL } from 'env'
|
||||||
@ -40,16 +39,6 @@ export type Events =
|
|||||||
export const TOKEN_PERSIST_KEY = 'TOKEN_PERSIST_KEY'
|
export const TOKEN_PERSIST_KEY = 'TOKEN_PERSIST_KEY'
|
||||||
const persistedToken = localStorage?.getItem(TOKEN_PERSIST_KEY) || ''
|
const persistedToken = localStorage?.getItem(TOKEN_PERSIST_KEY) || ''
|
||||||
|
|
||||||
export const authCommandBarConfig: CommandBarConfig<typeof authMachine> = {
|
|
||||||
'Log in': {
|
|
||||||
hide: 'both',
|
|
||||||
},
|
|
||||||
'Log out': {
|
|
||||||
args: [],
|
|
||||||
icon: 'arrowLeft',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export const authMachine = createMachine<UserContext, Events>(
|
export const authMachine = createMachine<UserContext, Events>(
|
||||||
{
|
{
|
||||||
id: 'Auth',
|
id: 'Auth',
|
||||||
|
|||||||
425
src/machines/commandBarMachine.ts
Normal file
@ -0,0 +1,425 @@
|
|||||||
|
import { assign, createMachine } from 'xstate'
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandArgument,
|
||||||
|
CommandArgumentWithName,
|
||||||
|
} from 'lib/commandTypes'
|
||||||
|
import { Selections } from 'lib/selections'
|
||||||
|
|
||||||
|
export const commandBarMachine = createMachine(
|
||||||
|
{
|
||||||
|
/** @xstate-layout N4IgpgJg5mDOIC5QGED2BbdBDAdhABAEJYBOAxMgDaqxgDaADALqKgAONAlgC6eo6sQAD0QBaAJwA6AGwAmAKwBmBoukAWafIAcDcSoA0IAJ6JZDaZIDs8hgzV6AjA61a1DWQF8PhtJlwFiciowUkYWJBAOWB4+AQiRBFF5CwdpcVkHS1lpVyU5QxNEh1lFGTUsrUUtOQd5SwZLLx8MbDwiUkkqGkgyAHk2MBwwwSiY-kEEswdJNWcNbPFLNMr5AsRFWUtJcVSdRR2VVMUmkF9WgI6u2ggyADFONv98WkowAGNufDeW-2GI0d443iiHESkktUUG1klTsSi0awQDgYWhmqkWjnUslBWhOZyegU61GuZAAghACN8-HhYH92FxAXFQAlRA5FpJ5FjFPYtNDpIpLOkEW4GNs4eJxJoGvJxOVcT82gSrj0AEpgdCoABuYC+8ogNOYI3psQmYlkGUkU2slhc6mkmUUCIyKLc4i0lkU8iUyjUHLlVIuJEkAGUwK8Pg8oDr-WQQ2HPpTzrTIkagUzEDkLHZkQK1CUtKCHAiNlsdjkVAdFEc-ed2oGAOJYbgACzAJAj+FIUAAruhBtxYGQACJwUPveO6pMA43AhA5UqWXOzBjS-nOR3LySqXNODTyZwONTV-GXXXPUcfHqTlOM4TrSublb5-lLewlIVySRaOrmGw-jLSI8FRPf0zzjS8HHCOlogZE1ERUEUSgPa05yQ1ZjEQeZP2-VwDzUN1zEabxTlPAkG2bVt207Hs+wHZAm1wGAvi7EgSD7DsSG7XscG4K9oOnNNEVUeQZGSOxc0qaQ7HhdDBJFeRc35extGkNI+UAgNJDIls2xwSMqK4-tJBJAB3LAYl0-AHjYLtuBjLsACN0B4djOL7XixhvBIDxUT8qj5VIkTwtQHRk8oUQcD15LMXQclcdTa00xttMojjqO42BJAANSwShOAgRsIzICB+DASQHg1VAAGtSo1HK8sbMASVSgz3JgmdMnKGYzRlbIdGXA8EX8zd3RsTY9yyY4iLxID6ySiiLP0misrq-LeF0shWxIVBAzYShGwAM229BJFq3LVsa5q3INf5r1gg9JIfXqqh0TQsiFTYrCyJZRpsXJ4oJVUNU4MBjLsxznITX5rqgjzYMsaZtGXaVNhcZFKgRd0RXUdJ6mUXQXX+jpAeB0GyQIRbuNa-jbwQcVSmhAUeTkWQ3HyGSBVKDQMyRAKAsJwNiZBshVXVLUXLSnjoeTPjUxpnmpFtcVs1cPNBq5T8fR5DrKwaHEppIomwCBoWAFEIGcinJcg6XYZnTRhJsbQJU9VlQTVtQNbqcVZhsSFxH5zoWzeSr2ya1z0qKkqypwCrqpOlaGrDiX9WtqdZYSeSEeXEplCceo1xkvQLGCmUIp0Mwck8fWQMVIOQ4spODIHYqcFK8qqpqhPuAu8P+zoCDDRlzzTCkCVWWlSxrBceTygRFxZHZNJbE2VQ5AFAO6PeevI0bmiNpY7bJF2g6jvjs7E8u9KqfTxAkK2fZYuRvdYUGjQZnqYVxRKdx-ZOHBUAgHAQQ00AyD1tgJUQCwLQMEyHUG0Gh7SOmmDuGB6gOTyVBMkDeRJIBgLahA4oFo8zWlSPUT00o0KFA9NMYKrhKwbH2GaQ81cawEljGOdskM8B4OpgkWY9MV5ZjqLUN6MlqGojoRFEo6QWYb1PC8McuCbpD1gosLYrhkTZCxNIawhYxEfW0HhCKKgzT8lBAHLS809KX37Dwm+iIWZSEinjTRy51BFnvNofM7hGZfgXBYuaOlrG9wyiZMya1IxWRsnY4eDi2TONsK45IaghQbEkO4VIKlsaVE2AE8iQTxZN2WufCJMS7o6JSBKNQLp7CT35EKZw2wnDVDyCvBczDmg10NsbYyZS7aZHBNU58q8sTQgxnud+kVsjyQzHrTprDLh11DjY+AyjwFy00DQ2EaQBSLAPLIDGWRNw2BUiXHkwVCJeCAA */
|
||||||
|
context: {
|
||||||
|
commands: [] as Command[],
|
||||||
|
selectedCommand: undefined as Command | undefined,
|
||||||
|
currentArgument: undefined as
|
||||||
|
| (CommandArgument<unknown> & { name: string })
|
||||||
|
| undefined,
|
||||||
|
selectionRanges: {
|
||||||
|
otherSelections: [],
|
||||||
|
codeBasedSelections: [],
|
||||||
|
} as Selections,
|
||||||
|
argumentsToSubmit: {} as { [x: string]: unknown },
|
||||||
|
},
|
||||||
|
id: 'Command Bar',
|
||||||
|
initial: 'Closed',
|
||||||
|
states: {
|
||||||
|
Closed: {
|
||||||
|
on: {
|
||||||
|
Open: {
|
||||||
|
target: 'Selecting command',
|
||||||
|
},
|
||||||
|
|
||||||
|
'Find and select command': {
|
||||||
|
target: 'Command selected',
|
||||||
|
actions: [
|
||||||
|
'Find and select command',
|
||||||
|
'Initialize arguments to submit',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
'Add commands': {
|
||||||
|
target: 'Closed',
|
||||||
|
|
||||||
|
actions: [
|
||||||
|
assign({
|
||||||
|
commands: (context, event) =>
|
||||||
|
[...context.commands, ...event.data.commands].sort(
|
||||||
|
sortCommands
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
|
||||||
|
internal: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
'Remove commands': {
|
||||||
|
target: 'Closed',
|
||||||
|
|
||||||
|
actions: [
|
||||||
|
assign({
|
||||||
|
commands: (context, event) =>
|
||||||
|
context.commands.filter(
|
||||||
|
(c) =>
|
||||||
|
!event.data.commands.some(
|
||||||
|
(c2) =>
|
||||||
|
c2.name === c.name &&
|
||||||
|
c2.ownerMachine === c.ownerMachine
|
||||||
|
)
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
|
||||||
|
internal: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
'Selecting command': {
|
||||||
|
on: {
|
||||||
|
'Select command': {
|
||||||
|
target: 'Command selected',
|
||||||
|
actions: ['Set selected command', 'Initialize arguments to submit'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
'Command selected': {
|
||||||
|
always: [
|
||||||
|
{
|
||||||
|
target: 'Closed',
|
||||||
|
cond: 'Command has no arguments',
|
||||||
|
actions: ['Execute command'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: 'Gathering arguments',
|
||||||
|
actions: [
|
||||||
|
assign({
|
||||||
|
currentArgument: (context, event) => {
|
||||||
|
const { selectedCommand } = context
|
||||||
|
if (!(selectedCommand && selectedCommand.args))
|
||||||
|
return undefined
|
||||||
|
const argName = Object.keys(selectedCommand.args)[0]
|
||||||
|
return {
|
||||||
|
...selectedCommand.args[argName],
|
||||||
|
name: argName,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
'Gathering arguments': {
|
||||||
|
states: {
|
||||||
|
'Awaiting input': {
|
||||||
|
on: {
|
||||||
|
'Submit argument': {
|
||||||
|
target: 'Validating',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
Validating: {
|
||||||
|
invoke: {
|
||||||
|
src: 'Validate argument',
|
||||||
|
id: 'validateArgument',
|
||||||
|
onDone: {
|
||||||
|
target: '#Command Bar.Checking Arguments',
|
||||||
|
actions: [
|
||||||
|
assign({
|
||||||
|
argumentsToSubmit: (context, event) => {
|
||||||
|
const [argName, argData] = Object.entries(event.data)[0]
|
||||||
|
const { currentArgument } = context
|
||||||
|
if (!currentArgument) return {}
|
||||||
|
return {
|
||||||
|
...context.argumentsToSubmit,
|
||||||
|
[argName]: argData,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
onError: [
|
||||||
|
{
|
||||||
|
target: 'Awaiting input',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
initial: 'Awaiting input',
|
||||||
|
|
||||||
|
on: {
|
||||||
|
'Change current argument': {
|
||||||
|
target: 'Gathering arguments',
|
||||||
|
internal: true,
|
||||||
|
actions: ['Set current argument'],
|
||||||
|
},
|
||||||
|
|
||||||
|
'Deselect command': {
|
||||||
|
target: 'Selecting command',
|
||||||
|
actions: [
|
||||||
|
assign({
|
||||||
|
selectedCommand: (_c, _e) => undefined,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
Review: {
|
||||||
|
entry: ['Clear current argument'],
|
||||||
|
on: {
|
||||||
|
'Submit command': {
|
||||||
|
target: 'Closed',
|
||||||
|
actions: ['Execute command'],
|
||||||
|
},
|
||||||
|
|
||||||
|
'Add argument': {
|
||||||
|
target: 'Gathering arguments',
|
||||||
|
actions: ['Set current argument'],
|
||||||
|
},
|
||||||
|
|
||||||
|
'Remove argument': {
|
||||||
|
target: 'Review',
|
||||||
|
actions: [
|
||||||
|
assign({
|
||||||
|
argumentsToSubmit: (context, event) => {
|
||||||
|
const argName = Object.keys(event.data)[0]
|
||||||
|
const { argumentsToSubmit } = context
|
||||||
|
const newArgumentsToSubmit = { ...argumentsToSubmit }
|
||||||
|
newArgumentsToSubmit[argName] = undefined
|
||||||
|
return newArgumentsToSubmit
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
'Edit argument': {
|
||||||
|
target: 'Gathering arguments',
|
||||||
|
actions: ['Set current argument'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
'Checking Arguments': {
|
||||||
|
invoke: {
|
||||||
|
src: 'Validate all arguments',
|
||||||
|
id: 'validateArguments',
|
||||||
|
onDone: [
|
||||||
|
{
|
||||||
|
target: 'Review',
|
||||||
|
cond: 'Command needs review',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: 'Closed',
|
||||||
|
actions: 'Execute command',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onError: [
|
||||||
|
{
|
||||||
|
target: 'Gathering arguments',
|
||||||
|
actions: ['Set current argument'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
on: {
|
||||||
|
Close: {
|
||||||
|
target: '.Closed',
|
||||||
|
},
|
||||||
|
|
||||||
|
Clear: {
|
||||||
|
target: '#Command Bar',
|
||||||
|
internal: true,
|
||||||
|
actions: ['Clear argument data'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
events: {} as
|
||||||
|
| { type: 'Open' }
|
||||||
|
| { type: 'Close' }
|
||||||
|
| { type: 'Clear' }
|
||||||
|
| {
|
||||||
|
type: 'Select command'
|
||||||
|
data: { command: Command }
|
||||||
|
}
|
||||||
|
| { type: 'Deselect command' }
|
||||||
|
| { type: 'Submit command'; data: { [x: string]: unknown } }
|
||||||
|
| {
|
||||||
|
type: 'Add argument'
|
||||||
|
data: { argument: CommandArgumentWithName<unknown> }
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'Remove argument'
|
||||||
|
data: { [x: string]: CommandArgumentWithName<unknown> }
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'Edit argument'
|
||||||
|
data: { arg: CommandArgumentWithName<unknown> }
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'Add commands'
|
||||||
|
data: { commands: Command[] }
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'Remove commands'
|
||||||
|
data: { commands: Command[] }
|
||||||
|
}
|
||||||
|
| { type: 'Submit argument'; data: { [x: string]: unknown } }
|
||||||
|
| {
|
||||||
|
type: 'done.invoke.validateArguments'
|
||||||
|
data: { [x: string]: unknown }
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'error.platform.validateArguments'
|
||||||
|
data: { message: string; arg: CommandArgumentWithName<unknown> }
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'Find and select command'
|
||||||
|
data: { name: string; ownerMachine: string }
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'Change current argument'
|
||||||
|
data: { arg: CommandArgumentWithName<unknown> }
|
||||||
|
},
|
||||||
|
},
|
||||||
|
predictableActionArguments: true,
|
||||||
|
preserveActionOrder: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
actions: {
|
||||||
|
'Execute command': (context, event) => {
|
||||||
|
const { selectedCommand } = context
|
||||||
|
if (!selectedCommand) return
|
||||||
|
if (selectedCommand?.args) {
|
||||||
|
selectedCommand?.onSubmit(
|
||||||
|
event.type === 'Submit command' ||
|
||||||
|
event.type === 'done.invoke.validateArguments'
|
||||||
|
? event.data
|
||||||
|
: undefined
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
selectedCommand?.onSubmit()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'Clear current argument': assign({
|
||||||
|
currentArgument: undefined,
|
||||||
|
}),
|
||||||
|
'Set current argument': assign({
|
||||||
|
currentArgument: (context, event) => {
|
||||||
|
switch (event.type) {
|
||||||
|
case 'error.platform.validateArguments':
|
||||||
|
return event.data.arg
|
||||||
|
case 'Edit argument':
|
||||||
|
return event.data.arg
|
||||||
|
case 'Change current argument':
|
||||||
|
return event.data.arg
|
||||||
|
default:
|
||||||
|
return context.currentArgument
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
'Clear argument data': assign({
|
||||||
|
selectedCommand: undefined,
|
||||||
|
currentArgument: undefined,
|
||||||
|
argumentsToSubmit: {},
|
||||||
|
}),
|
||||||
|
'Set selected command': assign({
|
||||||
|
selectedCommand: (c, e) =>
|
||||||
|
e.type === 'Select command' ? e.data.command : c.selectedCommand,
|
||||||
|
}),
|
||||||
|
'Find and select command': assign({
|
||||||
|
selectedCommand: (c, e) => {
|
||||||
|
if (e.type !== 'Find and select command') return c.selectedCommand
|
||||||
|
const found = c.commands.find(
|
||||||
|
(cmd) =>
|
||||||
|
cmd.name === e.data.name &&
|
||||||
|
cmd.ownerMachine === e.data.ownerMachine
|
||||||
|
)
|
||||||
|
|
||||||
|
return !!found ? found : c.selectedCommand
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
'Initialize arguments to submit': assign({
|
||||||
|
argumentsToSubmit: (c, e) => {
|
||||||
|
if (
|
||||||
|
e.type !== 'Select command' &&
|
||||||
|
e.type !== 'Find and select command'
|
||||||
|
)
|
||||||
|
return c.argumentsToSubmit
|
||||||
|
const command =
|
||||||
|
'command' in e.data ? e.data.command : c.selectedCommand!
|
||||||
|
if (!command.args) return {}
|
||||||
|
const args: { [x: string]: unknown } = {}
|
||||||
|
for (const [argName, arg] of Object.entries(command.args)) {
|
||||||
|
args[argName] = arg.payload
|
||||||
|
}
|
||||||
|
return args
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
guards: {
|
||||||
|
'Command needs review': (context, _) =>
|
||||||
|
context.selectedCommand?.needsReview || false,
|
||||||
|
},
|
||||||
|
services: {
|
||||||
|
'Validate argument': (context, event) => {
|
||||||
|
if (event.type !== 'Submit argument') return Promise.reject()
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// TODO: figure out if we should validate argument data here or in the form itself,
|
||||||
|
// and if we should support people configuring a argument's validation function
|
||||||
|
|
||||||
|
resolve(event.data)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
'Validate all arguments': (context, _) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
for (const [argName, arg] of Object.entries(
|
||||||
|
context.argumentsToSubmit
|
||||||
|
)) {
|
||||||
|
let argConfig = context.selectedCommand!.args![argName]
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof arg !== typeof argConfig.payload &&
|
||||||
|
typeof arg !== typeof argConfig.defaultValue &&
|
||||||
|
'options' in argConfig &&
|
||||||
|
typeof arg !== typeof argConfig.options[0].value
|
||||||
|
) {
|
||||||
|
return reject({
|
||||||
|
message: 'Argument payload is of the wrong type',
|
||||||
|
arg: {
|
||||||
|
...argConfig,
|
||||||
|
name: argName,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!arg && argConfig.required) {
|
||||||
|
return reject({
|
||||||
|
message: 'Argument payload is falsy but is required',
|
||||||
|
arg: {
|
||||||
|
...argConfig,
|
||||||
|
name: argName,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolve(context.argumentsToSubmit)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
delays: {},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function sortCommands(a: Command, b: Command) {
|
||||||
|
if (b.ownerMachine === 'auth') return -1
|
||||||
|
if (a.ownerMachine === 'auth') return 1
|
||||||
|
return a.name.localeCompare(b.name)
|
||||||
|
}
|
||||||
74
src/machines/commandBarMachine.typegen.ts
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
// This file was automatically generated. Edits will be overwritten
|
||||||
|
|
||||||
|
export interface Typegen0 {
|
||||||
|
'@@xstate/typegen': true
|
||||||
|
internalEvents: {
|
||||||
|
'': { type: '' }
|
||||||
|
'done.invoke.validateArgument': {
|
||||||
|
type: 'done.invoke.validateArgument'
|
||||||
|
data: unknown
|
||||||
|
__tip: 'See the XState TS docs to learn how to strongly type this.'
|
||||||
|
}
|
||||||
|
'done.invoke.validateArguments': {
|
||||||
|
type: 'done.invoke.validateArguments'
|
||||||
|
data: unknown
|
||||||
|
__tip: 'See the XState TS docs to learn how to strongly type this.'
|
||||||
|
}
|
||||||
|
'error.platform.validateArgument': {
|
||||||
|
type: 'error.platform.validateArgument'
|
||||||
|
data: unknown
|
||||||
|
}
|
||||||
|
'error.platform.validateArguments': {
|
||||||
|
type: 'error.platform.validateArguments'
|
||||||
|
data: unknown
|
||||||
|
}
|
||||||
|
'xstate.init': { type: 'xstate.init' }
|
||||||
|
}
|
||||||
|
invokeSrcNameMap: {
|
||||||
|
'Validate all arguments': 'done.invoke.validateArguments'
|
||||||
|
'Validate argument': 'done.invoke.validateArgument'
|
||||||
|
}
|
||||||
|
missingImplementations: {
|
||||||
|
actions:
|
||||||
|
| 'Add arguments'
|
||||||
|
| 'Close dialog'
|
||||||
|
| 'Execute command'
|
||||||
|
| 'Open dialog'
|
||||||
|
delays: never
|
||||||
|
guards: never
|
||||||
|
services: never
|
||||||
|
}
|
||||||
|
eventsCausingActions: {
|
||||||
|
'Add arguments': 'done.invoke.validateArguments'
|
||||||
|
'Add commands': 'Add commands'
|
||||||
|
'Close dialog': 'Close'
|
||||||
|
'Execute command': '' | 'Submit'
|
||||||
|
'Open dialog': 'Open'
|
||||||
|
'Remove argument': 'Remove argument'
|
||||||
|
'Remove commands': 'Remove commands'
|
||||||
|
'Set current argument':
|
||||||
|
| 'Add argument'
|
||||||
|
| 'Edit argument'
|
||||||
|
| 'error.platform.validateArguments'
|
||||||
|
}
|
||||||
|
eventsCausingDelays: {}
|
||||||
|
eventsCausingGuards: {
|
||||||
|
'Arguments are ready': 'done.invoke.validateArguments'
|
||||||
|
'Command has no arguments': ''
|
||||||
|
}
|
||||||
|
eventsCausingServices: {
|
||||||
|
'Validate all arguments': 'done.invoke.validateArgument'
|
||||||
|
'Validate argument': 'Submit'
|
||||||
|
}
|
||||||
|
matchesStates:
|
||||||
|
| 'Checking Arguments'
|
||||||
|
| 'Closed'
|
||||||
|
| 'Command selected'
|
||||||
|
| 'Gathering arguments'
|
||||||
|
| 'Gathering arguments.Awaiting input'
|
||||||
|
| 'Gathering arguments.Validating'
|
||||||
|
| 'Review'
|
||||||
|
| 'Selecting command'
|
||||||
|
| { 'Gathering arguments'?: 'Awaiting input' | 'Validating' }
|
||||||
|
tags: never
|
||||||
|
}
|
||||||
@ -1,56 +1,6 @@
|
|||||||
import { assign, createMachine } from 'xstate'
|
import { assign, createMachine } from 'xstate'
|
||||||
import { ProjectWithEntryPointMetadata } from '../Router'
|
import { ProjectWithEntryPointMetadata } from '../Router'
|
||||||
import { CommandBarConfig } from '../lib/commands'
|
import { HomeCommandSchema } from 'lib/commandBarConfigs/homeCommandConfig'
|
||||||
|
|
||||||
export const homeCommandConfig: CommandBarConfig<typeof homeMachine> = {
|
|
||||||
'Create project': {
|
|
||||||
icon: 'folderPlus',
|
|
||||||
args: [
|
|
||||||
{
|
|
||||||
name: 'name',
|
|
||||||
type: 'string',
|
|
||||||
getDefaultValueFromContext: 'defaultProjectName',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'Open project': {
|
|
||||||
icon: 'arrowRight',
|
|
||||||
args: [
|
|
||||||
{
|
|
||||||
name: 'name',
|
|
||||||
type: 'select',
|
|
||||||
getOptionsFromContext: 'projects',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'Delete project': {
|
|
||||||
icon: 'close',
|
|
||||||
args: [
|
|
||||||
{
|
|
||||||
name: 'name',
|
|
||||||
type: 'select',
|
|
||||||
getOptionsFromContext: 'projects',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'Rename project': {
|
|
||||||
icon: 'folder',
|
|
||||||
formatFunction: (args: string[]) =>
|
|
||||||
`Rename project "${args[0]}" to "${args[1]}"`,
|
|
||||||
args: [
|
|
||||||
{
|
|
||||||
name: 'oldName',
|
|
||||||
type: 'select',
|
|
||||||
getOptionsFromContext: 'projects',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'newName',
|
|
||||||
type: 'string',
|
|
||||||
getDefaultValueFromContext: 'defaultProjectName',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export const homeMachine = createMachine(
|
export const homeMachine = createMachine(
|
||||||
{
|
{
|
||||||
@ -188,10 +138,10 @@ export const homeMachine = createMachine(
|
|||||||
|
|
||||||
schema: {
|
schema: {
|
||||||
events: {} as
|
events: {} as
|
||||||
| { type: 'Open project'; data: { name: string } }
|
| { type: 'Open project'; data: HomeCommandSchema['Open project'] }
|
||||||
| { type: 'Rename project'; data: { oldName: string; newName: string } }
|
| { type: 'Rename project'; data: HomeCommandSchema['Rename project'] }
|
||||||
| { type: 'Create project'; data: { name: string } }
|
| { type: 'Create project'; data: HomeCommandSchema['Create project'] }
|
||||||
| { type: 'Delete project'; data: { name: string } }
|
| { type: 'Delete project'; data: HomeCommandSchema['Delete project'] }
|
||||||
| { type: 'navigate'; data: { name: string } }
|
| { type: 'navigate'; data: { name: string } }
|
||||||
| {
|
| {
|
||||||
type: 'done.invoke.read-projects'
|
type: 'done.invoke.read-projects'
|
||||||
|
|||||||
@ -32,14 +32,14 @@
|
|||||||
"Get vertical info": "done.invoke.get-vertical-info";
|
"Get vertical info": "done.invoke.get-vertical-info";
|
||||||
};
|
};
|
||||||
missingImplementations: {
|
missingImplementations: {
|
||||||
actions: "AST add line segment" | "AST start new sketch" | "Modify AST" | "Set selection" | "Update code selection cursors" | "create path" | "set tool" | "show default planes" | "sketch exit execute" | "toast extrude failed";
|
actions: "AST add line segment" | "AST start new sketch" | "Modify AST" | "Set selection" | "Update code selection cursors" | "create path" | "set tool" | "show default planes" | "sketch exit execute";
|
||||||
delays: never;
|
delays: never;
|
||||||
guards: "Selection contains axis" | "Selection contains edge" | "Selection contains face" | "Selection contains line" | "Selection contains point" | "Selection is not empty" | "Selection is one face";
|
guards: "Selection contains axis" | "Selection contains edge" | "Selection contains face" | "Selection contains line" | "Selection contains point" | "Selection is not empty" | "Selection is one face" | "has valid extrude selection";
|
||||||
services: "Get ABS X info" | "Get ABS Y info" | "Get angle info" | "Get horizontal info" | "Get length info" | "Get perpendicular distance info" | "Get vertical info";
|
services: "Get ABS X info" | "Get ABS Y info" | "Get angle info" | "Get horizontal info" | "Get length info" | "Get perpendicular distance info" | "Get vertical info";
|
||||||
};
|
};
|
||||||
eventsCausingActions: {
|
eventsCausingActions: {
|
||||||
"AST add line segment": "Add point";
|
"AST add line segment": "Add point";
|
||||||
"AST extrude": "" | "extrude intent";
|
"AST extrude": "Extrude";
|
||||||
"AST start new sketch": "Add point";
|
"AST start new sketch": "Add point";
|
||||||
"Add to code-based selection": "Deselect point" | "Deselect segment" | "Select all" | "Select edge" | "Select face" | "Select point" | "Select segment";
|
"Add to code-based selection": "Deselect point" | "Deselect segment" | "Select all" | "Select edge" | "Select face" | "Select point" | "Select segment";
|
||||||
"Add to other selection": "Select axis";
|
"Add to other selection": "Select axis";
|
||||||
@ -63,7 +63,7 @@
|
|||||||
"edit mode enter": "Enter sketch" | "Re-execute";
|
"edit mode enter": "Enter sketch" | "Re-execute";
|
||||||
"edit_mode_exit": "Cancel";
|
"edit_mode_exit": "Cancel";
|
||||||
"equip select": "CancelSketch" | "Constrain equal length" | "Constrain horizontally align" | "Constrain parallel" | "Constrain remove constraints" | "Constrain snap to X" | "Constrain snap to Y" | "Constrain vertically align" | "Deselect point" | "Deselect segment" | "Enter sketch" | "Make segment horizontal" | "Make segment vertical" | "Re-execute" | "Select default plane" | "Select point" | "Select segment" | "Set selection" | "done.invoke.get-abs-x-info" | "done.invoke.get-abs-y-info" | "done.invoke.get-angle-info" | "done.invoke.get-horizontal-info" | "done.invoke.get-length-info" | "done.invoke.get-perpendicular-distance-info" | "done.invoke.get-vertical-info" | "error.platform.get-abs-x-info" | "error.platform.get-abs-y-info" | "error.platform.get-angle-info" | "error.platform.get-horizontal-info" | "error.platform.get-length-info" | "error.platform.get-perpendicular-distance-info" | "error.platform.get-vertical-info";
|
"equip select": "CancelSketch" | "Constrain equal length" | "Constrain horizontally align" | "Constrain parallel" | "Constrain remove constraints" | "Constrain snap to X" | "Constrain snap to Y" | "Constrain vertically align" | "Deselect point" | "Deselect segment" | "Enter sketch" | "Make segment horizontal" | "Make segment vertical" | "Re-execute" | "Select default plane" | "Select point" | "Select segment" | "Set selection" | "done.invoke.get-abs-x-info" | "done.invoke.get-abs-y-info" | "done.invoke.get-angle-info" | "done.invoke.get-horizontal-info" | "done.invoke.get-length-info" | "done.invoke.get-perpendicular-distance-info" | "done.invoke.get-vertical-info" | "error.platform.get-abs-x-info" | "error.platform.get-abs-y-info" | "error.platform.get-angle-info" | "error.platform.get-horizontal-info" | "error.platform.get-length-info" | "error.platform.get-perpendicular-distance-info" | "error.platform.get-vertical-info";
|
||||||
"hide default planes": "Cancel" | "Select default plane" | "xstate.stop";
|
"hide default planes": "Cancel" | "Select default plane" | "Set selection" | "xstate.stop";
|
||||||
"reset sketch metadata": "Cancel" | "Select default plane";
|
"reset sketch metadata": "Cancel" | "Select default plane";
|
||||||
"set default plane id": "Select default plane";
|
"set default plane id": "Select default plane";
|
||||||
"set sketch metadata": "Enter sketch";
|
"set sketch metadata": "Enter sketch";
|
||||||
@ -72,9 +72,8 @@
|
|||||||
"set tool line": "Equip tool";
|
"set tool line": "Equip tool";
|
||||||
"set tool move": "Equip move tool" | "Re-execute" | "Set selection";
|
"set tool move": "Equip move tool" | "Re-execute" | "Set selection";
|
||||||
"show default planes": "Enter sketch";
|
"show default planes": "Enter sketch";
|
||||||
"sketch exit execute": "Cancel" | "Complete line" | "xstate.stop";
|
"sketch exit execute": "Cancel" | "Complete line" | "Set selection" | "xstate.stop";
|
||||||
"sketch mode enabled": "Enter sketch" | "Re-execute" | "Select default plane";
|
"sketch mode enabled": "Enter sketch" | "Re-execute" | "Select default plane";
|
||||||
"toast extrude failed": "";
|
|
||||||
};
|
};
|
||||||
eventsCausingDelays: {
|
eventsCausingDelays: {
|
||||||
|
|
||||||
@ -105,8 +104,7 @@
|
|||||||
"Selection is one face": "Enter sketch";
|
"Selection is one face": "Enter sketch";
|
||||||
"can move": "";
|
"can move": "";
|
||||||
"can move with execute": "";
|
"can move with execute": "";
|
||||||
"has no selection": "extrude intent";
|
"has valid extrude selection": "Extrude";
|
||||||
"has valid extrude selection": "" | "extrude intent";
|
|
||||||
"is editing existing sketch": "";
|
"is editing existing sketch": "";
|
||||||
};
|
};
|
||||||
eventsCausingServices: {
|
eventsCausingServices: {
|
||||||
@ -118,7 +116,7 @@
|
|||||||
"Get perpendicular distance info": "Constrain perpendicular distance";
|
"Get perpendicular distance info": "Constrain perpendicular distance";
|
||||||
"Get vertical info": "Constrain vertical distance";
|
"Get vertical info": "Constrain vertical distance";
|
||||||
};
|
};
|
||||||
matchesStates: "Sketch" | "Sketch no face" | "Sketch.Await ABS X info" | "Sketch.Await ABS Y info" | "Sketch.Await angle info" | "Sketch.Await horizontal distance info" | "Sketch.Await length info" | "Sketch.Await perpendicular distance info" | "Sketch.Await vertical distance info" | "Sketch.Line Tool" | "Sketch.Line Tool.Done" | "Sketch.Line Tool.Init" | "Sketch.Line Tool.No Points" | "Sketch.Line Tool.Point Added" | "Sketch.Line Tool.Segment Added" | "Sketch.Move Tool" | "Sketch.Move Tool.Move init" | "Sketch.Move Tool.Move with execute" | "Sketch.Move Tool.Move without re-execute" | "Sketch.Move Tool.No move" | "Sketch.SketchIdle" | "awaiting selection" | "checking selection" | "idle" | { "Sketch"?: "Await ABS X info" | "Await ABS Y info" | "Await angle info" | "Await horizontal distance info" | "Await length info" | "Await perpendicular distance info" | "Await vertical distance info" | "Line Tool" | "Move Tool" | "SketchIdle" | { "Line Tool"?: "Done" | "Init" | "No Points" | "Point Added" | "Segment Added";
|
matchesStates: "Sketch" | "Sketch no face" | "Sketch.Await ABS X info" | "Sketch.Await ABS Y info" | "Sketch.Await angle info" | "Sketch.Await horizontal distance info" | "Sketch.Await length info" | "Sketch.Await perpendicular distance info" | "Sketch.Await vertical distance info" | "Sketch.Line Tool" | "Sketch.Line Tool.Done" | "Sketch.Line Tool.Init" | "Sketch.Line Tool.No Points" | "Sketch.Line Tool.Point Added" | "Sketch.Line Tool.Segment Added" | "Sketch.Move Tool" | "Sketch.Move Tool.Move init" | "Sketch.Move Tool.Move with execute" | "Sketch.Move Tool.Move without re-execute" | "Sketch.Move Tool.No move" | "Sketch.SketchIdle" | "idle" | { "Sketch"?: "Await ABS X info" | "Await ABS Y info" | "Await angle info" | "Await horizontal distance info" | "Await length info" | "Await perpendicular distance info" | "Await vertical distance info" | "Line Tool" | "Move Tool" | "SketchIdle" | { "Line Tool"?: "Done" | "Init" | "No Points" | "Point Added" | "Segment Added";
|
||||||
"Move Tool"?: "Move init" | "Move with execute" | "Move without re-execute" | "No move"; }; };
|
"Move Tool"?: "Move init" | "Move with execute" | "Move without re-execute" | "No move"; }; };
|
||||||
tags: never;
|
tags: never;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { assign, createMachine } from 'xstate'
|
import { assign, createMachine } from 'xstate'
|
||||||
import { CommandBarConfig } from '../lib/commands'
|
|
||||||
import { Themes, getSystemTheme, setThemeClass } from '../lib/theme'
|
import { Themes, getSystemTheme, setThemeClass } from '../lib/theme'
|
||||||
import { CameraSystem, cameraSystems } from 'lib/cameraControls'
|
import { CameraSystem } from 'lib/cameraControls'
|
||||||
import { Models } from '@kittycad/lib'
|
import { Models } from '@kittycad/lib'
|
||||||
|
|
||||||
export const DEFAULT_PROJECT_NAME = 'project-$nnn'
|
export const DEFAULT_PROJECT_NAME = 'project-$nnn'
|
||||||
@ -24,85 +23,6 @@ export type Toggle = 'On' | 'Off'
|
|||||||
|
|
||||||
export const SETTINGS_PERSIST_KEY = 'SETTINGS_PERSIST_KEY'
|
export const SETTINGS_PERSIST_KEY = 'SETTINGS_PERSIST_KEY'
|
||||||
|
|
||||||
export const settingsCommandBarConfig: CommandBarConfig<
|
|
||||||
typeof settingsMachine
|
|
||||||
> = {
|
|
||||||
'Set Base Unit': {
|
|
||||||
icon: 'gear',
|
|
||||||
args: [
|
|
||||||
{
|
|
||||||
name: 'baseUnit',
|
|
||||||
type: 'select',
|
|
||||||
getDefaultValueFromContext: 'baseUnit',
|
|
||||||
options: Object.values(baseUnitsUnion).map((v) => ({ name: v })),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'Set Camera Controls': {
|
|
||||||
icon: 'gear',
|
|
||||||
args: [
|
|
||||||
{
|
|
||||||
name: 'cameraControls',
|
|
||||||
type: 'select',
|
|
||||||
getDefaultValueFromContext: 'cameraControls',
|
|
||||||
options: Object.values(cameraSystems).map((v) => ({ name: v })),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'Set Default Directory': {
|
|
||||||
hide: 'both',
|
|
||||||
},
|
|
||||||
'Set Default Project Name': {
|
|
||||||
icon: 'gear',
|
|
||||||
hide: 'web',
|
|
||||||
args: [
|
|
||||||
{
|
|
||||||
name: 'defaultProjectName',
|
|
||||||
type: 'string',
|
|
||||||
getDefaultValueFromContext: 'defaultProjectName',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'Set Onboarding Status': {
|
|
||||||
hide: 'both',
|
|
||||||
},
|
|
||||||
'Set Text Wrapping': {
|
|
||||||
icon: 'gear',
|
|
||||||
args: [
|
|
||||||
{
|
|
||||||
name: 'textWrapping',
|
|
||||||
type: 'select',
|
|
||||||
getDefaultValueFromContext: 'textWrapping',
|
|
||||||
options: [{ name: 'On' }, { name: 'Off' }],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'Set Theme': {
|
|
||||||
icon: 'gear',
|
|
||||||
args: [
|
|
||||||
{
|
|
||||||
name: 'theme',
|
|
||||||
type: 'select',
|
|
||||||
getDefaultValueFromContext: 'theme',
|
|
||||||
options: Object.values(Themes).map((v): { name: string } => ({
|
|
||||||
name: v,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'Set Unit System': {
|
|
||||||
icon: 'gear',
|
|
||||||
args: [
|
|
||||||
{
|
|
||||||
name: 'unitSystem',
|
|
||||||
type: 'select',
|
|
||||||
getDefaultValueFromContext: 'unitSystem',
|
|
||||||
options: [{ name: UnitSystem.Imperial }, { name: UnitSystem.Metric }],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export const settingsMachine = createMachine(
|
export const settingsMachine = createMachine(
|
||||||
{
|
{
|
||||||
/** @xstate-layout N4IgpgJg5mDOIC5QGUwBc0EsB2VYDpMIAbMAYlTQAIAVACzAFswBtABgF1FQAHAe1iYsfbNxAAPRAA42+AEwB2KQFYAzGznKAnADZli1QBoQAT2kBGKfm37lOned3nzqgL6vjlLLgJFSFdCoAETAAMwBDAFdiagAFACc+ACswAGNqADlw5nYuJBB+QWFRfMkEABY5fDYa2rra83LjMwQdLWV8BXLyuxlVLU1Ld090bzxCEnJKYLComODMeLS0PniTXLFCoUwRMTK7fC1zNql7NgUjtnKjU0RlBSqpLVUVPVUda60tYZAvHHG-FNAgBVbBCKjIEywNBMDb5LbFPaILqdfRSORsS4qcxXZqIHqyK6qY4XOxsGTKco-P4+Cb+aYAIXCsDAVFBQjhvAE212pWkskUKnUml0+gUNxaqkU+EccnKF1UCnucnMcjcHl+o3+vkmZBofCgUFIMwARpEoFRYuFsGBiJyCtzEXzWrJlGxlKdVFKvfY1XiEBjyvhVOVzBdzu13pYFNStbTAQFqAB5bAmvjheIQf4QtDhNCRWD2hE7EqgfayHTEh7lHQNSxSf1Scz4cpHHFyFVujTKczuDXYPgQOBiGl4TaOktIhAAWg6X3nC4Xp39050sYw2rpYHHRUnztVhPJqmUlIGbEriv9WhrLZ6uibHcqUr7riAA */
|
/** @xstate-layout N4IgpgJg5mDOIC5QGUwBc0EsB2VYDpMIAbMAYlTQAIAVACzAFswBtABgF1FQAHAe1iYsfbNxAAPRAA42+AEwB2KQFYAzGznKAnADZli1QBoQAT2kBGKfm37lOned3nzqgL6vjlLLgJFSFdCoAETAAMwBDAFdiagAFACc+ACswAGNqADlw5nYuJBB+QWFRfMkEABY5fDYa2rra83LjMwQdLWV8BXLyuxlVLU1Ld090bzxCEnJKYLComODMeLS0PniTXLFCoUwRMTK7fC1zNql7NgUjtnKjU0RlBSqpLVUVPVUda60tYZAvHHG-FNAgBVbBCKjIEywNBMDb5LbFPaILqdfRSORsS4qcxXZqIHqyK6qY4XOxsGTKco-P4+Cb+aYAIXCsDAVFBQjhvAE212pWkskUKnUml0+gUNxaqkU+EccnKF1UCnucnMcjcHl+o3+vkmZBofCgUFIMwARpEoFRYuFsGBiJyCtzEXzWrJlGxlKdVFKvfY1XiEBjyvhVOVzBdzu13pYFNStbTAQFqAB5bAmvjheIQf4QtDhNCRWD2hE7EqgfayHTEh7lHQNSxSf1Scz4cpHHFyFVujTKczuDXYPgQOBiGl4TaOktIhAAWg6X3nC4Xp39050sYw2rpYHHRUnztVhPJqmUlIGbEriv9WhrLZ6uibHcqUr7riAA */
|
||||||
|
|||||||
@ -17,7 +17,7 @@ import { Link } from 'react-router-dom'
|
|||||||
import { ProjectWithEntryPointMetadata, HomeLoaderData } from '../Router'
|
import { ProjectWithEntryPointMetadata, HomeLoaderData } from '../Router'
|
||||||
import Loading from '../components/Loading'
|
import Loading from '../components/Loading'
|
||||||
import { useMachine } from '@xstate/react'
|
import { useMachine } from '@xstate/react'
|
||||||
import { homeCommandConfig, homeMachine } from '../machines/homeMachine'
|
import { homeMachine } from '../machines/homeMachine'
|
||||||
import { ContextFrom, EventFrom } from 'xstate'
|
import { ContextFrom, EventFrom } from 'xstate'
|
||||||
import { paths } from '../Router'
|
import { paths } from '../Router'
|
||||||
import {
|
import {
|
||||||
@ -30,11 +30,12 @@ import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
|||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||||
import { DEFAULT_PROJECT_NAME } from 'machines/settingsMachine'
|
import { DEFAULT_PROJECT_NAME } from 'machines/settingsMachine'
|
||||||
import { sep } from '@tauri-apps/api/path'
|
import { sep } from '@tauri-apps/api/path'
|
||||||
|
import { homeCommandBarConfig } from 'lib/commandBarConfigs/homeCommandConfig'
|
||||||
|
|
||||||
// This route only opens in the Tauri desktop context for now,
|
// This route only opens in the Tauri desktop context for now,
|
||||||
// as defined in Router.tsx, so we can use the Tauri APIs and types.
|
// as defined in Router.tsx, so we can use the Tauri APIs and types.
|
||||||
const Home = () => {
|
const Home = () => {
|
||||||
const { commands, setCommandBarOpen } = useCommandsContext()
|
const { commandBarSend } = useCommandsContext()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { projects: loadedProjects } = useLoaderData() as HomeLoaderData
|
const { projects: loadedProjects } = useLoaderData() as HomeLoaderData
|
||||||
const {
|
const {
|
||||||
@ -56,7 +57,7 @@ const Home = () => {
|
|||||||
event: EventFrom<typeof homeMachine>
|
event: EventFrom<typeof homeMachine>
|
||||||
) => {
|
) => {
|
||||||
if (event.data && 'name' in event.data) {
|
if (event.data && 'name' in event.data) {
|
||||||
setCommandBarOpen(false)
|
commandBarSend({ type: 'Close' })
|
||||||
navigate(
|
navigate(
|
||||||
`${paths.FILE}/${encodeURIComponent(
|
`${paths.FILE}/${encodeURIComponent(
|
||||||
context.defaultDirectory + sep + event.data.name
|
context.defaultDirectory + sep + event.data.name
|
||||||
@ -143,12 +144,11 @@ const Home = () => {
|
|||||||
|
|
||||||
const isSortByModified = sort?.includes('modified') || !sort || sort === null
|
const isSortByModified = sort?.includes('modified') || !sort || sort === null
|
||||||
|
|
||||||
useStateMachineCommands<typeof homeMachine>({
|
useStateMachineCommands({
|
||||||
commands,
|
machineId: 'home',
|
||||||
send,
|
send,
|
||||||
state,
|
state,
|
||||||
commandBarConfig: homeCommandConfig,
|
commandBarConfig: homeCommandBarConfig,
|
||||||
owner: 'home',
|
|
||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
|
import usePlatform from 'hooks/usePlatform'
|
||||||
import { OnboardingButtons, onboardingPaths, useDismiss, useNextClick } from '.'
|
import { OnboardingButtons, onboardingPaths, useDismiss, useNextClick } from '.'
|
||||||
import { useStore } from '../../useStore'
|
import { useStore } from '../../useStore'
|
||||||
import { Platform, platform } from '@tauri-apps/api/os'
|
|
||||||
import { useEffect, useState } from 'react'
|
|
||||||
|
|
||||||
export default function CmdK() {
|
export default function CmdK() {
|
||||||
const { buttonDownInStream } = useStore((s) => ({
|
const { buttonDownInStream } = useStore((s) => ({
|
||||||
@ -9,14 +8,7 @@ export default function CmdK() {
|
|||||||
}))
|
}))
|
||||||
const dismiss = useDismiss()
|
const dismiss = useDismiss()
|
||||||
const next = useNextClick(onboardingPaths.USER_MENU)
|
const next = useNextClick(onboardingPaths.USER_MENU)
|
||||||
const [platformName, setPlatformName] = useState<Platform | ''>('')
|
const platformName = usePlatform()
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
async function getPlatform() {
|
|
||||||
setPlatformName(await platform())
|
|
||||||
}
|
|
||||||
void getPlatform()
|
|
||||||
}, [setPlatformName])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-50 grid items-end justify-center pointer-events-none">
|
<div className="fixed inset-0 z-50 grid items-end justify-center pointer-events-none">
|
||||||
@ -29,13 +21,13 @@ export default function CmdK() {
|
|||||||
<h2 className="text-2xl">Command Bar</h2>
|
<h2 className="text-2xl">Command Bar</h2>
|
||||||
<p className="my-4">
|
<p className="my-4">
|
||||||
Press{' '}
|
Press{' '}
|
||||||
{platformName === 'win32' ? (
|
{platformName === 'darwin' ? (
|
||||||
<>
|
<>
|
||||||
<kbd>Win</kbd> + <kbd>/</kbd>
|
<kbd>⌘</kbd> + <kbd>K</kbd>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<kbd>OS</kbd> + <kbd>K</kbd>
|
<kbd>Ctrl</kbd> + <kbd>/</kbd>
|
||||||
</>
|
</>
|
||||||
)}{' '}
|
)}{' '}
|
||||||
to open the command bar. Try changing your theme with it.
|
to open the command bar. Try changing your theme with it.
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import { faSignInAlt } from '@fortawesome/free-solid-svg-icons'
|
|
||||||
import { ActionButton } from '../components/ActionButton'
|
import { ActionButton } from '../components/ActionButton'
|
||||||
import { isTauri } from '../lib/isTauri'
|
import { isTauri } from '../lib/isTauri'
|
||||||
import { invoke } from '@tauri-apps/api/tauri'
|
import { invoke } from '@tauri-apps/api/tauri'
|
||||||
@ -65,7 +64,7 @@ const SignIn = () => {
|
|||||||
<ActionButton
|
<ActionButton
|
||||||
Element="button"
|
Element="button"
|
||||||
onClick={signInTauri}
|
onClick={signInTauri}
|
||||||
icon={{ icon: faSignInAlt }}
|
icon={{ icon: 'arrowRight' }}
|
||||||
className="w-fit mt-4"
|
className="w-fit mt-4"
|
||||||
data-testid="sign-in-button"
|
data-testid="sign-in-button"
|
||||||
>
|
>
|
||||||
@ -80,7 +79,7 @@ const SignIn = () => {
|
|||||||
typeof window !== 'undefined' &&
|
typeof window !== 'undefined' &&
|
||||||
window.location.href.replace('signin', '')
|
window.location.href.replace('signin', '')
|
||||||
)}`}
|
)}`}
|
||||||
icon={{ icon: faSignInAlt }}
|
icon={{ icon: 'arrowRight' }}
|
||||||
className="w-fit mt-4"
|
className="w-fit mt-4"
|
||||||
>
|
>
|
||||||
Sign in
|
Sign in
|
||||||
|
|||||||
276
src/wasm-lib/Cargo.lock
generated
@ -127,12 +127,6 @@ dependencies = [
|
|||||||
"backtrace",
|
"backtrace",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "approx"
|
|
||||||
version = "0.1.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "08abcc3b4e9339e33a3d0a5ed15d84a687350c05689d825e0f6655eef9e76a94"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arc-swap"
|
name = "arc-swap"
|
||||||
version = "1.6.0"
|
version = "1.6.0"
|
||||||
@ -249,7 +243,7 @@ dependencies = [
|
|||||||
"libm",
|
"libm",
|
||||||
"num-bigint",
|
"num-bigint",
|
||||||
"num-integer",
|
"num-integer",
|
||||||
"num-traits 0.2.17",
|
"num-traits",
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -335,7 +329,7 @@ dependencies = [
|
|||||||
"indexmap 1.9.3",
|
"indexmap 1.9.3",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rand 0.8.5",
|
"rand",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_bytes",
|
"serde_bytes",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@ -400,18 +394,6 @@ version = "1.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cgmath"
|
|
||||||
version = "0.16.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "64a4b57c8f4e3a2e9ac07e0f6abc9c24b6fc9e1b54c3478cfb598f3d0023e51c"
|
|
||||||
dependencies = [
|
|
||||||
"approx",
|
|
||||||
"mint",
|
|
||||||
"num-traits 0.1.43",
|
|
||||||
"rand 0.4.6",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chrono"
|
name = "chrono"
|
||||||
version = "0.4.31"
|
version = "0.4.31"
|
||||||
@ -420,10 +402,8 @@ checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"android-tzdata",
|
"android-tzdata",
|
||||||
"iana-time-zone",
|
"iana-time-zone",
|
||||||
"js-sys",
|
"num-traits",
|
||||||
"num-traits 0.2.17",
|
|
||||||
"serde",
|
"serde",
|
||||||
"wasm-bindgen",
|
|
||||||
"windows-targets 0.48.5",
|
"windows-targets 0.48.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -600,7 +580,7 @@ dependencies = [
|
|||||||
"criterion-plot",
|
"criterion-plot",
|
||||||
"is-terminal",
|
"is-terminal",
|
||||||
"itertools 0.10.5",
|
"itertools 0.10.5",
|
||||||
"num-traits 0.2.17",
|
"num-traits",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"oorandom",
|
"oorandom",
|
||||||
"plotters",
|
"plotters",
|
||||||
@ -762,49 +742,6 @@ dependencies = [
|
|||||||
"syn 2.0.39",
|
"syn 2.0.39",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "diesel"
|
|
||||||
version = "2.1.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "62c6fcf842f17f8c78ecf7c81d75c5ce84436b41ee07e03f490fbb5f5a8731d8"
|
|
||||||
dependencies = [
|
|
||||||
"bigdecimal",
|
|
||||||
"bitflags 2.4.1",
|
|
||||||
"byteorder",
|
|
||||||
"chrono",
|
|
||||||
"diesel_derives",
|
|
||||||
"mysqlclient-sys",
|
|
||||||
"num-bigint",
|
|
||||||
"num-integer",
|
|
||||||
"num-traits 0.2.17",
|
|
||||||
"percent-encoding",
|
|
||||||
"r2d2",
|
|
||||||
"serde_json",
|
|
||||||
"url",
|
|
||||||
"uuid",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "diesel_derives"
|
|
||||||
version = "2.1.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ef8337737574f55a468005a83499da720f20c65586241ffea339db9ecdfd2b44"
|
|
||||||
dependencies = [
|
|
||||||
"diesel_table_macro_syntax",
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 2.0.39",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "diesel_table_macro_syntax"
|
|
||||||
version = "0.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "fc5557efc453706fed5e4fa85006fe9817c224c3f480a34c7e5959fd700921c5"
|
|
||||||
dependencies = [
|
|
||||||
"syn 2.0.39",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "diff"
|
name = "diff"
|
||||||
version = "0.1.13"
|
version = "0.1.13"
|
||||||
@ -869,26 +806,6 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "enum-iterator"
|
|
||||||
version = "1.4.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7add3873b5dd076766ee79c8e406ad1a472c385476b9e38849f8eec24f1be689"
|
|
||||||
dependencies = [
|
|
||||||
"enum-iterator-derive",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "enum-iterator-derive"
|
|
||||||
version = "1.2.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "eecf8589574ce9b895052fa12d69af7a233f99e6107f5cb8dd1044f2a17bfdcb"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 2.0.39",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "equivalent"
|
name = "equivalent"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
@ -905,28 +822,6 @@ dependencies = [
|
|||||||
"windows-sys 0.48.0",
|
"windows-sys 0.48.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "euler"
|
|
||||||
version = "0.4.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3f19d11568a4a46aee488bdab3a2963e5e2c3cfd6091aa0abceaddcea82c0bc1"
|
|
||||||
dependencies = [
|
|
||||||
"approx",
|
|
||||||
"cgmath",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "execution-plan"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"bytes",
|
|
||||||
"kittycad",
|
|
||||||
"kittycad-modeling-cmds",
|
|
||||||
"serde",
|
|
||||||
"thiserror",
|
|
||||||
"uuid",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "expectorate"
|
name = "expectorate"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
@ -1041,12 +936,6 @@ dependencies = [
|
|||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "fuchsia-cprng"
|
|
||||||
version = "0.1.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "funty"
|
name = "funty"
|
||||||
version = "2.0.0"
|
version = "2.0.0"
|
||||||
@ -1400,7 +1289,7 @@ dependencies = [
|
|||||||
"gif",
|
"gif",
|
||||||
"jpeg-decoder",
|
"jpeg-decoder",
|
||||||
"num-rational",
|
"num-rational",
|
||||||
"num-traits 0.2.17",
|
"num-traits",
|
||||||
"png",
|
"png",
|
||||||
"qoi",
|
"qoi",
|
||||||
"tiff",
|
"tiff",
|
||||||
@ -1439,12 +1328,6 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "inflections"
|
|
||||||
version = "1.1.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a257582fdcde896fd96463bf2d40eefea0580021c0712a0e2b028b60b47a837a"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "insta"
|
name = "insta"
|
||||||
version = "1.34.0"
|
version = "1.34.0"
|
||||||
@ -1611,7 +1494,7 @@ dependencies = [
|
|||||||
"log",
|
"log",
|
||||||
"parse-display",
|
"parse-display",
|
||||||
"phonenumber",
|
"phonenumber",
|
||||||
"rand 0.8.5",
|
"rand",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"reqwest-conditional-middleware",
|
"reqwest-conditional-middleware",
|
||||||
"reqwest-middleware",
|
"reqwest-middleware",
|
||||||
@ -1628,42 +1511,6 @@ dependencies = [
|
|||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "kittycad-modeling-cmds"
|
|
||||||
version = "0.1.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c3217f9ffe9dce4b16303eeef539e7e27b743bc1c46eff8ce64657745a2b75ca"
|
|
||||||
dependencies = [
|
|
||||||
"chrono",
|
|
||||||
"data-encoding",
|
|
||||||
"diesel",
|
|
||||||
"diesel_derives",
|
|
||||||
"enum-iterator",
|
|
||||||
"enum-iterator-derive",
|
|
||||||
"euler",
|
|
||||||
"http",
|
|
||||||
"kittycad-unit-conversion-derive",
|
|
||||||
"measurements",
|
|
||||||
"parse-display",
|
|
||||||
"parse-display-derive",
|
|
||||||
"schemars",
|
|
||||||
"serde",
|
|
||||||
"serde_bytes",
|
|
||||||
"uuid",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "kittycad-unit-conversion-derive"
|
|
||||||
version = "0.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7001c46a92c1edce6722a3900539b198230980799035f02d92b4e7df3fc08738"
|
|
||||||
dependencies = [
|
|
||||||
"inflections",
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 1.0.109",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lazy_static"
|
name = "lazy_static"
|
||||||
version = "1.4.0"
|
version = "1.4.0"
|
||||||
@ -1763,15 +1610,6 @@ version = "0.7.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
|
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "measurements"
|
|
||||||
version = "0.11.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f5b734b4e8187ea5777bc29c086f0970a27d8de42061b48f5af32cafc0ca904b"
|
|
||||||
dependencies = [
|
|
||||||
"libm",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.6.4"
|
version = "2.6.4"
|
||||||
@ -1819,12 +1657,6 @@ dependencies = [
|
|||||||
"simd-adler32",
|
"simd-adler32",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "mint"
|
|
||||||
version = "0.5.9"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e53debba6bda7a793e5f99b8dacf19e626084f525f7829104ba9898f367d85ff"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mio"
|
name = "mio"
|
||||||
version = "0.8.9"
|
version = "0.8.9"
|
||||||
@ -1836,16 +1668,6 @@ dependencies = [
|
|||||||
"windows-sys 0.48.0",
|
"windows-sys 0.48.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "mysqlclient-sys"
|
|
||||||
version = "0.2.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f61b381528ba293005c42a409dd73d034508e273bf90481f17ec2e964a6e969b"
|
|
||||||
dependencies = [
|
|
||||||
"pkg-config",
|
|
||||||
"vcpkg",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "newline-converter"
|
name = "newline-converter"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
@ -1873,7 +1695,7 @@ checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
"num-integer",
|
"num-integer",
|
||||||
"num-traits 0.2.17",
|
"num-traits",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1883,7 +1705,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
|
checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
"num-traits 0.2.17",
|
"num-traits",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1894,16 +1716,7 @@ checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
"num-integer",
|
"num-integer",
|
||||||
"num-traits 0.2.17",
|
"num-traits",
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "num-traits"
|
|
||||||
version = "0.1.43"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31"
|
|
||||||
dependencies = [
|
|
||||||
"num-traits 0.2.17",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1989,7 +1802,7 @@ dependencies = [
|
|||||||
"phonenumber",
|
"phonenumber",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"rand 0.8.5",
|
"rand",
|
||||||
"regex",
|
"regex",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"reqwest-middleware",
|
"reqwest-middleware",
|
||||||
@ -2041,7 +1854,7 @@ dependencies = [
|
|||||||
"lazy_static",
|
"lazy_static",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project",
|
"pin-project",
|
||||||
"rand 0.8.5",
|
"rand",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -2196,7 +2009,7 @@ version = "0.3.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d2c224ba00d7cadd4d5c660deaf2098e5e80e07846537c51f9cfa4be50c1fd45"
|
checksum = "d2c224ba00d7cadd4d5c660deaf2098e5e80e07846537c51f9cfa4be50c1fd45"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"num-traits 0.2.17",
|
"num-traits",
|
||||||
"plotters-backend",
|
"plotters-backend",
|
||||||
"plotters-svg",
|
"plotters-svg",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
@ -2313,36 +2126,12 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "r2d2"
|
|
||||||
version = "0.8.10"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93"
|
|
||||||
dependencies = [
|
|
||||||
"log",
|
|
||||||
"parking_lot 0.12.1",
|
|
||||||
"scheduled-thread-pool",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "radium"
|
name = "radium"
|
||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
|
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rand"
|
|
||||||
version = "0.4.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293"
|
|
||||||
dependencies = [
|
|
||||||
"fuchsia-cprng",
|
|
||||||
"libc",
|
|
||||||
"rand_core 0.3.1",
|
|
||||||
"rdrand",
|
|
||||||
"winapi",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand"
|
name = "rand"
|
||||||
version = "0.8.5"
|
version = "0.8.5"
|
||||||
@ -2351,7 +2140,7 @@ checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"rand_chacha",
|
"rand_chacha",
|
||||||
"rand_core 0.6.4",
|
"rand_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2361,24 +2150,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ppv-lite86",
|
"ppv-lite86",
|
||||||
"rand_core 0.6.4",
|
"rand_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rand_core"
|
|
||||||
version = "0.3.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b"
|
|
||||||
dependencies = [
|
|
||||||
"rand_core 0.4.2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rand_core"
|
|
||||||
version = "0.4.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand_core"
|
name = "rand_core"
|
||||||
version = "0.6.4"
|
version = "0.6.4"
|
||||||
@ -2408,15 +2182,6 @@ dependencies = [
|
|||||||
"crossbeam-utils",
|
"crossbeam-utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rdrand"
|
|
||||||
version = "0.4.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2"
|
|
||||||
dependencies = [
|
|
||||||
"rand_core 0.3.1",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.2.16"
|
version = "0.2.16"
|
||||||
@ -2625,7 +2390,7 @@ checksum = "e09bbcb5003282bcb688f0bae741b278e9c7e8f378f561522c9806c58e075d9b"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
"rand 0.8.5",
|
"rand",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2753,15 +2518,6 @@ dependencies = [
|
|||||||
"windows-sys 0.48.0",
|
"windows-sys 0.48.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "scheduled-thread-pool"
|
|
||||||
version = "0.2.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19"
|
|
||||||
dependencies = [
|
|
||||||
"parking_lot 0.12.1",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "schemars"
|
name = "schemars"
|
||||||
version = "0.8.16"
|
version = "0.8.16"
|
||||||
@ -3678,7 +3434,7 @@ dependencies = [
|
|||||||
"http",
|
"http",
|
||||||
"httparse",
|
"httparse",
|
||||||
"log",
|
"log",
|
||||||
"rand 0.8.5",
|
"rand",
|
||||||
"rustls",
|
"rustls",
|
||||||
"sha1",
|
"sha1",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
|||||||
@ -54,7 +54,6 @@ members = [
|
|||||||
"derive-docs",
|
"derive-docs",
|
||||||
"kcl",
|
"kcl",
|
||||||
"kcl-macros",
|
"kcl-macros",
|
||||||
"execution-plan",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
|
|||||||
@ -1,15 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "execution-plan"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
repository = "https://github.com/KittyCAD/modeling-app"
|
|
||||||
rust-version = "1.73"
|
|
||||||
description = "A DSL for composing KittyCAD API queries"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
bytes = "1.5"
|
|
||||||
kittycad = { workspace = true, features = ["requests"] }
|
|
||||||
kittycad-modeling-cmds = "0.1.4"
|
|
||||||
serde = { version = "1", features = ["derive"] }
|
|
||||||
thiserror = "1"
|
|
||||||
uuid = "1.6.1"
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
use crate::{ExecutionError, Value};
|
|
||||||
|
|
||||||
mod impls;
|
|
||||||
|
|
||||||
/// Types that can be written to or read from KCEP program memory,
|
|
||||||
/// but require multiple values to store.
|
|
||||||
/// They get laid out into multiple consecutive memory addresses.
|
|
||||||
pub trait Composite: Sized {
|
|
||||||
/// Store the value in memory.
|
|
||||||
fn into_parts(self) -> Vec<Value>;
|
|
||||||
/// Read the value from memory.
|
|
||||||
fn from_parts(values: &[Option<Value>]) -> Result<Self, ExecutionError>;
|
|
||||||
}
|
|
||||||
@ -1,98 +0,0 @@
|
|||||||
use kittycad_modeling_cmds::{id::ModelingCmdId, shared::Point3d, MovePathPen};
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
use crate::{Address, ExecutionError, Value};
|
|
||||||
|
|
||||||
use super::Composite;
|
|
||||||
|
|
||||||
impl<T> Composite for kittycad_modeling_cmds::shared::Point3d<T>
|
|
||||||
where
|
|
||||||
Value: From<T>,
|
|
||||||
T: TryFrom<Value, Error = ExecutionError>,
|
|
||||||
{
|
|
||||||
fn into_parts(self) -> Vec<Value> {
|
|
||||||
let points = [self.x, self.y, self.z];
|
|
||||||
points.into_iter().map(|component| component.into()).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn from_parts(values: &[Option<Value>]) -> Result<Self, ExecutionError> {
|
|
||||||
let err = ExecutionError::MemoryWrongSize { expected: 3 };
|
|
||||||
let [x, y, z] = [0, 1, 2].map(|n| values.get(n).ok_or(err.clone()));
|
|
||||||
let x = x?.to_owned().ok_or(err.clone())?.try_into()?;
|
|
||||||
let y = y?.to_owned().ok_or(err.clone())?.try_into()?;
|
|
||||||
let z = z?.to_owned().ok_or(err.clone())?.try_into()?;
|
|
||||||
Ok(Self { x, y, z })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const START_PATH: &str = "StartPath";
|
|
||||||
const MOVE_PATH_PEN: &str = "MovePathPen";
|
|
||||||
|
|
||||||
impl Composite for MovePathPen {
|
|
||||||
fn into_parts(self) -> Vec<Value> {
|
|
||||||
let MovePathPen { path, to } = self;
|
|
||||||
let to = to.into_parts();
|
|
||||||
let mut vals = Vec::with_capacity(1 + to.len());
|
|
||||||
vals.push(Value::Uuid(path.into()));
|
|
||||||
vals.extend(to);
|
|
||||||
vals
|
|
||||||
}
|
|
||||||
|
|
||||||
fn from_parts(values: &[Option<Value>]) -> Result<Self, ExecutionError> {
|
|
||||||
let path = get_some(values, 0)?;
|
|
||||||
let path = Uuid::try_from(path)?;
|
|
||||||
let path = ModelingCmdId::from(path);
|
|
||||||
let to = Point3d::from_parts(&values[1..])?;
|
|
||||||
let params = MovePathPen { path, to };
|
|
||||||
Ok(params)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Memory layout for modeling commands:
|
|
||||||
/// Field 0 is the command's name.
|
|
||||||
/// Fields 1 onwards are the command's fields.
|
|
||||||
impl Composite for kittycad_modeling_cmds::ModelingCmd {
|
|
||||||
fn into_parts(self) -> Vec<Value> {
|
|
||||||
let (endpoint_name, params) = match self {
|
|
||||||
kittycad_modeling_cmds::ModelingCmd::StartPath => (START_PATH, vec![]),
|
|
||||||
kittycad_modeling_cmds::ModelingCmd::MovePathPen(MovePathPen { path, to }) => {
|
|
||||||
let to = to.into_parts();
|
|
||||||
let mut vals = Vec::with_capacity(1 + to.len());
|
|
||||||
vals.push(Value::Uuid(path.into()));
|
|
||||||
vals.extend(to);
|
|
||||||
(MOVE_PATH_PEN, vals)
|
|
||||||
}
|
|
||||||
_ => todo!(),
|
|
||||||
};
|
|
||||||
let mut out = Vec::with_capacity(params.len() + 1);
|
|
||||||
out.push(endpoint_name.to_owned().into());
|
|
||||||
out.extend(params);
|
|
||||||
out
|
|
||||||
}
|
|
||||||
|
|
||||||
fn from_parts(values: &[Option<Value>]) -> Result<Self, ExecutionError> {
|
|
||||||
// Check the array has an element at index 0
|
|
||||||
let first_memory = values
|
|
||||||
.get(0)
|
|
||||||
.ok_or(ExecutionError::MemoryWrongSize { expected: 1 })?
|
|
||||||
.to_owned();
|
|
||||||
// The element should be Some
|
|
||||||
let first_memory = first_memory.ok_or(ExecutionError::MemoryWrongSize { expected: 1 })?;
|
|
||||||
let endpoint_name: String = first_memory.try_into()?;
|
|
||||||
match endpoint_name.as_str() {
|
|
||||||
START_PATH => Ok(Self::StartPath),
|
|
||||||
MOVE_PATH_PEN => {
|
|
||||||
let params = MovePathPen::from_parts(&values[1..])?;
|
|
||||||
Ok(Self::MovePathPen(params))
|
|
||||||
}
|
|
||||||
other => Err(ExecutionError::UnrecognizedEndpoint(other.to_owned())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_some(values: &[Option<Value>], i: usize) -> Result<Value, ExecutionError> {
|
|
||||||
let addr = Address(0); // TODO: pass the `start` addr in
|
|
||||||
let v = values.get(i).ok_or(ExecutionError::MemoryEmpty { addr })?.to_owned();
|
|
||||||
let v = v.ok_or(ExecutionError::MemoryEmpty { addr })?.to_owned();
|
|
||||||
Ok(v)
|
|
||||||
}
|
|
||||||
@ -1,323 +0,0 @@
|
|||||||
//! A KittyCAD execution plan (KCEP) is a list of
|
|
||||||
//! - KittyCAD API requests to make
|
|
||||||
//! - Values to send in API requests
|
|
||||||
//! - Values to assign from API responses
|
|
||||||
//! - Computation to perform on values
|
|
||||||
//! You can think of it as a domain-specific language for making KittyCAD API calls and using
|
|
||||||
//! the results to make other API calls.
|
|
||||||
|
|
||||||
use composite::Composite;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::fmt;
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
mod composite;
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests;
|
|
||||||
|
|
||||||
/// KCEP's program memory. A flat, linear list of values.
|
|
||||||
#[derive(Debug)]
|
|
||||||
#[cfg_attr(test, derive(PartialEq))]
|
|
||||||
pub struct Memory(Vec<Option<Value>>);
|
|
||||||
|
|
||||||
impl Default for Memory {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self(vec![None; 1024])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// An address in KCEP's program memory.
|
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
|
||||||
pub struct Address(usize);
|
|
||||||
|
|
||||||
impl fmt::Display for Address {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
self.0.fmt(f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<usize> for Address {
|
|
||||||
fn from(value: usize) -> Self {
|
|
||||||
Self(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Memory {
|
|
||||||
/// Get a value from KCEP's program memory.
|
|
||||||
pub fn get(&self, Address(addr): &Address) -> Option<&Value> {
|
|
||||||
self.0[*addr].as_ref()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Store a value in KCEP's program memory.
|
|
||||||
pub fn set(&mut self, Address(addr): Address, value: Value) {
|
|
||||||
// If isn't big enough for this value, double the size of memory until it is.
|
|
||||||
while addr > self.0.len() {
|
|
||||||
self.0.extend(vec![None; self.0.len()]);
|
|
||||||
}
|
|
||||||
self.0[addr] = Some(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Store a composite value (i.e. a value which takes up multiple addresses in memory).
|
|
||||||
/// Store its parts in consecutive memory addresses starting at `start`.
|
|
||||||
pub fn set_composite<T: Composite>(&mut self, composite_value: T, start: Address) {
|
|
||||||
let parts = composite_value.into_parts().into_iter();
|
|
||||||
for (value, addr) in parts.zip(start.0..) {
|
|
||||||
self.0[addr] = Some(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a composite value (i.e. a value which takes up multiple addresses in memory).
|
|
||||||
/// Its parts are stored in consecutive memory addresses starting at `start`.
|
|
||||||
pub fn get_composite<T: Composite>(&self, start: Address) -> Result<T, ExecutionError> {
|
|
||||||
let values = &self.0[start.0..];
|
|
||||||
T::from_parts(values)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A value stored in KCEP program memory.
|
|
||||||
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
|
|
||||||
pub enum Value {
|
|
||||||
String(String),
|
|
||||||
NumericValue(NumericValue),
|
|
||||||
Uuid(Uuid),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Uuid> for Value {
|
|
||||||
fn from(u: Uuid) -> Self {
|
|
||||||
Self::Uuid(u)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<String> for Value {
|
|
||||||
fn from(value: String) -> Self {
|
|
||||||
Self::String(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<f64> for Value {
|
|
||||||
fn from(value: f64) -> Self {
|
|
||||||
Self::NumericValue(NumericValue::Float(value))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<Value> for String {
|
|
||||||
type Error = ExecutionError;
|
|
||||||
|
|
||||||
fn try_from(value: Value) -> Result<Self, Self::Error> {
|
|
||||||
if let Value::String(s) = value {
|
|
||||||
Ok(s)
|
|
||||||
} else {
|
|
||||||
Err(ExecutionError::MemoryWrongType {
|
|
||||||
expected: "string",
|
|
||||||
actual: format!("{value:?}"),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<Value> for Uuid {
|
|
||||||
type Error = ExecutionError;
|
|
||||||
|
|
||||||
fn try_from(value: Value) -> Result<Self, Self::Error> {
|
|
||||||
if let Value::Uuid(u) = value {
|
|
||||||
Ok(u)
|
|
||||||
} else {
|
|
||||||
Err(ExecutionError::MemoryWrongType {
|
|
||||||
expected: "uuid",
|
|
||||||
actual: format!("{value:?}"),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl TryFrom<Value> for f64 {
|
|
||||||
type Error = ExecutionError;
|
|
||||||
|
|
||||||
fn try_from(value: Value) -> Result<Self, Self::Error> {
|
|
||||||
if let Value::NumericValue(NumericValue::Float(x)) = value {
|
|
||||||
Ok(x)
|
|
||||||
} else {
|
|
||||||
Err(ExecutionError::MemoryWrongType {
|
|
||||||
expected: "float",
|
|
||||||
actual: format!("{value:?}"),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
impl From<usize> for Value {
|
|
||||||
fn from(value: usize) -> Self {
|
|
||||||
Self::NumericValue(NumericValue::Integer(value))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
|
|
||||||
pub enum NumericValue {
|
|
||||||
Integer(usize),
|
|
||||||
Float(f64),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// One step of the execution plan.
|
|
||||||
#[derive(Serialize, Deserialize)]
|
|
||||||
pub enum Instruction {
|
|
||||||
/// Call the KittyCAD API.
|
|
||||||
ApiRequest {
|
|
||||||
/// Which ModelingCmd to call.
|
|
||||||
/// It's a composite value starting at the given address.
|
|
||||||
endpoint: Address,
|
|
||||||
/// Which address should the response be stored in?
|
|
||||||
store_response: Option<usize>,
|
|
||||||
/// Look up each API request in this register number.
|
|
||||||
arguments: Vec<Address>,
|
|
||||||
},
|
|
||||||
/// Set a value in memory.
|
|
||||||
Set {
|
|
||||||
/// Which memory address to set.
|
|
||||||
address: Address,
|
|
||||||
/// What value to set the memory address to.
|
|
||||||
value: Value,
|
|
||||||
},
|
|
||||||
/// Perform arithmetic on values in memory.
|
|
||||||
Arithmetic {
|
|
||||||
/// What to do.
|
|
||||||
arithmetic: Arithmetic,
|
|
||||||
/// Write the output to this memory address.
|
|
||||||
destination: Address,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Instruction to perform arithmetic on values in memory.
|
|
||||||
#[derive(Deserialize, Serialize)]
|
|
||||||
pub struct Arithmetic {
|
|
||||||
/// Apply this operation
|
|
||||||
pub operation: Operation,
|
|
||||||
/// First operand for the operation
|
|
||||||
pub operand0: Operand,
|
|
||||||
/// Second operand for the operation
|
|
||||||
pub operand1: Operand,
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! arithmetic_body {
|
|
||||||
($arith:ident, $mem:ident, $method:ident) => {
|
|
||||||
match (
|
|
||||||
$arith.operand0.eval(&$mem)?.clone(),
|
|
||||||
$arith.operand1.eval(&$mem)?.clone(),
|
|
||||||
) {
|
|
||||||
// If both operands are numeric, then do the arithmetic operation.
|
|
||||||
(Value::NumericValue(x), Value::NumericValue(y)) => {
|
|
||||||
let num = match (x, y) {
|
|
||||||
(NumericValue::Integer(x), NumericValue::Integer(y)) => NumericValue::Integer(x.$method(y)),
|
|
||||||
(NumericValue::Integer(x), NumericValue::Float(y)) => NumericValue::Float((x as f64).$method(y)),
|
|
||||||
(NumericValue::Float(x), NumericValue::Integer(y)) => NumericValue::Float(x.$method(y as f64)),
|
|
||||||
(NumericValue::Float(x), NumericValue::Float(y)) => NumericValue::Float(x.$method(y)),
|
|
||||||
};
|
|
||||||
Ok(Value::NumericValue(num))
|
|
||||||
}
|
|
||||||
// This operation can only be done on numeric types.
|
|
||||||
_ => Err(ExecutionError::CannotApplyOperation {
|
|
||||||
op: $arith.operation,
|
|
||||||
operands: vec![
|
|
||||||
$arith.operand0.eval(&$mem)?.clone().to_owned(),
|
|
||||||
$arith.operand1.eval(&$mem)?.clone().to_owned(),
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
impl Arithmetic {
|
|
||||||
/// Calculate the the arithmetic equation.
|
|
||||||
/// May read values from the given memory.
|
|
||||||
fn calculate(self, mem: &Memory) -> Result<Value, ExecutionError> {
|
|
||||||
use std::ops::{Add, Div, Mul, Sub};
|
|
||||||
match self.operation {
|
|
||||||
Operation::Add => {
|
|
||||||
arithmetic_body!(self, mem, add)
|
|
||||||
}
|
|
||||||
Operation::Mul => {
|
|
||||||
arithmetic_body!(self, mem, mul)
|
|
||||||
}
|
|
||||||
Operation::Sub => {
|
|
||||||
arithmetic_body!(self, mem, sub)
|
|
||||||
}
|
|
||||||
Operation::Div => {
|
|
||||||
arithmetic_body!(self, mem, div)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Operations that can be applied to values in memory.
|
|
||||||
#[derive(Debug, Deserialize, Serialize, Clone, Copy)]
|
|
||||||
pub enum Operation {
|
|
||||||
Add,
|
|
||||||
Mul,
|
|
||||||
Sub,
|
|
||||||
Div,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for Operation {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
match self {
|
|
||||||
Operation::Add => "+",
|
|
||||||
Operation::Mul => "*",
|
|
||||||
Operation::Sub => "-",
|
|
||||||
Operation::Div => "/",
|
|
||||||
}
|
|
||||||
.fmt(f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Argument to an operation.
|
|
||||||
#[derive(Deserialize, Serialize)]
|
|
||||||
pub enum Operand {
|
|
||||||
Literal(Value),
|
|
||||||
Reference(Address),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Operand {
|
|
||||||
/// Evaluate the operand, getting its value.
|
|
||||||
fn eval(&self, mem: &Memory) -> Result<Value, ExecutionError> {
|
|
||||||
match self {
|
|
||||||
Operand::Literal(v) => Ok(v.to_owned()),
|
|
||||||
Operand::Reference(addr) => match mem.get(addr) {
|
|
||||||
None => Err(ExecutionError::MemoryEmpty { addr: *addr }),
|
|
||||||
Some(v) => Ok(v.to_owned()),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Execute the plan.
|
|
||||||
pub fn execute(mem: &mut Memory, plan: Vec<Instruction>) -> Result<(), ExecutionError> {
|
|
||||||
for step in plan {
|
|
||||||
match step {
|
|
||||||
Instruction::ApiRequest { .. } => todo!("Execute API calls"),
|
|
||||||
Instruction::Set { address, value } => {
|
|
||||||
mem.set(address, value);
|
|
||||||
}
|
|
||||||
Instruction::Arithmetic {
|
|
||||||
arithmetic,
|
|
||||||
destination,
|
|
||||||
} => {
|
|
||||||
let out = arithmetic.calculate(mem)?;
|
|
||||||
mem.set(destination, out);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Errors that could occur when executing a KittyCAD execution plan.
|
|
||||||
#[derive(Debug, thiserror::Error, Clone)]
|
|
||||||
pub enum ExecutionError {
|
|
||||||
#[error("Memory address {addr} was not set")]
|
|
||||||
MemoryEmpty { addr: Address },
|
|
||||||
#[error("Cannot apply operation {op} to operands {operands:?}")]
|
|
||||||
CannotApplyOperation { op: Operation, operands: Vec<Value> },
|
|
||||||
#[error("Tried to read a '{expected}' from KCEP program memory, found an '{actual}' instead")]
|
|
||||||
MemoryWrongType { expected: &'static str, actual: String },
|
|
||||||
#[error("Wanted {expected} values but did not get enough")]
|
|
||||||
MemoryWrongSize { expected: usize },
|
|
||||||
#[error("No endpoint {0} recognized")]
|
|
||||||
UnrecognizedEndpoint(String),
|
|
||||||
}
|
|
||||||
@ -1,117 +0,0 @@
|
|||||||
use kittycad_modeling_cmds::{id::ModelingCmdId, shared::Point3d, ModelingCmd, MovePathPen};
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn write_addr_to_memory() {
|
|
||||||
let plan = vec![Instruction::Set {
|
|
||||||
address: Address(0),
|
|
||||||
value: 3.4.into(),
|
|
||||||
}];
|
|
||||||
let mut mem = Memory::default();
|
|
||||||
execute(&mut mem, plan).unwrap();
|
|
||||||
assert_eq!(mem.get(&Address(0)), Some(&3.4.into()))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn add_literals() {
|
|
||||||
let plan = vec![Instruction::Arithmetic {
|
|
||||||
arithmetic: Arithmetic {
|
|
||||||
operation: Operation::Add,
|
|
||||||
operand0: Operand::Literal(3.into()),
|
|
||||||
operand1: Operand::Literal(2.into()),
|
|
||||||
},
|
|
||||||
destination: Address(1),
|
|
||||||
}];
|
|
||||||
let mut mem = Memory::default();
|
|
||||||
execute(&mut mem, plan).unwrap();
|
|
||||||
assert_eq!(mem.get(&Address(1)), Some(&5.into()))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn add_literal_to_reference() {
|
|
||||||
let plan = vec![
|
|
||||||
// Memory addr 0 contains 450
|
|
||||||
Instruction::Set {
|
|
||||||
address: Address(0),
|
|
||||||
value: 450.into(),
|
|
||||||
},
|
|
||||||
// Add 20 to addr 0
|
|
||||||
Instruction::Arithmetic {
|
|
||||||
arithmetic: Arithmetic {
|
|
||||||
operation: Operation::Add,
|
|
||||||
operand0: Operand::Reference(Address(0)),
|
|
||||||
operand1: Operand::Literal(20.into()),
|
|
||||||
},
|
|
||||||
destination: Address(1),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
// 20 + 450 = 470
|
|
||||||
let mut mem = Memory::default();
|
|
||||||
execute(&mut mem, plan).unwrap();
|
|
||||||
assert_eq!(mem.get(&Address(1)), Some(&470.into()))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn add_to_composite_value() {
|
|
||||||
let mut mem = Memory::default();
|
|
||||||
|
|
||||||
// Write a point to memory.
|
|
||||||
let point_before = Point3d {
|
|
||||||
x: 2.0f64,
|
|
||||||
y: 3.0,
|
|
||||||
z: 4.0,
|
|
||||||
};
|
|
||||||
let start_addr = Address(0);
|
|
||||||
mem.set_composite(point_before, start_addr);
|
|
||||||
assert_eq!(mem.0[0], Some(2.0.into()));
|
|
||||||
assert_eq!(mem.0[1], Some(3.0.into()));
|
|
||||||
assert_eq!(mem.0[2], Some(4.0.into()));
|
|
||||||
|
|
||||||
// Update the point's x-value in memory.
|
|
||||||
execute(
|
|
||||||
&mut mem,
|
|
||||||
vec![Instruction::Arithmetic {
|
|
||||||
arithmetic: Arithmetic {
|
|
||||||
operation: Operation::Add,
|
|
||||||
operand0: Operand::Reference(start_addr),
|
|
||||||
operand1: Operand::Literal(40.into()),
|
|
||||||
},
|
|
||||||
destination: start_addr,
|
|
||||||
}],
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// Read the point out of memory, validate it.
|
|
||||||
let point_after: Point3d<f64> = mem.get_composite(start_addr).unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
point_after,
|
|
||||||
Point3d {
|
|
||||||
x: 42.0,
|
|
||||||
y: 3.0,
|
|
||||||
z: 4.0
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn api_types() {
|
|
||||||
let mut mem = Memory::default();
|
|
||||||
let start_addr = Address(0);
|
|
||||||
let id = ModelingCmdId(Uuid::parse_str("6306afa2-3999-4b03-af30-1efad7cdc6fc").unwrap());
|
|
||||||
let p = Point3d {
|
|
||||||
x: 2.0f64,
|
|
||||||
y: 3.0,
|
|
||||||
z: 4.0,
|
|
||||||
};
|
|
||||||
let val_in = ModelingCmd::MovePathPen(MovePathPen { path: id, to: p });
|
|
||||||
mem.set_composite(val_in, start_addr);
|
|
||||||
let val_out: ModelingCmd = mem.get_composite(start_addr).unwrap();
|
|
||||||
match val_out {
|
|
||||||
ModelingCmd::MovePathPen(params) => {
|
|
||||||
assert_eq!(params.to, p);
|
|
||||||
assert_eq!(params.path, id);
|
|
||||||
}
|
|
||||||
_ => panic!("unexpected ModelingCmd variant"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -8497,9 +8497,9 @@ ws@^8.8.0:
|
|||||||
integrity sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==
|
integrity sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==
|
||||||
|
|
||||||
xstate@^4.38.2:
|
xstate@^4.38.2:
|
||||||
version "4.38.2"
|
version "4.38.3"
|
||||||
resolved "https://registry.yarnpkg.com/xstate/-/xstate-4.38.2.tgz#1b74544fc9c8c6c713ba77f81c6017e65aa89804"
|
resolved "https://registry.yarnpkg.com/xstate/-/xstate-4.38.3.tgz#4e15e7ad3aa0ca1eea2010548a5379966d8f1075"
|
||||||
integrity sha512-Fba/DwEPDLneHT3tbJ9F3zafbQXszOlyCJyQqqdzmtlY/cwE2th462KK48yaANf98jHlP6lJvxfNtN0LFKXPQg==
|
integrity sha512-SH7nAaaPQx57dx6qvfcIgqKRXIh4L0A1iYEqim4s1u7c9VoCgzZc+63FY90AKU4ZzOC2cfJzTnpO4zK7fCUzzw==
|
||||||
|
|
||||||
y18n@^5.0.5:
|
y18n@^5.0.5:
|
||||||
version "5.0.8"
|
version "5.0.8"
|
||||||
|
|||||||